1<?php
2/**
3 * A PHP_CodeSniffer_File object represents a PHP source file and the tokens
4 * associated with it.
5 *
6 * PHP version 5
7 *
8 * @category  PHP
9 * @package   PHP_CodeSniffer
10 * @author    Greg Sherwood <gsherwood@squiz.net>
11 * @author    Marc McIntyre <mmcintyre@squiz.net>
12 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
13 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
14 * @link      http://pear.php.net/package/PHP_CodeSniffer
15 */
16
17/**
18 * A PHP_CodeSniffer_File object represents a PHP source file and the tokens
19 * associated with it.
20 *
21 * It provides a means for traversing the token stack, along with
22 * other token related operations. If a PHP_CodeSniffer_Sniff finds and error or
23 *  warning within a PHP_CodeSniffer_File, you can raise an error using the
24 *  addError() or addWarning() methods.
25 *
26 * <b>Token Information</b>
27 *
28 * Each token within the stack contains information about itself:
29 *
30 * <code>
31 *   array(
32 *    'code'       => 301,       // the token type code (see token_get_all())
33 *    'content'    => 'if',      // the token content
34 *    'type'       => 'T_IF',    // the token name
35 *    'line'       => 56,        // the line number when the token is located
36 *    'column'     => 12,        // the column in the line where this token
37 *                               // starts (starts from 1)
38 *    'level'      => 2          // the depth a token is within the scopes open
39 *    'conditions' => array(     // a list of scope condition token
40 *                               // positions => codes that
41 *                     2 => 50,  // opened the scopes that this token exists
42 *                     9 => 353, // in (see conditional tokens section below)
43 *                    ),
44 *   );
45 * </code>
46 *
47 * <b>Conditional Tokens</b>
48 *
49 * In addition to the standard token fields, conditions contain information to
50 * determine where their scope begins and ends:
51 *
52 * <code>
53 *   array(
54 *    'scope_condition' => 38, // the token position of the condition
55 *    'scope_opener'    => 41, // the token position that started the scope
56 *    'scope_closer'    => 70, // the token position that ended the scope
57 *   );
58 * </code>
59 *
60 * The condition, the scope opener and the scope closer each contain this
61 * information.
62 *
63 * <b>Parenthesis Tokens</b>
64 *
65 * Each parenthesis token (T_OPEN_PARENTHESIS and T_CLOSE_PARENTHESIS) has a
66 * reference to their opening and closing parenthesis, one being itself, the
67 * other being its opposite.
68 *
69 * <code>
70 *   array(
71 *    'parenthesis_opener' => 34,
72 *    'parenthesis_closer' => 40,
73 *   );
74 * </code>
75 *
76 * Some tokens can "own" a set of parenthesis. For example a T_FUNCTION token
77 * has parenthesis around its argument list. These tokens also have the
78 * parenthesis_opener and and parenthesis_closer indices. Not all parenthesis
79 * have owners, for example parenthesis used for arithmetic operations and
80 * function calls. The parenthesis tokens that have an owner have the following
81 * auxiliary array indices.
82 *
83 * <code>
84 *   array(
85 *    'parenthesis_opener' => 34,
86 *    'parenthesis_closer' => 40,
87 *    'parenthesis_owner'  => 33,
88 *   );
89 * </code>
90 *
91 * Each token within a set of parenthesis also has an array index
92 * 'nested_parenthesis' which is an array of the
93 * left parenthesis => right parenthesis token positions.
94 *
95 * <code>
96 *   'nested_parenthesis' => array(
97 *                             12 => 15
98 *                             11 => 14
99 *                            );
100 * </code>
101 *
102 * <b>Extended Tokens</b>
103 *
104 * PHP_CodeSniffer extends and augments some of the tokens created by
105 * <i>token_get_all()</i>. A full list of these tokens can be seen in the
106 * <i>Tokens.php</i> file.
107 *
108 * @category  PHP
109 * @package   PHP_CodeSniffer
110 * @author    Greg Sherwood <gsherwood@squiz.net>
111 * @author    Marc McIntyre <mmcintyre@squiz.net>
112 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
113 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
114 * @version   Release: @package_version@
115 * @link      http://pear.php.net/package/PHP_CodeSniffer
116 */
117class PHP_CodeSniffer_File
118{
119
120    /**
121     * The absolute path to the file associated with this object.
122     *
123     * @var string
124     */
125    private $_file = '';
126
127    /**
128     * The EOL character this file uses.
129     *
130     * @var string
131     */
132    public $eolChar = '';
133
134    /**
135     * The PHP_CodeSniffer object controlling this run.
136     *
137     * @var PHP_CodeSniffer
138     */
139    public $phpcs = null;
140
141    /**
142     * The Fixer object to control fixing errors.
143     *
144     * @var PHP_CodeSniffer_Fixer
145     */
146    public $fixer = null;
147
148    /**
149     * The tokenizer being used for this file.
150     *
151     * @var object
152     */
153    public $tokenizer = null;
154
155    /**
156     * The tokenizer being used for this file.
157     *
158     * @var string
159     */
160    public $tokenizerType = 'PHP';
161
162    /**
163     * The number of tokens in this file.
164     *
165     * Stored here to save calling count() everywhere.
166     *
167     * @var int
168     */
169    public $numTokens = 0;
170
171    /**
172     * The tokens stack map.
173     *
174     * Note that the tokens in this array differ in format to the tokens
175     * produced by token_get_all(). Tokens are initially produced with
176     * token_get_all(), then augmented so that it's easier to process them.
177     *
178     * @var array()
179     * @see Tokens.php
180     */
181    private $_tokens = array();
182
183    /**
184     * The errors raised from PHP_CodeSniffer_Sniffs.
185     *
186     * @var array()
187     * @see getErrors()
188     */
189    private $_errors = array();
190
191    /**
192     * The warnings raised from PHP_CodeSniffer_Sniffs.
193     *
194     * @var array()
195     * @see getWarnings()
196     */
197    private $_warnings = array();
198
199    /**
200     * The metrics recorded from PHP_CodeSniffer_Sniffs.
201     *
202     * @var array()
203     * @see getMetrics()
204     */
205    private $_metrics = array();
206
207    /**
208     * Record the errors and warnings raised.
209     *
210     * @var bool
211     */
212    private $_recordErrors = true;
213
214    /**
215     * An array of lines that are being ignored.
216     *
217     * @var array()
218     */
219    private static $_ignoredLines = array();
220
221    /**
222     * An array of sniffs that are being ignored.
223     *
224     * @var array()
225     */
226    private $_ignoredListeners = array();
227
228    /**
229     * An array of message codes that are being ignored.
230     *
231     * @var array()
232     */
233    private $_ignoredCodes = array();
234
235    /**
236     * The total number of errors raised.
237     *
238     * @var int
239     */
240    private $_errorCount = 0;
241
242    /**
243     * The total number of warnings raised.
244     *
245     * @var int
246     */
247    private $_warningCount = 0;
248
249    /**
250     * The total number of errors/warnings that can be fixed.
251     *
252     * @var int
253     */
254    private $_fixableCount = 0;
255
256    /**
257     * An array of sniffs listening to this file's processing.
258     *
259     * @var array(PHP_CodeSniffer_Sniff)
260     */
261    private $_listeners = array();
262
263    /**
264     * The class name of the sniff currently processing the file.
265     *
266     * @var string
267     */
268    private $_activeListener = '';
269
270    /**
271     * An array of sniffs being processed and how long they took.
272     *
273     * @var array()
274     */
275    private $_listenerTimes = array();
276
277    /**
278     * An array of rules from the ruleset.xml file.
279     *
280     * This value gets set by PHP_CodeSniffer when the object is created.
281     * It may be empty, indicating that the ruleset does not override
282     * any of the default sniff settings.
283     *
284     * @var array
285     */
286    protected $ruleset = array();
287
288
289    /**
290     * Constructs a PHP_CodeSniffer_File.
291     *
292     * @param string          $file      The absolute path to the file to process.
293     * @param array(string)   $listeners The initial listeners listening to processing of this file.
294     *                                   to processing of this file.
295     * @param array           $ruleset   An array of rules from the ruleset.xml file.
296     *                                   ruleset.xml file.
297     * @param PHP_CodeSniffer $phpcs     The PHP_CodeSniffer object controlling this run.
298     *                                   this run.
299     *
300     * @throws PHP_CodeSniffer_Exception If the register() method does
301     *                                   not return an array.
302     */
303    public function __construct(
304        $file,
305        array $listeners,
306        array $ruleset,
307        PHP_CodeSniffer $phpcs
308    ) {
309        $this->_file      = trim($file);
310        $this->_listeners = $listeners;
311        $this->ruleset    = $ruleset;
312        $this->phpcs      = $phpcs;
313        $this->fixer      = new PHP_CodeSniffer_Fixer();
314
315        if (PHP_CODESNIFFER_INTERACTIVE === false) {
316            $cliValues = $phpcs->cli->getCommandLineValues();
317            if (isset($cliValues['showSources']) === true
318                && $cliValues['showSources'] !== true
319            ) {
320                $recordErrors = false;
321                foreach ($cliValues['reports'] as $report => $output) {
322                    $reportClass = $phpcs->reporting->factory($report);
323                    if (property_exists($reportClass, 'recordErrors') === false
324                        || $reportClass->recordErrors === true
325                    ) {
326                        $recordErrors = true;
327                        break;
328                    }
329                }
330
331                $this->_recordErrors = $recordErrors;
332            }
333        }
334
335    }//end __construct()
336
337
338    /**
339     * Sets the name of the currently active sniff.
340     *
341     * @param string $activeListener The class name of the current sniff.
342     *
343     * @return void
344     */
345    public function setActiveListener($activeListener)
346    {
347        $this->_activeListener = $activeListener;
348
349    }//end setActiveListener()
350
351
352    /**
353     * Adds a listener to the token stack that listens to the specific tokens.
354     *
355     * When PHP_CodeSniffer encounters on the the tokens specified in $tokens,
356     * it invokes the process method of the sniff.
357     *
358     * @param PHP_CodeSniffer_Sniff $listener The listener to add to the
359     *                                        listener stack.
360     * @param array(int)            $tokens   The token types the listener wishes to
361     *                                        listen to.
362     *
363     * @return void
364     */
365    public function addTokenListener(PHP_CodeSniffer_Sniff $listener, array $tokens)
366    {
367        $class = get_class($listener);
368        foreach ($tokens as $token) {
369            if (isset($this->_listeners[$token]) === false) {
370                $this->_listeners[$token] = array();
371            }
372
373            if (isset($this->_listeners[$token][$class]) === false) {
374                $this->_listeners[$token][$class] = $listener;
375            }
376        }
377
378    }//end addTokenListener()
379
380
381    /**
382     * Removes a listener from listening from the specified tokens.
383     *
384     * @param PHP_CodeSniffer_Sniff $listener The listener to remove from the
385     *                                        listener stack.
386     * @param array(int)            $tokens   The token types the listener wishes to
387     *                                        stop listen to.
388     *
389     * @return void
390     */
391    public function removeTokenListener(
392        PHP_CodeSniffer_Sniff $listener,
393        array $tokens
394    ) {
395        $class = get_class($listener);
396        foreach ($tokens as $token) {
397            if (isset($this->_listeners[$token]) === false) {
398                continue;
399            }
400
401            unset($this->_listeners[$token][$class]);
402        }
403
404    }//end removeTokenListener()
405
406
407    /**
408     * Rebuilds the list of listeners to ensure their state is cleared.
409     *
410     * @return void
411     */
412    public function refreshTokenListeners()
413    {
414        $this->phpcs->populateTokenListeners();
415        $this->_listeners = $this->phpcs->getTokenSniffs();
416
417    }//end refreshTokenListeners()
418
419
420    /**
421     * Returns the token stack for this file.
422     *
423     * @return array
424     */
425    public function getTokens()
426    {
427        return $this->_tokens;
428
429    }//end getTokens()
430
431
432    /**
433     * Starts the stack traversal and tells listeners when tokens are found.
434     *
435     * @param string $contents The contents to parse. If NULL, the content
436     *                         is taken from the file system.
437     *
438     * @return void
439     */
440    public function start($contents=null)
441    {
442        $this->_errors       = array();
443        $this->_warnings     = array();
444        $this->_errorCount   = 0;
445        $this->_warningCount = 0;
446        $this->_fixableCount = 0;
447
448        // Reset the ignored lines because lines numbers may have changed
449        // if we are fixing this file.
450        self::$_ignoredLines = array();
451
452        try {
453            $this->eolChar = self::detectLineEndings($this->_file, $contents);
454        } catch (PHP_CodeSniffer_Exception $e) {
455            $this->addWarning($e->getMessage(), null, 'Internal.DetectLineEndings');
456            return;
457        }
458
459        // If this is standard input, see if a filename was passed in as well.
460        // This is done by including: phpcs_input_file: [file path]
461        // as the first line of content.
462        if ($this->_file === 'STDIN') {
463            $cliValues = $this->phpcs->cli->getCommandLineValues();
464            if ($cliValues['stdinPath'] !== '') {
465                $this->_file = $cliValues['stdinPath'];
466            } else if ($contents !== null && substr($contents, 0, 17) === 'phpcs_input_file:') {
467                $eolPos      = strpos($contents, $this->eolChar);
468                $filename    = trim(substr($contents, 17, ($eolPos - 17)));
469                $contents    = substr($contents, ($eolPos + strlen($this->eolChar)));
470                $this->_file = $filename;
471            }
472        }
473
474        $this->_parse($contents);
475        $this->fixer->startFile($this);
476
477        if (PHP_CODESNIFFER_VERBOSITY > 2) {
478            echo "\t*** START TOKEN PROCESSING ***".PHP_EOL;
479        }
480
481        $foundCode        = false;
482        $listeners        = $this->phpcs->getSniffs();
483        $listenerIgnoreTo = array();
484        $inTests          = defined('PHP_CODESNIFFER_IN_TESTS');
485
486        // Foreach of the listeners that have registered to listen for this
487        // token, get them to process it.
488        foreach ($this->_tokens as $stackPtr => $token) {
489            // Check for ignored lines.
490            if ($token['code'] === T_COMMENT
491                || $token['code'] === T_DOC_COMMENT_TAG
492                || ($inTests === true && $token['code'] === T_INLINE_HTML)
493            ) {
494                if (strpos($token['content'], '@codingStandards') !== false) {
495                    if (strpos($token['content'], '@codingStandardsIgnoreFile') !== false) {
496                        // Ignoring the whole file, just a little late.
497                        $this->_errors       = array();
498                        $this->_warnings     = array();
499                        $this->_errorCount   = 0;
500                        $this->_warningCount = 0;
501                        $this->_fixableCount = 0;
502                        return;
503                    } else if (strpos($token['content'], '@codingStandardsChangeSetting') !== false) {
504                        $start   = strpos($token['content'], '@codingStandardsChangeSetting');
505                        $comment = substr($token['content'], ($start + 30));
506                        $parts   = explode(' ', $comment);
507                        if (count($parts) >= 3
508                            && isset($this->phpcs->sniffCodes[$parts[0]]) === true
509                        ) {
510                            $listenerCode  = array_shift($parts);
511                            $propertyCode  = array_shift($parts);
512                            $propertyValue = rtrim(implode(' ', $parts), " */\r\n");
513                            $listenerClass = $this->phpcs->sniffCodes[$listenerCode];
514                            $this->phpcs->setSniffProperty($listenerClass, $propertyCode, $propertyValue);
515                        }
516                    }//end if
517                }//end if
518            }//end if
519
520            if (PHP_CODESNIFFER_VERBOSITY > 2) {
521                $type    = $token['type'];
522                $content = PHP_CodeSniffer::prepareForOutput($token['content']);
523                echo "\t\tProcess token $stackPtr: $type => $content".PHP_EOL;
524            }
525
526            if ($token['code'] !== T_INLINE_HTML) {
527                $foundCode = true;
528            }
529
530            if (isset($this->_listeners[$token['code']]) === false) {
531                continue;
532            }
533
534            foreach ($this->_listeners[$token['code']] as $listenerData) {
535                if (isset($this->_ignoredListeners[$listenerData['class']]) === true
536                    || (isset($listenerIgnoreTo[$listenerData['class']]) === true
537                    && $listenerIgnoreTo[$listenerData['class']] > $stackPtr)
538                ) {
539                    // This sniff is ignoring past this token, or the whole file.
540                    continue;
541                }
542
543                // Make sure this sniff supports the tokenizer
544                // we are currently using.
545                $class = $listenerData['class'];
546
547                if (isset($listenerData['tokenizers'][$this->tokenizerType]) === false) {
548                    continue;
549                }
550
551                // If the file path matches one of our ignore patterns, skip it.
552                // While there is support for a type of each pattern
553                // (absolute or relative) we don't actually support it here.
554                foreach ($listenerData['ignore'] as $pattern) {
555                    // We assume a / directory separator, as do the exclude rules
556                    // most developers write, so we need a special case for any system
557                    // that is different.
558                    if (DIRECTORY_SEPARATOR === '\\') {
559                        $pattern = str_replace('/', '\\\\', $pattern);
560                    }
561
562                    $pattern = '`'.$pattern.'`i';
563                    if (preg_match($pattern, $this->_file) === 1) {
564                        $this->_ignoredListeners[$class] = true;
565                        continue(2);
566                    }
567                }
568
569                $this->_activeListener = $class;
570
571                if (PHP_CODESNIFFER_VERBOSITY > 2) {
572                    $startTime = microtime(true);
573                    echo "\t\t\tProcessing ".$this->_activeListener.'... ';
574                }
575
576                $ignoreTo = $listeners[$class]->process($this, $stackPtr);
577                if ($ignoreTo !== null) {
578                    $listenerIgnoreTo[$this->_activeListener] = $ignoreTo;
579                }
580
581                if (PHP_CODESNIFFER_VERBOSITY > 2) {
582                    $timeTaken = (microtime(true) - $startTime);
583                    if (isset($this->_listenerTimes[$this->_activeListener]) === false) {
584                        $this->_listenerTimes[$this->_activeListener] = 0;
585                    }
586
587                    $this->_listenerTimes[$this->_activeListener] += $timeTaken;
588
589                    $timeTaken = round(($timeTaken), 4);
590                    echo "DONE in $timeTaken seconds".PHP_EOL;
591                }
592
593                $this->_activeListener = '';
594            }//end foreach
595        }//end foreach
596
597        if ($this->_recordErrors === false) {
598            $this->_errors   = array();
599            $this->_warnings = array();
600        }
601
602        // If short open tags are off but the file being checked uses
603        // short open tags, the whole content will be inline HTML
604        // and nothing will be checked. So try and handle this case.
605        // We don't show this error for STDIN because we can't be sure the content
606        // actually came directly from the user. It could be something like
607        // refs from a Git pre-push hook.
608        if ($foundCode === false && $this->tokenizerType === 'PHP' && $this->_file !== 'STDIN') {
609            $shortTags = (bool) ini_get('short_open_tag');
610            if ($shortTags === false) {
611                $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.';
612                $this->addWarning($error, null, 'Internal.NoCodeFound');
613            }
614        }
615
616        if (PHP_CODESNIFFER_VERBOSITY > 2) {
617            echo "\t*** END TOKEN PROCESSING ***".PHP_EOL;
618            echo "\t*** START SNIFF PROCESSING REPORT ***".PHP_EOL;
619
620            asort($this->_listenerTimes, SORT_NUMERIC);
621            $this->_listenerTimes = array_reverse($this->_listenerTimes, true);
622            foreach ($this->_listenerTimes as $listener => $timeTaken) {
623                echo "\t$listener: ".round(($timeTaken), 4).' secs'.PHP_EOL;
624            }
625
626            echo "\t*** END SNIFF PROCESSING REPORT ***".PHP_EOL;
627        }
628
629    }//end start()
630
631
632    /**
633     * Remove vars stored in this file that are no longer required.
634     *
635     * @return void
636     */
637    public function cleanUp()
638    {
639        $this->_tokens    = null;
640        $this->_listeners = null;
641
642    }//end cleanUp()
643
644
645    /**
646     * Tokenizes the file and prepares it for the test run.
647     *
648     * @param string $contents The contents to parse. If NULL, the content
649     *                         is taken from the file system.
650     *
651     * @return void
652     */
653    private function _parse($contents=null)
654    {
655        if ($contents === null && empty($this->_tokens) === false) {
656            // File has already been parsed.
657            return;
658        }
659
660        $stdin     = false;
661        $cliValues = $this->phpcs->cli->getCommandLineValues();
662        if (empty($cliValues['files']) === true) {
663            $stdin = true;
664        }
665
666        // Determine the tokenizer from the file extension.
667        $fileParts = explode('.', $this->_file);
668        $extension = array_pop($fileParts);
669        if (isset($this->phpcs->allowedFileExtensions[$extension]) === true) {
670            $tokenizerClass      = 'PHP_CodeSniffer_Tokenizers_'.$this->phpcs->allowedFileExtensions[$extension];
671            $this->tokenizerType = $this->phpcs->allowedFileExtensions[$extension];
672        } else if (isset($this->phpcs->defaultFileExtensions[$extension]) === true) {
673            $tokenizerClass      = 'PHP_CodeSniffer_Tokenizers_'.$this->phpcs->defaultFileExtensions[$extension];
674            $this->tokenizerType = $this->phpcs->defaultFileExtensions[$extension];
675        } else {
676            // Revert to default.
677            $tokenizerClass = 'PHP_CodeSniffer_Tokenizers_'.$this->tokenizerType;
678        }
679
680        $tokenizer       = new $tokenizerClass();
681        $this->tokenizer = $tokenizer;
682
683        if ($contents === null) {
684            $contents = file_get_contents($this->_file);
685        }
686
687        try {
688            $tabWidth = null;
689            $encoding = null;
690            if (defined('PHP_CODESNIFFER_IN_TESTS') === true) {
691                $cliValues = $this->phpcs->cli->getCommandLineValues();
692                if (isset($cliValues['tabWidth']) === true) {
693                    $tabWidth = $cliValues['tabWidth'];
694                }
695
696                if (isset($cliValues['encoding']) === true) {
697                    $encoding = $cliValues['encoding'];
698                }
699            }
700
701            $this->_tokens = self::tokenizeString($contents, $tokenizer, $this->eolChar, $tabWidth, $encoding);
702        } catch (PHP_CodeSniffer_Exception $e) {
703            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
704            if (PHP_CODESNIFFER_VERBOSITY > 0 || (PHP_CODESNIFFER_CBF === true && $stdin === false)) {
705                echo "[$this->tokenizerType => tokenizer error]... ";
706                if (PHP_CODESNIFFER_VERBOSITY > 1) {
707                    echo PHP_EOL;
708                }
709            }
710
711            return;
712        }//end try
713
714        $this->numTokens = count($this->_tokens);
715
716        // Check for mixed line endings as these can cause tokenizer errors and we
717        // should let the user know that the results they get may be incorrect.
718        // This is done by removing all backslashes, removing the newline char we
719        // detected, then converting newlines chars into text. If any backslashes
720        // are left at the end, we have additional newline chars in use.
721        $contents = str_replace('\\', '', $contents);
722        $contents = str_replace($this->eolChar, '', $contents);
723        $contents = str_replace("\n", '\n', $contents);
724        $contents = str_replace("\r", '\r', $contents);
725        if (strpos($contents, '\\') !== false) {
726            $error = 'File has mixed line endings; this may cause incorrect results';
727            $this->addWarning($error, 0, 'Internal.LineEndings.Mixed');
728        }
729
730        if (PHP_CODESNIFFER_VERBOSITY > 0 || (PHP_CODESNIFFER_CBF === true && $stdin === false)) {
731            if ($this->numTokens === 0) {
732                $numLines = 0;
733            } else {
734                $numLines = $this->_tokens[($this->numTokens - 1)]['line'];
735            }
736
737            echo "[$this->tokenizerType => $this->numTokens tokens in $numLines lines]... ";
738            if (PHP_CODESNIFFER_VERBOSITY > 1) {
739                echo PHP_EOL;
740            }
741        }
742
743    }//end _parse()
744
745
746    /**
747     * Opens a file and detects the EOL character being used.
748     *
749     * @param string $file     The full path to the file.
750     * @param string $contents The contents to parse. If NULL, the content
751     *                         is taken from the file system.
752     *
753     * @return string
754     * @throws PHP_CodeSniffer_Exception If $file could not be opened.
755     */
756    public static function detectLineEndings($file, $contents=null)
757    {
758        if ($contents === null) {
759            // Determine the newline character being used in this file.
760            // Will be either \r, \r\n or \n.
761            if (is_readable($file) === false) {
762                $error = 'Error opening file; file no longer exists or you do not have access to read the file';
763                throw new PHP_CodeSniffer_Exception($error);
764            } else {
765                $handle = fopen($file, 'r');
766                if ($handle === false) {
767                    $error = 'Error opening file; could not auto-detect line endings';
768                    throw new PHP_CodeSniffer_Exception($error);
769                }
770            }
771
772            $firstLine = fgets($handle);
773            fclose($handle);
774
775            $eolChar = substr($firstLine, -1);
776            if ($eolChar === "\n") {
777                $secondLastChar = substr($firstLine, -2, 1);
778                if ($secondLastChar === "\r") {
779                    $eolChar = "\r\n";
780                }
781            } else if ($eolChar !== "\r") {
782                // Must not be an EOL char at the end of the line.
783                // Probably a one-line file, so assume \n as it really
784                // doesn't matter considering there are no newlines.
785                $eolChar = "\n";
786            }
787        } else {
788            if (preg_match("/\r\n?|\n/", $contents, $matches) !== 1) {
789                // Assuming there are no newlines.
790                $eolChar = "\n";
791            } else {
792                $eolChar = $matches[0];
793            }
794        }//end if
795
796        return $eolChar;
797
798    }//end detectLineEndings()
799
800
801    /**
802     * Records an error against a specific token in the file.
803     *
804     * @param string  $error    The error message.
805     * @param int     $stackPtr The stack position where the error occurred.
806     * @param string  $code     A violation code unique to the sniff message.
807     * @param array   $data     Replacements for the error message.
808     * @param int     $severity The severity level for this error. A value of 0
809     *                          will be converted into the default severity level.
810     * @param boolean $fixable  Can the error be fixed by the sniff?
811     *
812     * @return boolean
813     */
814    public function addError(
815        $error,
816        $stackPtr,
817        $code='',
818        $data=array(),
819        $severity=0,
820        $fixable=false
821    ) {
822        if ($stackPtr === null) {
823            $line   = 1;
824            $column = 1;
825        } else {
826            $line   = $this->_tokens[$stackPtr]['line'];
827            $column = $this->_tokens[$stackPtr]['column'];
828        }
829
830        return $this->_addError($error, $line, $column, $code, $data, $severity, $fixable);
831
832    }//end addError()
833
834
835    /**
836     * Records a warning against a specific token in the file.
837     *
838     * @param string  $warning  The error message.
839     * @param int     $stackPtr The stack position where the error occurred.
840     * @param string  $code     A violation code unique to the sniff message.
841     * @param array   $data     Replacements for the warning message.
842     * @param int     $severity The severity level for this warning. A value of 0
843     *                          will be converted into the default severity level.
844     * @param boolean $fixable  Can the warning be fixed by the sniff?
845     *
846     * @return boolean
847     */
848    public function addWarning(
849        $warning,
850        $stackPtr,
851        $code='',
852        $data=array(),
853        $severity=0,
854        $fixable=false
855    ) {
856        if ($stackPtr === null) {
857            $line   = 1;
858            $column = 1;
859        } else {
860            $line   = $this->_tokens[$stackPtr]['line'];
861            $column = $this->_tokens[$stackPtr]['column'];
862        }
863
864        return $this->_addWarning($warning, $line, $column, $code, $data, $severity, $fixable);
865
866    }//end addWarning()
867
868
869    /**
870     * Records an error against a specific line in the file.
871     *
872     * @param string $error    The error message.
873     * @param int    $line     The line on which the error occurred.
874     * @param string $code     A violation code unique to the sniff message.
875     * @param array  $data     Replacements for the error message.
876     * @param int    $severity The severity level for this error. A value of 0
877     *                         will be converted into the default severity level.
878     *
879     * @return boolean
880     */
881    public function addErrorOnLine(
882        $error,
883        $line,
884        $code='',
885        $data=array(),
886        $severity=0
887    ) {
888        return $this->_addError($error, $line, 1, $code, $data, $severity, false);
889
890    }//end addErrorOnLine()
891
892
893    /**
894     * Records a warning against a specific token in the file.
895     *
896     * @param string $warning  The error message.
897     * @param int    $line     The line on which the warning occurred.
898     * @param string $code     A violation code unique to the sniff message.
899     * @param array  $data     Replacements for the warning message.
900     * @param int    $severity The severity level for this warning. A value of 0
901     *                         will be converted into the default severity level.
902     *
903     * @return boolean
904     */
905    public function addWarningOnLine(
906        $warning,
907        $line,
908        $code='',
909        $data=array(),
910        $severity=0
911    ) {
912        return $this->_addWarning($warning, $line, 1, $code, $data, $severity, false);
913
914    }//end addWarningOnLine()
915
916
917    /**
918     * Records a fixable error against a specific token in the file.
919     *
920     * Returns true if the error was recorded and should be fixed.
921     *
922     * @param string $error    The error message.
923     * @param int    $stackPtr The stack position where the error occurred.
924     * @param string $code     A violation code unique to the sniff message.
925     * @param array  $data     Replacements for the error message.
926     * @param int    $severity The severity level for this error. A value of 0
927     *                         will be converted into the default severity level.
928     *
929     * @return boolean
930     */
931    public function addFixableError(
932        $error,
933        $stackPtr,
934        $code='',
935        $data=array(),
936        $severity=0
937    ) {
938        $recorded = $this->addError($error, $stackPtr, $code, $data, $severity, true);
939        if ($recorded === true && $this->fixer->enabled === true) {
940            return true;
941        }
942
943        return false;
944
945    }//end addFixableError()
946
947
948    /**
949     * Records a fixable warning against a specific token in the file.
950     *
951     * Returns true if the warning was recorded and should be fixed.
952     *
953     * @param string $warning  The error message.
954     * @param int    $stackPtr The stack position where the error occurred.
955     * @param string $code     A violation code unique to the sniff message.
956     * @param array  $data     Replacements for the warning message.
957     * @param int    $severity The severity level for this warning. A value of 0
958     *                         will be converted into the default severity level.
959     *
960     * @return boolean
961     */
962    public function addFixableWarning(
963        $warning,
964        $stackPtr,
965        $code='',
966        $data=array(),
967        $severity=0
968    ) {
969        $recorded = $this->addWarning($warning, $stackPtr, $code, $data, $severity, true);
970        if ($recorded === true && $this->fixer->enabled === true) {
971            return true;
972        }
973
974        return false;
975
976    }//end addFixableWarning()
977
978
979    /**
980     * Adds an error to the error stack.
981     *
982     * @param string  $error    The error message.
983     * @param int     $line     The line on which the error occurred.
984     * @param int     $column   The column at which the error occurred.
985     * @param string  $code     A violation code unique to the sniff message.
986     * @param array   $data     Replacements for the error message.
987     * @param int     $severity The severity level for this error. A value of 0
988     *                          will be converted into the default severity level.
989     * @param boolean $fixable  Can the error be fixed by the sniff?
990     *
991     * @return boolean
992     */
993    private function _addError($error, $line, $column, $code, $data, $severity, $fixable)
994    {
995        if (isset(self::$_ignoredLines[$line]) === true) {
996            return false;
997        }
998
999        // Work out which sniff generated the error.
1000        if (substr($code, 0, 9) === 'Internal.') {
1001            // Any internal message.
1002            $sniffCode = $code;
1003        } else {
1004            $parts = explode('_', str_replace('\\', '_', $this->_activeListener));
1005            if (isset($parts[3]) === true) {
1006                $sniff = $parts[0].'.'.$parts[2].'.'.$parts[3];
1007
1008                // Remove "Sniff" from the end.
1009                $sniff = substr($sniff, 0, -5);
1010            } else {
1011                $sniff = 'unknownSniff';
1012            }
1013
1014            $sniffCode = $sniff;
1015            if ($code !== '') {
1016                $sniffCode .= '.'.$code;
1017            }
1018        }//end if
1019
1020        // If we know this sniff code is being ignored for this file, return early.
1021        if (isset($this->_ignoredCodes[$sniffCode]) === true) {
1022            return false;
1023        }
1024
1025        // Make sure this message type has not been set to "warning".
1026        if (isset($this->ruleset[$sniffCode]['type']) === true
1027            && $this->ruleset[$sniffCode]['type'] === 'warning'
1028        ) {
1029            // Pass this off to the warning handler.
1030            return $this->_addWarning($error, $line, $column, $code, $data, $severity, $fixable);
1031        } else if ($this->phpcs->cli->errorSeverity === 0) {
1032            // Don't bother doing any processing as errors are just going to
1033            // be hidden in the reports anyway.
1034            return false;
1035        }
1036
1037        // Make sure we are interested in this severity level.
1038        if (isset($this->ruleset[$sniffCode]['severity']) === true) {
1039            $severity = $this->ruleset[$sniffCode]['severity'];
1040        } else if ($severity === 0) {
1041            $severity = PHPCS_DEFAULT_ERROR_SEV;
1042        }
1043
1044        if ($this->phpcs->cli->errorSeverity > $severity) {
1045            return false;
1046        }
1047
1048        // Make sure we are not ignoring this file.
1049        $patterns = $this->phpcs->getIgnorePatterns($sniffCode);
1050        foreach ($patterns as $pattern => $type) {
1051            // While there is support for a type of each pattern
1052            // (absolute or relative) we don't actually support it here.
1053            $replacements = array(
1054                             '\\,' => ',',
1055                             '*'   => '.*',
1056                            );
1057
1058            // We assume a / directory separator, as do the exclude rules
1059            // most developers write, so we need a special case for any system
1060            // that is different.
1061            if (DIRECTORY_SEPARATOR === '\\') {
1062                $replacements['/'] = '\\\\';
1063            }
1064
1065            $pattern = '`'.strtr($pattern, $replacements).'`i';
1066            if (preg_match($pattern, $this->_file) === 1) {
1067                $this->_ignoredCodes[$sniffCode] = true;
1068                return false;
1069            }
1070        }//end foreach
1071
1072        $this->_errorCount++;
1073        if ($fixable === true) {
1074            $this->_fixableCount++;
1075        }
1076
1077        if ($this->_recordErrors === false) {
1078            if (isset($this->_errors[$line]) === false) {
1079                $this->_errors[$line] = 0;
1080            }
1081
1082            $this->_errors[$line]++;
1083            return true;
1084        }
1085
1086        // Work out the error message.
1087        if (isset($this->ruleset[$sniffCode]['message']) === true) {
1088            $error = $this->ruleset[$sniffCode]['message'];
1089        }
1090
1091        if (empty($data) === true) {
1092            $message = $error;
1093        } else {
1094            $message = vsprintf($error, $data);
1095        }
1096
1097        if (isset($this->_errors[$line]) === false) {
1098            $this->_errors[$line] = array();
1099        }
1100
1101        if (isset($this->_errors[$line][$column]) === false) {
1102            $this->_errors[$line][$column] = array();
1103        }
1104
1105        $this->_errors[$line][$column][] = array(
1106                                            'message'  => $message,
1107                                            'source'   => $sniffCode,
1108                                            'severity' => $severity,
1109                                            'fixable'  => $fixable,
1110                                           );
1111
1112        if (PHP_CODESNIFFER_VERBOSITY > 1
1113            && $this->fixer->enabled === true
1114            && $fixable === true
1115        ) {
1116            @ob_end_clean();
1117            echo "\tE: [Line $line] $message ($sniffCode)".PHP_EOL;
1118            ob_start();
1119        }
1120
1121        return true;
1122
1123    }//end _addError()
1124
1125
1126    /**
1127     * Adds an warning to the warning stack.
1128     *
1129     * @param string  $warning  The error message.
1130     * @param int     $line     The line on which the warning occurred.
1131     * @param int     $column   The column at which the warning occurred.
1132     * @param string  $code     A violation code unique to the sniff message.
1133     * @param array   $data     Replacements for the warning message.
1134     * @param int     $severity The severity level for this warning. A value of 0
1135     *                          will be converted into the default severity level.
1136     * @param boolean $fixable  Can the warning be fixed by the sniff?
1137     *
1138     * @return boolean
1139     */
1140    private function _addWarning($warning, $line, $column, $code, $data, $severity, $fixable)
1141    {
1142        if (isset(self::$_ignoredLines[$line]) === true) {
1143            return false;
1144        }
1145
1146        // Work out which sniff generated the warning.
1147        if (substr($code, 0, 9) === 'Internal.') {
1148            // Any internal message.
1149            $sniffCode = $code;
1150        } else {
1151            $parts = explode('_', str_replace('\\', '_', $this->_activeListener));
1152            if (isset($parts[3]) === true) {
1153                $sniff = $parts[0].'.'.$parts[2].'.'.$parts[3];
1154
1155                // Remove "Sniff" from the end.
1156                $sniff = substr($sniff, 0, -5);
1157            } else {
1158                $sniff = 'unknownSniff';
1159            }
1160
1161            $sniffCode = $sniff;
1162            if ($code !== '') {
1163                $sniffCode .= '.'.$code;
1164            }
1165        }//end if
1166
1167        // If we know this sniff code is being ignored for this file, return early.
1168        if (isset($this->_ignoredCodes[$sniffCode]) === true) {
1169            return false;
1170        }
1171
1172        // Make sure this message type has not been set to "error".
1173        if (isset($this->ruleset[$sniffCode]['type']) === true
1174            && $this->ruleset[$sniffCode]['type'] === 'error'
1175        ) {
1176            // Pass this off to the error handler.
1177            return $this->_addError($warning, $line, $column, $code, $data, $severity, $fixable);
1178        } else if ($this->phpcs->cli->warningSeverity === 0) {
1179            // Don't bother doing any processing as warnings are just going to
1180            // be hidden in the reports anyway.
1181            return false;
1182        }
1183
1184        // Make sure we are interested in this severity level.
1185        if (isset($this->ruleset[$sniffCode]['severity']) === true) {
1186            $severity = $this->ruleset[$sniffCode]['severity'];
1187        } else if ($severity === 0) {
1188            $severity = PHPCS_DEFAULT_WARN_SEV;
1189        }
1190
1191        if ($this->phpcs->cli->warningSeverity > $severity) {
1192            return false;
1193        }
1194
1195        // Make sure we are not ignoring this file.
1196        $patterns = $this->phpcs->getIgnorePatterns($sniffCode);
1197        foreach ($patterns as $pattern => $type) {
1198            // While there is support for a type of each pattern
1199            // (absolute or relative) we don't actually support it here.
1200            $replacements = array(
1201                             '\\,' => ',',
1202                             '*'   => '.*',
1203                            );
1204
1205            // We assume a / directory separator, as do the exclude rules
1206            // most developers write, so we need a special case for any system
1207            // that is different.
1208            if (DIRECTORY_SEPARATOR === '\\') {
1209                $replacements['/'] = '\\\\';
1210            }
1211
1212            $pattern = '`'.strtr($pattern, $replacements).'`i';
1213            if (preg_match($pattern, $this->_file) === 1) {
1214                $this->_ignoredCodes[$sniffCode] = true;
1215                return false;
1216            }
1217        }//end foreach
1218
1219        $this->_warningCount++;
1220        if ($fixable === true) {
1221            $this->_fixableCount++;
1222        }
1223
1224        if ($this->_recordErrors === false) {
1225            if (isset($this->_warnings[$line]) === false) {
1226                $this->_warnings[$line] = 0;
1227            }
1228
1229            $this->_warnings[$line]++;
1230            return true;
1231        }
1232
1233        // Work out the warning message.
1234        if (isset($this->ruleset[$sniffCode]['message']) === true) {
1235            $warning = $this->ruleset[$sniffCode]['message'];
1236        }
1237
1238        if (empty($data) === true) {
1239            $message = $warning;
1240        } else {
1241            $message = vsprintf($warning, $data);
1242        }
1243
1244        if (isset($this->_warnings[$line]) === false) {
1245            $this->_warnings[$line] = array();
1246        }
1247
1248        if (isset($this->_warnings[$line][$column]) === false) {
1249            $this->_warnings[$line][$column] = array();
1250        }
1251
1252        $this->_warnings[$line][$column][] = array(
1253                                              'message'  => $message,
1254                                              'source'   => $sniffCode,
1255                                              'severity' => $severity,
1256                                              'fixable'  => $fixable,
1257                                             );
1258
1259        if (PHP_CODESNIFFER_VERBOSITY > 1
1260            && $this->fixer->enabled === true
1261            && $fixable === true
1262        ) {
1263            @ob_end_clean();
1264            echo "\tW: $message ($sniffCode)".PHP_EOL;
1265            ob_start();
1266        }
1267
1268        return true;
1269
1270    }//end _addWarning()
1271
1272
1273    /**
1274     * Adds an warning to the warning stack.
1275     *
1276     * @param int    $stackPtr The stack position where the metric was recorded.
1277     * @param string $metric   The name of the metric being recorded.
1278     * @param string $value    The value of the metric being recorded.
1279     *
1280     * @return boolean
1281     */
1282    public function recordMetric($stackPtr, $metric, $value)
1283    {
1284        if (isset($this->_metrics[$metric]) === false) {
1285            $this->_metrics[$metric] = array(
1286                                        'values' => array(
1287                                                     $value => array($stackPtr),
1288                                                    ),
1289                                       );
1290        } else {
1291            if (isset($this->_metrics[$metric]['values'][$value]) === false) {
1292                $this->_metrics[$metric]['values'][$value] = array($stackPtr);
1293            } else {
1294                $this->_metrics[$metric]['values'][$value][] = $stackPtr;
1295            }
1296        }
1297
1298        return true;
1299
1300    }//end recordMetric()
1301
1302
1303    /**
1304     * Returns the number of errors raised.
1305     *
1306     * @return int
1307     */
1308    public function getErrorCount()
1309    {
1310        return $this->_errorCount;
1311
1312    }//end getErrorCount()
1313
1314
1315    /**
1316     * Returns the number of warnings raised.
1317     *
1318     * @return int
1319     */
1320    public function getWarningCount()
1321    {
1322        return $this->_warningCount;
1323
1324    }//end getWarningCount()
1325
1326
1327    /**
1328     * Returns the number of successes recorded.
1329     *
1330     * @return int
1331     */
1332    public function getSuccessCount()
1333    {
1334        return $this->_successCount;
1335
1336    }//end getSuccessCount()
1337
1338
1339    /**
1340     * Returns the number of fixable errors/warnings raised.
1341     *
1342     * @return int
1343     */
1344    public function getFixableCount()
1345    {
1346        return $this->_fixableCount;
1347
1348    }//end getFixableCount()
1349
1350
1351    /**
1352     * Returns the list of ignored lines.
1353     *
1354     * @return array
1355     */
1356    public function getIgnoredLines()
1357    {
1358        return self::$_ignoredLines;
1359
1360    }//end getIgnoredLines()
1361
1362
1363    /**
1364     * Returns the errors raised from processing this file.
1365     *
1366     * @return array
1367     */
1368    public function getErrors()
1369    {
1370        return $this->_errors;
1371
1372    }//end getErrors()
1373
1374
1375    /**
1376     * Returns the warnings raised from processing this file.
1377     *
1378     * @return array
1379     */
1380    public function getWarnings()
1381    {
1382        return $this->_warnings;
1383
1384    }//end getWarnings()
1385
1386
1387    /**
1388     * Returns the metrics found while processing this file.
1389     *
1390     * @return array
1391     */
1392    public function getMetrics()
1393    {
1394        return $this->_metrics;
1395
1396    }//end getMetrics()
1397
1398
1399    /**
1400     * Returns the absolute filename of this file.
1401     *
1402     * @return string
1403     */
1404    public function getFilename()
1405    {
1406        return $this->_file;
1407
1408    }//end getFilename()
1409
1410
1411    /**
1412     * Creates an array of tokens when given some PHP code.
1413     *
1414     * Starts by using token_get_all() but does a lot of extra processing
1415     * to insert information about the context of the token.
1416     *
1417     * @param string $string    The string to tokenize.
1418     * @param object $tokenizer A tokenizer class to use to tokenize the string.
1419     * @param string $eolChar   The EOL character to use for splitting strings.
1420     * @param int    $tabWidth  The number of spaces each tab respresents.
1421     * @param string $encoding  The charset of the sniffed file.
1422     *
1423     * @throws PHP_CodeSniffer_Exception If the file cannot be processed.
1424     * @return array
1425     */
1426    public static function tokenizeString($string, $tokenizer, $eolChar='\n', $tabWidth=null, $encoding=null)
1427    {
1428        // Minified files often have a very large number of characters per line
1429        // and cause issues when tokenizing.
1430        if (property_exists($tokenizer, 'skipMinified') === true
1431            && $tokenizer->skipMinified === true
1432        ) {
1433            $numChars = strlen($string);
1434            $numLines = (substr_count($string, $eolChar) + 1);
1435            $average  = ($numChars / $numLines);
1436            if ($average > 100) {
1437                throw new PHP_CodeSniffer_Exception('File appears to be minified and cannot be processed');
1438            }
1439        }
1440
1441        $tokens = $tokenizer->tokenizeString($string, $eolChar);
1442
1443        if ($tabWidth === null) {
1444            $tabWidth = PHP_CODESNIFFER_TAB_WIDTH;
1445        }
1446
1447        if ($encoding === null) {
1448            $encoding = PHP_CODESNIFFER_ENCODING;
1449        }
1450
1451        self::_createPositionMap($tokens, $tokenizer, $eolChar, $encoding, $tabWidth);
1452        self::_createTokenMap($tokens, $tokenizer, $eolChar);
1453        self::_createParenthesisNestingMap($tokens, $tokenizer, $eolChar);
1454        self::_createScopeMap($tokens, $tokenizer, $eolChar);
1455
1456        self::_createLevelMap($tokens, $tokenizer, $eolChar);
1457
1458        // Allow the tokenizer to do additional processing if required.
1459        $tokenizer->processAdditional($tokens, $eolChar);
1460
1461        return $tokens;
1462
1463    }//end tokenizeString()
1464
1465
1466    /**
1467     * Sets token position information.
1468     *
1469     * Can also convert tabs into spaces. Each tab can represent between
1470     * 1 and $width spaces, so this cannot be a straight string replace.
1471     *
1472     * @param array  $tokens    The array of tokens to process.
1473     * @param object $tokenizer The tokenizer being used to process this file.
1474     * @param string $eolChar   The EOL character to use for splitting strings.
1475     * @param string $encoding  The charset of the sniffed file.
1476     * @param int    $tabWidth  The number of spaces that each tab represents.
1477     *                          Set to 0 to disable tab replacement.
1478     *
1479     * @return void
1480     */
1481    private static function _createPositionMap(&$tokens, $tokenizer, $eolChar, $encoding, $tabWidth)
1482    {
1483        $currColumn    = 1;
1484        $lineNumber    = 1;
1485        $eolLen        = (strlen($eolChar) * -1);
1486        $tokenizerType = get_class($tokenizer);
1487        $ignoring      = false;
1488        $inTests       = defined('PHP_CODESNIFFER_IN_TESTS');
1489
1490        $checkEncoding = false;
1491        if ($encoding !== 'iso-8859-1' && function_exists('iconv_strlen') === true) {
1492            $checkEncoding = true;
1493        }
1494
1495        $tokensWithTabs = array(
1496                           T_WHITESPACE               => true,
1497                           T_COMMENT                  => true,
1498                           T_DOC_COMMENT              => true,
1499                           T_DOC_COMMENT_WHITESPACE   => true,
1500                           T_DOC_COMMENT_STRING       => true,
1501                           T_CONSTANT_ENCAPSED_STRING => true,
1502                           T_DOUBLE_QUOTED_STRING     => true,
1503                           T_HEREDOC                  => true,
1504                           T_NOWDOC                   => true,
1505                           T_INLINE_HTML              => true,
1506                          );
1507
1508        $numTokens = count($tokens);
1509        for ($i = 0; $i < $numTokens; $i++) {
1510            $tokens[$i]['line']   = $lineNumber;
1511            $tokens[$i]['column'] = $currColumn;
1512
1513            if ($tokenizerType === 'PHP_CodeSniffer_Tokenizers_PHP'
1514                && isset(PHP_CodeSniffer_Tokens::$knownLengths[$tokens[$i]['code']]) === true
1515            ) {
1516                // There are no tabs in the tokens we know the length of.
1517                $length      = PHP_CodeSniffer_Tokens::$knownLengths[$tokens[$i]['code']];
1518                $currColumn += $length;
1519            } else if ($tabWidth === 0
1520                || isset($tokensWithTabs[$tokens[$i]['code']]) === false
1521                || strpos($tokens[$i]['content'], "\t") === false
1522            ) {
1523                // There are no tabs in this content, or we aren't replacing them.
1524                if ($checkEncoding === true) {
1525                    // Not using the default encoding, so take a bit more care.
1526                    $length = @iconv_strlen($tokens[$i]['content'], $encoding);
1527                    if ($length === false) {
1528                        // String contained invalid characters, so revert to default.
1529                        $length = strlen($tokens[$i]['content']);
1530                    }
1531                } else {
1532                    $length = strlen($tokens[$i]['content']);
1533                }
1534
1535                $currColumn += $length;
1536            } else {
1537                if (str_replace("\t", '', $tokens[$i]['content']) === '') {
1538                    // String only contains tabs, so we can shortcut the process.
1539                    $numTabs = strlen($tokens[$i]['content']);
1540
1541                    $newContent   = '';
1542                    $firstTabSize = ($tabWidth - (($currColumn - 1) % $tabWidth));
1543                    $length       = ($firstTabSize + ($tabWidth * ($numTabs - 1)));
1544                    $currColumn  += $length;
1545                    $newContent   = str_repeat(' ', $length);
1546                } else {
1547                    // We need to determine the length of each tab.
1548                    $tabs = explode("\t", $tokens[$i]['content']);
1549
1550                    $numTabs    = (count($tabs) - 1);
1551                    $tabNum     = 0;
1552                    $newContent = '';
1553                    $length     = 0;
1554
1555                    foreach ($tabs as $content) {
1556                        if ($content !== '') {
1557                            $newContent .= $content;
1558                            if ($checkEncoding === true) {
1559                                // Not using the default encoding, so take a bit more care.
1560                                $contentLength = @iconv_strlen($content, $encoding);
1561                                if ($contentLength === false) {
1562                                    // String contained invalid characters, so revert to default.
1563                                    $contentLength = strlen($content);
1564                                }
1565                            } else {
1566                                $contentLength = strlen($content);
1567                            }
1568
1569                            $currColumn += $contentLength;
1570                            $length     += $contentLength;
1571                        }
1572
1573                        // The last piece of content does not have a tab after it.
1574                        if ($tabNum === $numTabs) {
1575                            break;
1576                        }
1577
1578                        // Process the tab that comes after the content.
1579                        $lastCurrColumn = $currColumn;
1580                        $tabNum++;
1581
1582                        // Move the pointer to the next tab stop.
1583                        if (($currColumn % $tabWidth) === 0) {
1584                            // This is the first tab, and we are already at a
1585                            // tab stop, so this tab counts as a single space.
1586                            $currColumn++;
1587                        } else {
1588                            $currColumn++;
1589                            while (($currColumn % $tabWidth) !== 0) {
1590                                $currColumn++;
1591                            }
1592
1593                            $currColumn++;
1594                        }
1595
1596                        $length     += ($currColumn - $lastCurrColumn);
1597                        $newContent .= str_repeat(' ', ($currColumn - $lastCurrColumn));
1598                    }//end foreach
1599                }//end if
1600
1601                $tokens[$i]['orig_content'] = $tokens[$i]['content'];
1602                $tokens[$i]['content']      = $newContent;
1603            }//end if
1604
1605            $tokens[$i]['length'] = $length;
1606
1607            if (isset(PHP_CodeSniffer_Tokens::$knownLengths[$tokens[$i]['code']]) === false
1608                && strpos($tokens[$i]['content'], $eolChar) !== false
1609            ) {
1610                $lineNumber++;
1611                $currColumn = 1;
1612
1613                // Newline chars are not counted in the token length.
1614                $tokens[$i]['length'] += $eolLen;
1615            }
1616
1617            if ($tokens[$i]['code'] === T_COMMENT
1618                || $tokens[$i]['code'] === T_DOC_COMMENT_TAG
1619                || ($inTests === true && $tokens[$i]['code'] === T_INLINE_HTML)
1620            ) {
1621                if (strpos($tokens[$i]['content'], '@codingStandards') !== false) {
1622                    if ($ignoring === false
1623                        && strpos($tokens[$i]['content'], '@codingStandardsIgnoreStart') !== false
1624                    ) {
1625                        $ignoring = true;
1626                    } else if ($ignoring === true
1627                        && strpos($tokens[$i]['content'], '@codingStandardsIgnoreEnd') !== false
1628                    ) {
1629                        $ignoring = false;
1630                        // Ignore this comment too.
1631                        self::$_ignoredLines[$tokens[$i]['line']] = true;
1632                    } else if ($ignoring === false
1633                        && strpos($tokens[$i]['content'], '@codingStandardsIgnoreLine') !== false
1634                    ) {
1635                        self::$_ignoredLines[($tokens[$i]['line'] + 1)] = true;
1636                        // Ignore this comment too.
1637                        self::$_ignoredLines[$tokens[$i]['line']] = true;
1638                    }
1639                }
1640            }//end if
1641
1642            if ($ignoring === true) {
1643                self::$_ignoredLines[$tokens[$i]['line']] = true;
1644            }
1645        }//end for
1646
1647    }//end _createPositionMap()
1648
1649
1650    /**
1651     * Creates a map of brackets positions.
1652     *
1653     * @param array  $tokens    The array of tokens to process.
1654     * @param object $tokenizer The tokenizer being used to process this file.
1655     * @param string $eolChar   The EOL character to use for splitting strings.
1656     *
1657     * @return void
1658     */
1659    private static function _createTokenMap(&$tokens, $tokenizer, $eolChar)
1660    {
1661        if (PHP_CODESNIFFER_VERBOSITY > 1) {
1662            echo "\t*** START TOKEN MAP ***".PHP_EOL;
1663        }
1664
1665        $squareOpeners = array();
1666        $curlyOpeners  = array();
1667        $numTokens     = count($tokens);
1668
1669        $openers   = array();
1670        $openOwner = null;
1671
1672        for ($i = 0; $i < $numTokens; $i++) {
1673            /*
1674                Parenthesis mapping.
1675            */
1676
1677            if (isset(PHP_CodeSniffer_Tokens::$parenthesisOpeners[$tokens[$i]['code']]) === true) {
1678                $tokens[$i]['parenthesis_opener'] = null;
1679                $tokens[$i]['parenthesis_closer'] = null;
1680                $tokens[$i]['parenthesis_owner']  = $i;
1681                $openOwner = $i;
1682            } else if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
1683                $openers[] = $i;
1684                $tokens[$i]['parenthesis_opener'] = $i;
1685                if ($openOwner !== null) {
1686                    $tokens[$openOwner]['parenthesis_opener'] = $i;
1687                    $tokens[$i]['parenthesis_owner']          = $openOwner;
1688                    $openOwner = null;
1689                }
1690            } else if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
1691                // Did we set an owner for this set of parenthesis?
1692                $numOpeners = count($openers);
1693                if ($numOpeners !== 0) {
1694                    $opener = array_pop($openers);
1695                    if (isset($tokens[$opener]['parenthesis_owner']) === true) {
1696                        $owner = $tokens[$opener]['parenthesis_owner'];
1697
1698                        $tokens[$owner]['parenthesis_closer'] = $i;
1699                        $tokens[$i]['parenthesis_owner']      = $owner;
1700                    }
1701
1702                    $tokens[$i]['parenthesis_opener']      = $opener;
1703                    $tokens[$i]['parenthesis_closer']      = $i;
1704                    $tokens[$opener]['parenthesis_closer'] = $i;
1705                }
1706            }//end if
1707
1708            /*
1709                Bracket mapping.
1710            */
1711
1712            switch ($tokens[$i]['code']) {
1713            case T_OPEN_SQUARE_BRACKET:
1714                $squareOpeners[] = $i;
1715
1716                if (PHP_CODESNIFFER_VERBOSITY > 1) {
1717                    echo str_repeat("\t", count($squareOpeners));
1718                    echo str_repeat("\t", count($curlyOpeners));
1719                    echo "=> Found square bracket opener at $i".PHP_EOL;
1720                }
1721                break;
1722            case T_OPEN_CURLY_BRACKET:
1723                if (isset($tokens[$i]['scope_closer']) === false) {
1724                    $curlyOpeners[] = $i;
1725
1726                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
1727                        echo str_repeat("\t", count($squareOpeners));
1728                        echo str_repeat("\t", count($curlyOpeners));
1729                        echo "=> Found curly bracket opener at $i".PHP_EOL;
1730                    }
1731                }
1732                break;
1733            case T_CLOSE_SQUARE_BRACKET:
1734                if (empty($squareOpeners) === false) {
1735                    $opener = array_pop($squareOpeners);
1736                    $tokens[$i]['bracket_opener']      = $opener;
1737                    $tokens[$i]['bracket_closer']      = $i;
1738                    $tokens[$opener]['bracket_opener'] = $opener;
1739                    $tokens[$opener]['bracket_closer'] = $i;
1740
1741                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
1742                        echo str_repeat("\t", count($squareOpeners));
1743                        echo str_repeat("\t", count($curlyOpeners));
1744                        echo "\t=> Found square bracket closer at $i for $opener".PHP_EOL;
1745                    }
1746                }
1747                break;
1748            case T_CLOSE_CURLY_BRACKET:
1749                if (empty($curlyOpeners) === false
1750                    && isset($tokens[$i]['scope_opener']) === false
1751                ) {
1752                    $opener = array_pop($curlyOpeners);
1753                    $tokens[$i]['bracket_opener']      = $opener;
1754                    $tokens[$i]['bracket_closer']      = $i;
1755                    $tokens[$opener]['bracket_opener'] = $opener;
1756                    $tokens[$opener]['bracket_closer'] = $i;
1757
1758                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
1759                        echo str_repeat("\t", count($squareOpeners));
1760                        echo str_repeat("\t", count($curlyOpeners));
1761                        echo "\t=> Found curly bracket closer at $i for $opener".PHP_EOL;
1762                    }
1763                }
1764                break;
1765            default:
1766                continue;
1767            }//end switch
1768        }//end for
1769
1770        // Cleanup for any openers that we didn't find closers for.
1771        // This typically means there was a syntax error breaking things.
1772        foreach ($openers as $opener) {
1773            unset($tokens[$opener]['parenthesis_opener']);
1774            unset($tokens[$opener]['parenthesis_owner']);
1775        }
1776
1777        if (PHP_CODESNIFFER_VERBOSITY > 1) {
1778            echo "\t*** END TOKEN MAP ***".PHP_EOL;
1779        }
1780
1781    }//end _createTokenMap()
1782
1783
1784    /**
1785     * Creates a map for the parenthesis tokens that surround other tokens.
1786     *
1787     * @param array  $tokens    The array of tokens to process.
1788     * @param object $tokenizer The tokenizer being used to process this file.
1789     * @param string $eolChar   The EOL character to use for splitting strings.
1790     *
1791     * @return void
1792     */
1793    private static function _createParenthesisNestingMap(
1794        &$tokens,
1795        $tokenizer,
1796        $eolChar
1797    ) {
1798        $numTokens = count($tokens);
1799        $map       = array();
1800        for ($i = 0; $i < $numTokens; $i++) {
1801            if (isset($tokens[$i]['parenthesis_opener']) === true
1802                && $i === $tokens[$i]['parenthesis_opener']
1803            ) {
1804                if (empty($map) === false) {
1805                    $tokens[$i]['nested_parenthesis'] = $map;
1806                }
1807
1808                if (isset($tokens[$i]['parenthesis_closer']) === true) {
1809                    $map[$tokens[$i]['parenthesis_opener']]
1810                        = $tokens[$i]['parenthesis_closer'];
1811                }
1812            } else if (isset($tokens[$i]['parenthesis_closer']) === true
1813                && $i === $tokens[$i]['parenthesis_closer']
1814            ) {
1815                array_pop($map);
1816                if (empty($map) === false) {
1817                    $tokens[$i]['nested_parenthesis'] = $map;
1818                }
1819            } else {
1820                if (empty($map) === false) {
1821                    $tokens[$i]['nested_parenthesis'] = $map;
1822                }
1823            }//end if
1824        }//end for
1825
1826    }//end _createParenthesisNestingMap()
1827
1828
1829    /**
1830     * Creates a scope map of tokens that open scopes.
1831     *
1832     * @param array  $tokens    The array of tokens to process.
1833     * @param object $tokenizer The tokenizer being used to process this file.
1834     * @param string $eolChar   The EOL character to use for splitting strings.
1835     *
1836     * @return void
1837     * @see    _recurseScopeMap()
1838     */
1839    private static function _createScopeMap(&$tokens, $tokenizer, $eolChar)
1840    {
1841        if (PHP_CODESNIFFER_VERBOSITY > 1) {
1842            echo "\t*** START SCOPE MAP ***".PHP_EOL;
1843        }
1844
1845        $numTokens = count($tokens);
1846        for ($i = 0; $i < $numTokens; $i++) {
1847            // Check to see if the current token starts a new scope.
1848            if (isset($tokenizer->scopeOpeners[$tokens[$i]['code']]) === true) {
1849                if (PHP_CODESNIFFER_VERBOSITY > 1) {
1850                    $type    = $tokens[$i]['type'];
1851                    $content = PHP_CodeSniffer::prepareForOutput($tokens[$i]['content']);
1852                    echo "\tStart scope map at $i:$type => $content".PHP_EOL;
1853                }
1854
1855                if (isset($tokens[$i]['scope_condition']) === true) {
1856                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
1857                        echo "\t* already processed, skipping *".PHP_EOL;
1858                    }
1859
1860                    continue;
1861                }
1862
1863                $i = self::_recurseScopeMap(
1864                    $tokens,
1865                    $numTokens,
1866                    $tokenizer,
1867                    $eolChar,
1868                    $i
1869                );
1870            }//end if
1871        }//end for
1872
1873        if (PHP_CODESNIFFER_VERBOSITY > 1) {
1874            echo "\t*** END SCOPE MAP ***".PHP_EOL;
1875        }
1876
1877    }//end _createScopeMap()
1878
1879
1880    /**
1881     * Recurses though the scope openers to build a scope map.
1882     *
1883     * @param array  $tokens    The array of tokens to process.
1884     * @param int    $numTokens The size of the tokens array.
1885     * @param object $tokenizer The tokenizer being used to process this file.
1886     * @param string $eolChar   The EOL character to use for splitting strings.
1887     * @param int    $stackPtr  The position in the stack of the token that
1888     *                          opened the scope (eg. an IF token or FOR token).
1889     * @param int    $depth     How many scope levels down we are.
1890     * @param int    $ignore    How many curly braces we are ignoring.
1891     *
1892     * @return int The position in the stack that closed the scope.
1893     */
1894    private static function _recurseScopeMap(
1895        &$tokens,
1896        $numTokens,
1897        $tokenizer,
1898        $eolChar,
1899        $stackPtr,
1900        $depth=1,
1901        &$ignore=0
1902    ) {
1903        if (PHP_CODESNIFFER_VERBOSITY > 1) {
1904            echo str_repeat("\t", $depth);
1905            echo "=> Begin scope map recursion at token $stackPtr with depth $depth".PHP_EOL;
1906        }
1907
1908        $opener    = null;
1909        $currType  = $tokens[$stackPtr]['code'];
1910        $startLine = $tokens[$stackPtr]['line'];
1911
1912        // We will need this to restore the value if we end up
1913        // returning a token ID that causes our calling function to go back
1914        // over already ignored braces.
1915        $originalIgnore = $ignore;
1916
1917        // If the start token for this scope opener is the same as
1918        // the scope token, we have already found our opener.
1919        if (isset($tokenizer->scopeOpeners[$currType]['start'][$currType]) === true) {
1920            $opener = $stackPtr;
1921        }
1922
1923        for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
1924            $tokenType = $tokens[$i]['code'];
1925
1926            if (PHP_CODESNIFFER_VERBOSITY > 1) {
1927                $type    = $tokens[$i]['type'];
1928                $line    = $tokens[$i]['line'];
1929                $content = PHP_CodeSniffer::prepareForOutput($tokens[$i]['content']);
1930
1931                echo str_repeat("\t", $depth);
1932                echo "Process token $i on line $line [";
1933                if ($opener !== null) {
1934                    echo "opener:$opener;";
1935                }
1936
1937                if ($ignore > 0) {
1938                    echo "ignore=$ignore;";
1939                }
1940
1941                echo "]: $type => $content".PHP_EOL;
1942            }//end if
1943
1944            // Very special case for IF statements in PHP that can be defined without
1945            // scope tokens. E.g., if (1) 1; 1 ? (1 ? 1 : 1) : 1;
1946            // If an IF statement below this one has an opener but no
1947            // keyword, the opener will be incorrectly assigned to this IF statement.
1948            // The same case also applies to USE statements, which don't have to have
1949            // openers, so a following USE statement can cause an incorrect brace match.
1950            if (($currType === T_IF || $currType === T_ELSE || $currType === T_USE)
1951                && $opener === null
1952                && $tokens[$i]['code'] === T_SEMICOLON
1953            ) {
1954                if (PHP_CODESNIFFER_VERBOSITY > 1) {
1955                    $type = $tokens[$stackPtr]['type'];
1956                    echo str_repeat("\t", $depth);
1957                    echo "=> Found semicolon before scope opener for $stackPtr:$type, bailing".PHP_EOL;
1958                }
1959
1960                return $i;
1961            }
1962
1963            if ($opener === null
1964                && $ignore === 0
1965                && $tokenType === T_CLOSE_CURLY_BRACKET
1966                && isset($tokenizer->scopeOpeners[$currType]['end'][$tokenType]) === true
1967            ) {
1968                if (PHP_CODESNIFFER_VERBOSITY > 1) {
1969                    $type = $tokens[$stackPtr]['type'];
1970                    echo str_repeat("\t", $depth);
1971                    echo "=> Found curly brace closer before scope opener for $stackPtr:$type, bailing".PHP_EOL;
1972                }
1973
1974                return ($i - 1);
1975            }
1976
1977            if ($opener !== null
1978                && (isset($tokens[$i]['scope_opener']) === false
1979                || $tokenizer->scopeOpeners[$tokens[$stackPtr]['code']]['shared'] === true)
1980                && isset($tokenizer->scopeOpeners[$currType]['end'][$tokenType]) === true
1981            ) {
1982                if ($ignore > 0 && $tokenType === T_CLOSE_CURLY_BRACKET) {
1983                    // The last opening bracket must have been for a string
1984                    // offset or alike, so let's ignore it.
1985                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
1986                        echo str_repeat("\t", $depth);
1987                        echo '* finished ignoring curly brace *'.PHP_EOL;
1988                    }
1989
1990                    $ignore--;
1991                    continue;
1992                } else if ($tokens[$opener]['code'] === T_OPEN_CURLY_BRACKET
1993                    && $tokenType !== T_CLOSE_CURLY_BRACKET
1994                ) {
1995                    // The opener is a curly bracket so the closer must be a curly bracket as well.
1996                    // We ignore this closer to handle cases such as T_ELSE or T_ELSEIF being considered
1997                    // a closer of T_IF when it should not.
1998                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
1999                        $type = $tokens[$stackPtr]['type'];
2000                        echo str_repeat("\t", $depth);
2001                        echo "=> Ignoring non-curly scope closer for $stackPtr:$type".PHP_EOL;
2002                    }
2003                } else {
2004                    $scopeCloser = $i;
2005                    $todo        = array(
2006                                    $stackPtr,
2007                                    $opener,
2008                                   );
2009
2010                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2011                        $type       = $tokens[$stackPtr]['type'];
2012                        $closerType = $tokens[$scopeCloser]['type'];
2013                        echo str_repeat("\t", $depth);
2014                        echo "=> Found scope closer ($scopeCloser:$closerType) for $stackPtr:$type".PHP_EOL;
2015                    }
2016
2017                    $validCloser = true;
2018                    if (($tokens[$stackPtr]['code'] === T_IF || $tokens[$stackPtr]['code'] === T_ELSEIF)
2019                        && ($tokenType === T_ELSE || $tokenType === T_ELSEIF)
2020                    ) {
2021                        // To be a closer, this token must have an opener.
2022                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2023                            echo str_repeat("\t", $depth);
2024                            echo "* closer needs to be tested *".PHP_EOL;
2025                        }
2026
2027                        $i = self::_recurseScopeMap(
2028                            $tokens,
2029                            $numTokens,
2030                            $tokenizer,
2031                            $eolChar,
2032                            $i,
2033                            ($depth + 1),
2034                            $ignore
2035                        );
2036
2037                        if (isset($tokens[$scopeCloser]['scope_opener']) === false) {
2038                            $validCloser = false;
2039                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2040                                echo str_repeat("\t", $depth);
2041                                echo "* closer is not valid (no opener found) *".PHP_EOL;
2042                            }
2043                        } else if ($tokens[$tokens[$scopeCloser]['scope_opener']]['code'] !== $tokens[$opener]['code']) {
2044                            $validCloser = false;
2045                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2046                                echo str_repeat("\t", $depth);
2047                                $type       = $tokens[$tokens[$scopeCloser]['scope_opener']]['type'];
2048                                $openerType = $tokens[$opener]['type'];
2049                                echo "* closer is not valid (mismatched opener type; $type != $openerType) *".PHP_EOL;
2050                            }
2051                        } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
2052                            echo str_repeat("\t", $depth);
2053                            echo "* closer was valid *".PHP_EOL;
2054                        }
2055                    } else {
2056                        // The closer was not processed, so we need to
2057                        // complete that token as well.
2058                        $todo[] = $scopeCloser;
2059                    }//end if
2060
2061                    if ($validCloser === true) {
2062                        foreach ($todo as $token) {
2063                            $tokens[$token]['scope_condition'] = $stackPtr;
2064                            $tokens[$token]['scope_opener']    = $opener;
2065                            $tokens[$token]['scope_closer']    = $scopeCloser;
2066                        }
2067
2068                        if ($tokenizer->scopeOpeners[$tokens[$stackPtr]['code']]['shared'] === true) {
2069                            // As we are going back to where we started originally, restore
2070                            // the ignore value back to its original value.
2071                            $ignore = $originalIgnore;
2072                            return $opener;
2073                        } else if ($scopeCloser === $i
2074                            && isset($tokenizer->scopeOpeners[$tokenType]) === true
2075                        ) {
2076                            // Unset scope_condition here or else the token will appear to have
2077                            // already been processed, and it will be skipped. Normally we want that,
2078                            // but in this case, the token is both a closer and an opener, so
2079                            // it needs to act like an opener. This is also why we return the
2080                            // token before this one; so the closer has a chance to be processed
2081                            // a second time, but as an opener.
2082                            unset($tokens[$scopeCloser]['scope_condition']);
2083                            return ($i - 1);
2084                        } else {
2085                            return $i;
2086                        }
2087                    } else {
2088                        continue;
2089                    }//end if
2090                }//end if
2091            }//end if
2092
2093            // Is this an opening condition ?
2094            if (isset($tokenizer->scopeOpeners[$tokenType]) === true) {
2095                if ($opener === null) {
2096                    if ($tokenType === T_USE) {
2097                        // PHP use keywords are special because they can be
2098                        // used as blocks but also inline in function definitions.
2099                        // So if we find them nested inside another opener, just skip them.
2100                        continue;
2101                    }
2102
2103                    if ($tokenType === T_FUNCTION
2104                        && $tokens[$stackPtr]['code'] !== T_FUNCTION
2105                    ) {
2106                        // Probably a closure, so process it manually.
2107                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2108                            $type = $tokens[$stackPtr]['type'];
2109                            echo str_repeat("\t", $depth);
2110                            echo "=> Found function before scope opener for $stackPtr:$type, processing manually".PHP_EOL;
2111                        }
2112
2113                        if (isset($tokens[$i]['scope_closer']) === true) {
2114                            // We've already processed this closure.
2115                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2116                                echo str_repeat("\t", $depth);
2117                                echo '* already processed, skipping *'.PHP_EOL;
2118                            }
2119
2120                            $i = $tokens[$i]['scope_closer'];
2121                            continue;
2122                        }
2123
2124                        $i = self::_recurseScopeMap(
2125                            $tokens,
2126                            $numTokens,
2127                            $tokenizer,
2128                            $eolChar,
2129                            $i,
2130                            ($depth + 1),
2131                            $ignore
2132                        );
2133
2134                        continue;
2135                    }//end if
2136
2137                    // Found another opening condition but still haven't
2138                    // found our opener, so we are never going to find one.
2139                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2140                        $type = $tokens[$stackPtr]['type'];
2141                        echo str_repeat("\t", $depth);
2142                        echo "=> Found new opening condition before scope opener for $stackPtr:$type, ";
2143                    }
2144
2145                    if (($tokens[$stackPtr]['code'] === T_IF
2146                        || $tokens[$stackPtr]['code'] === T_ELSEIF
2147                        || $tokens[$stackPtr]['code'] === T_ELSE)
2148                        && ($tokens[$i]['code'] === T_ELSE
2149                        || $tokens[$i]['code'] === T_ELSEIF)
2150                    ) {
2151                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2152                            echo "continuing".PHP_EOL;
2153                        }
2154
2155                        return ($i - 1);
2156                    } else {
2157                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2158                            echo "backtracking".PHP_EOL;
2159                        }
2160
2161                        return $stackPtr;
2162                    }
2163                }//end if
2164
2165                if (PHP_CODESNIFFER_VERBOSITY > 1) {
2166                    echo str_repeat("\t", $depth);
2167                    echo '* token is an opening condition *'.PHP_EOL;
2168                }
2169
2170                $isShared = ($tokenizer->scopeOpeners[$tokenType]['shared'] === true);
2171
2172                if (isset($tokens[$i]['scope_condition']) === true) {
2173                    // We've been here before.
2174                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2175                        echo str_repeat("\t", $depth);
2176                        echo '* already processed, skipping *'.PHP_EOL;
2177                    }
2178
2179                    if ($isShared === false
2180                        && isset($tokens[$i]['scope_closer']) === true
2181                    ) {
2182                        $i = $tokens[$i]['scope_closer'];
2183                    }
2184
2185                    continue;
2186                } else if ($currType === $tokenType
2187                    && $isShared === false
2188                    && $opener === null
2189                ) {
2190                    // We haven't yet found our opener, but we have found another
2191                    // scope opener which is the same type as us, and we don't
2192                    // share openers, so we will never find one.
2193                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2194                        echo str_repeat("\t", $depth);
2195                        echo '* it was another token\'s opener, bailing *'.PHP_EOL;
2196                    }
2197
2198                    return $stackPtr;
2199                } else {
2200                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2201                        echo str_repeat("\t", $depth);
2202                        echo '* searching for opener *'.PHP_EOL;
2203                    }
2204
2205                    if (isset($tokenizer->scopeOpeners[$tokenType]['end'][T_CLOSE_CURLY_BRACKET]) === true) {
2206                        $oldIgnore = $ignore;
2207                        $ignore    = 0;
2208                    }
2209
2210                    // PHP has a max nesting level for functions. Stop before we hit that limit
2211                    // because too many loops means we've run into trouble anyway.
2212                    if ($depth > 50) {
2213                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2214                            echo str_repeat("\t", $depth);
2215                            echo '* reached maximum nesting level; aborting *'.PHP_EOL;
2216                        }
2217
2218                        throw new PHP_CodeSniffer_Exception('Maximum nesting level reached; file could not be processed');
2219                    }
2220
2221                    $oldDepth = $depth;
2222                    if ($isShared === true
2223                        && isset($tokenizer->scopeOpeners[$tokenType]['with'][$currType]) === true
2224                    ) {
2225                        // Don't allow the depth to increment because this is
2226                        // possibly not a true nesting if we are sharing our closer.
2227                        // This can happen, for example, when a SWITCH has a large
2228                        // number of CASE statements with the same shared BREAK.
2229                        $depth--;
2230                    }
2231
2232                    $i = self::_recurseScopeMap(
2233                        $tokens,
2234                        $numTokens,
2235                        $tokenizer,
2236                        $eolChar,
2237                        $i,
2238                        ($depth + 1),
2239                        $ignore
2240                    );
2241
2242                    $depth = $oldDepth;
2243
2244                    if (isset($tokenizer->scopeOpeners[$tokenType]['end'][T_CLOSE_CURLY_BRACKET]) === true) {
2245                        $ignore = $oldIgnore;
2246                    }
2247                }//end if
2248            }//end if
2249
2250            if (isset($tokenizer->scopeOpeners[$currType]['start'][$tokenType]) === true
2251                && $opener === null
2252            ) {
2253                if ($tokenType === T_OPEN_CURLY_BRACKET) {
2254                    if (isset($tokens[$stackPtr]['parenthesis_closer']) === true
2255                        && $i < $tokens[$stackPtr]['parenthesis_closer']
2256                    ) {
2257                        // We found a curly brace inside the condition of the
2258                        // current scope opener, so it must be a string offset.
2259                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2260                            echo str_repeat("\t", $depth);
2261                            echo '* ignoring curly brace inside condition *'.PHP_EOL;
2262                        }
2263
2264                        $ignore++;
2265                    } else {
2266                        // Make sure this is actually an opener and not a
2267                        // string offset (e.g., $var{0}).
2268                        for ($x = ($i - 1); $x > 0; $x--) {
2269                            if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$x]['code']]) === true) {
2270                                continue;
2271                            } else {
2272                                // If the first non-whitespace/comment token looks like this
2273                                // brace is a string offset, or this brace is mid-way through
2274                                // a new statement, it isn't a scope opener.
2275                                $disallowed  = PHP_CodeSniffer_Tokens::$assignmentTokens;
2276                                $disallowed += array(
2277                                                T_VARIABLE         => true,
2278                                                T_OBJECT_OPERATOR  => true,
2279                                                T_COMMA            => true,
2280                                                T_OPEN_PARENTHESIS => true,
2281                                               );
2282
2283                                if (isset($disallowed[$tokens[$x]['code']]) === true) {
2284                                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2285                                        echo str_repeat("\t", $depth);
2286                                        echo '* ignoring curly brace after condition *'.PHP_EOL;
2287                                    }
2288
2289                                    $ignore++;
2290                                }//end if
2291
2292                                break;
2293                            }//end if
2294                        }//end for
2295                    }//end if
2296                }//end if
2297
2298                if ($ignore === 0 || $tokenType !== T_OPEN_CURLY_BRACKET) {
2299                    // We found the opening scope token for $currType.
2300                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2301                        $type = $tokens[$stackPtr]['type'];
2302                        echo str_repeat("\t", $depth);
2303                        echo "=> Found scope opener for $stackPtr:$type".PHP_EOL;
2304                    }
2305
2306                    $opener = $i;
2307                }
2308            } else if ($tokenType === T_OPEN_PARENTHESIS) {
2309                if (isset($tokens[$i]['parenthesis_owner']) === true) {
2310                    $owner = $tokens[$i]['parenthesis_owner'];
2311                    if (isset(PHP_CodeSniffer_Tokens::$scopeOpeners[$tokens[$owner]['code']]) === true
2312                        && isset($tokens[$i]['parenthesis_closer']) === true
2313                    ) {
2314                        // If we get into here, then we opened a parenthesis for
2315                        // a scope (eg. an if or else if) so we need to update the
2316                        // start of the line so that when we check to see
2317                        // if the closing parenthesis is more than 3 lines away from
2318                        // the statement, we check from the closing parenthesis.
2319                        $startLine = $tokens[$tokens[$i]['parenthesis_closer']]['line'];
2320                    }
2321                }
2322            } else if ($tokenType === T_OPEN_CURLY_BRACKET && $opener !== null) {
2323                // We opened something that we don't have a scope opener for.
2324                // Examples of this are curly brackets for string offsets etc.
2325                // We want to ignore this so that we don't have an invalid scope
2326                // map.
2327                if (PHP_CODESNIFFER_VERBOSITY > 1) {
2328                    echo str_repeat("\t", $depth);
2329                    echo '* ignoring curly brace *'.PHP_EOL;
2330                }
2331
2332                $ignore++;
2333            } else if ($tokenType === T_CLOSE_CURLY_BRACKET && $ignore > 0) {
2334                // We found the end token for the opener we were ignoring.
2335                if (PHP_CODESNIFFER_VERBOSITY > 1) {
2336                    echo str_repeat("\t", $depth);
2337                    echo '* finished ignoring curly brace *'.PHP_EOL;
2338                }
2339
2340                $ignore--;
2341            } else if ($opener === null
2342                && isset($tokenizer->scopeOpeners[$currType]) === true
2343            ) {
2344                // If we still haven't found the opener after 3 lines,
2345                // we're not going to find it, unless we know it requires
2346                // an opener (in which case we better keep looking) or the last
2347                // token was empty (in which case we'll just confirm there is
2348                // more code in this file and not just a big comment).
2349                if ($tokens[$i]['line'] >= ($startLine + 3)
2350                    && isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[($i - 1)]['code']]) === false
2351                ) {
2352                    if ($tokenizer->scopeOpeners[$currType]['strict'] === true) {
2353                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2354                            $type  = $tokens[$stackPtr]['type'];
2355                            $lines = ($tokens[$i]['line'] - $startLine);
2356                            echo str_repeat("\t", $depth);
2357                            echo "=> Still looking for $stackPtr:$type scope opener after $lines lines".PHP_EOL;
2358                        }
2359                    } else {
2360                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2361                            $type = $tokens[$stackPtr]['type'];
2362                            echo str_repeat("\t", $depth);
2363                            echo "=> Couldn't find scope opener for $stackPtr:$type, bailing".PHP_EOL;
2364                        }
2365
2366                        return $stackPtr;
2367                    }
2368                }
2369            } else if ($opener !== null
2370                && $tokenType !== T_BREAK
2371                && isset($tokenizer->endScopeTokens[$tokenType]) === true
2372            ) {
2373                if (isset($tokens[$i]['scope_condition']) === false) {
2374                    if ($ignore > 0) {
2375                        // We found the end token for the opener we were ignoring.
2376                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2377                            echo str_repeat("\t", $depth);
2378                            echo '* finished ignoring curly brace *'.PHP_EOL;
2379                        }
2380
2381                        $ignore--;
2382                    } else {
2383                        // We found a token that closes the scope but it doesn't
2384                        // have a condition, so it belongs to another token and
2385                        // our token doesn't have a closer, so pretend this is
2386                        // the closer.
2387                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2388                            $type = $tokens[$stackPtr]['type'];
2389                            echo str_repeat("\t", $depth);
2390                            echo "=> Found (unexpected) scope closer for $stackPtr:$type".PHP_EOL;
2391                        }
2392
2393                        foreach (array($stackPtr, $opener) as $token) {
2394                            $tokens[$token]['scope_condition'] = $stackPtr;
2395                            $tokens[$token]['scope_opener']    = $opener;
2396                            $tokens[$token]['scope_closer']    = $i;
2397                        }
2398
2399                        return ($i - 1);
2400                    }//end if
2401                }//end if
2402            }//end if
2403        }//end for
2404
2405        return $stackPtr;
2406
2407    }//end _recurseScopeMap()
2408
2409
2410    /**
2411     * Constructs the level map.
2412     *
2413     * The level map adds a 'level' index to each token which indicates the
2414     * depth that a token within a set of scope blocks. It also adds a
2415     * 'condition' index which is an array of the scope conditions that opened
2416     * each of the scopes - position 0 being the first scope opener.
2417     *
2418     * @param array  $tokens    The array of tokens to process.
2419     * @param object $tokenizer The tokenizer being used to process this file.
2420     * @param string $eolChar   The EOL character to use for splitting strings.
2421     *
2422     * @return void
2423     */
2424    private static function _createLevelMap(&$tokens, $tokenizer, $eolChar)
2425    {
2426        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2427            echo "\t*** START LEVEL MAP ***".PHP_EOL;
2428        }
2429
2430        $numTokens  = count($tokens);
2431        $level      = 0;
2432        $conditions = array();
2433        $lastOpener = null;
2434        $openers    = array();
2435
2436        for ($i = 0; $i < $numTokens; $i++) {
2437            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2438                $type = $tokens[$i]['type'];
2439                $line = $tokens[$i]['line'];
2440                $len  = $tokens[$i]['length'];
2441                $col  = $tokens[$i]['column'];
2442
2443                $content = PHP_CodeSniffer::prepareForOutput($tokens[$i]['content']);
2444
2445                echo str_repeat("\t", ($level + 1));
2446                echo "Process token $i on line $line [col:$col;len:$len;lvl:$level;";
2447                if (empty($conditions) !== true) {
2448                    $condString = 'conds;';
2449                    foreach ($conditions as $condition) {
2450                        $condString .= token_name($condition).',';
2451                    }
2452
2453                    echo rtrim($condString, ',').';';
2454                }
2455
2456                echo "]: $type => $content".PHP_EOL;
2457            }//end if
2458
2459            $tokens[$i]['level']      = $level;
2460            $tokens[$i]['conditions'] = $conditions;
2461
2462            if (isset($tokens[$i]['scope_condition']) === true) {
2463                // Check to see if this token opened the scope.
2464                if ($tokens[$i]['scope_opener'] === $i) {
2465                    $stackPtr = $tokens[$i]['scope_condition'];
2466                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2467                        $type = $tokens[$stackPtr]['type'];
2468                        echo str_repeat("\t", ($level + 1));
2469                        echo "=> Found scope opener for $stackPtr:$type".PHP_EOL;
2470                    }
2471
2472                    $stackPtr = $tokens[$i]['scope_condition'];
2473
2474                    // If we find a scope opener that has a shared closer,
2475                    // then we need to go back over the condition map that we
2476                    // just created and fix ourselves as we just added some
2477                    // conditions where there was none. This happens for T_CASE
2478                    // statements that are using the same break statement.
2479                    if ($lastOpener !== null && $tokens[$lastOpener]['scope_closer'] === $tokens[$i]['scope_closer']) {
2480                        // This opener shares its closer with the previous opener,
2481                        // but we still need to check if the two openers share their
2482                        // closer with each other directly (like CASE and DEFAULT)
2483                        // or if they are just sharing because one doesn't have a
2484                        // closer (like CASE with no BREAK using a SWITCHes closer).
2485                        $thisType = $tokens[$tokens[$i]['scope_condition']]['code'];
2486                        $opener   = $tokens[$lastOpener]['scope_condition'];
2487
2488                        $isShared = isset($tokenizer->scopeOpeners[$thisType]['with'][$tokens[$opener]['code']]);
2489
2490                        reset($tokenizer->scopeOpeners[$thisType]['end']);
2491                        reset($tokenizer->scopeOpeners[$tokens[$opener]['code']]['end']);
2492                        $sameEnd = (current($tokenizer->scopeOpeners[$thisType]['end']) === current($tokenizer->scopeOpeners[$tokens[$opener]['code']]['end']));
2493
2494                        if ($isShared === true && $sameEnd === true) {
2495                            $badToken = $opener;
2496                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2497                                $type = $tokens[$badToken]['type'];
2498                                echo str_repeat("\t", ($level + 1));
2499                                echo "* shared closer, cleaning up $badToken:$type *".PHP_EOL;
2500                            }
2501
2502                            for ($x = $tokens[$i]['scope_condition']; $x <= $i; $x++) {
2503                                $oldConditions = $tokens[$x]['conditions'];
2504                                $oldLevel      = $tokens[$x]['level'];
2505                                $tokens[$x]['level']--;
2506                                unset($tokens[$x]['conditions'][$badToken]);
2507                                if (PHP_CODESNIFFER_VERBOSITY > 1) {
2508                                    $type     = $tokens[$x]['type'];
2509                                    $oldConds = '';
2510                                    foreach ($oldConditions as $condition) {
2511                                        $oldConds .= token_name($condition).',';
2512                                    }
2513
2514                                    $oldConds = rtrim($oldConds, ',');
2515
2516                                    $newConds = '';
2517                                    foreach ($tokens[$x]['conditions'] as $condition) {
2518                                        $newConds .= token_name($condition).',';
2519                                    }
2520
2521                                    $newConds = rtrim($newConds, ',');
2522
2523                                    $newLevel = $tokens[$x]['level'];
2524                                    echo str_repeat("\t", ($level + 1));
2525                                    echo "* cleaned $x:$type *".PHP_EOL;
2526                                    echo str_repeat("\t", ($level + 2));
2527                                    echo "=> level changed from $oldLevel to $newLevel".PHP_EOL;
2528                                    echo str_repeat("\t", ($level + 2));
2529                                    echo "=> conditions changed from $oldConds to $newConds".PHP_EOL;
2530                                }//end if
2531                            }//end for
2532
2533                            unset($conditions[$badToken]);
2534                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2535                                $type = $tokens[$badToken]['type'];
2536                                echo str_repeat("\t", ($level + 1));
2537                                echo "* token $badToken:$type removed from conditions array *".PHP_EOL;
2538                            }
2539
2540                            unset($openers[$lastOpener]);
2541
2542                            $level--;
2543                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2544                                echo str_repeat("\t", ($level + 2));
2545                                echo '* level decreased *'.PHP_EOL;
2546                            }
2547                        }//end if
2548                    }//end if
2549
2550                    $level++;
2551                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2552                        echo str_repeat("\t", ($level + 1));
2553                        echo '* level increased *'.PHP_EOL;
2554                    }
2555
2556                    $conditions[$stackPtr] = $tokens[$stackPtr]['code'];
2557                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2558                        $type = $tokens[$stackPtr]['type'];
2559                        echo str_repeat("\t", ($level + 1));
2560                        echo "* token $stackPtr:$type added to conditions array *".PHP_EOL;
2561                    }
2562
2563                    $lastOpener = $tokens[$i]['scope_opener'];
2564                    if ($lastOpener !== null) {
2565                        $openers[$lastOpener] = $lastOpener;
2566                    }
2567                } else if ($lastOpener !== null && $tokens[$lastOpener]['scope_closer'] === $i) {
2568                    foreach (array_reverse($openers) as $opener) {
2569                        if ($tokens[$opener]['scope_closer'] === $i) {
2570                            $oldOpener = array_pop($openers);
2571                            if (empty($openers) === false) {
2572                                $lastOpener           = array_pop($openers);
2573                                $openers[$lastOpener] = $lastOpener;
2574                            } else {
2575                                $lastOpener = null;
2576                            }
2577
2578                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2579                                $type = $tokens[$oldOpener]['type'];
2580                                echo str_repeat("\t", ($level + 1));
2581                                echo "=> Found scope closer for $oldOpener:$type".PHP_EOL;
2582                            }
2583
2584                            $oldCondition = array_pop($conditions);
2585                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2586                                echo str_repeat("\t", ($level + 1));
2587                                echo '* token '.token_name($oldCondition).' removed from conditions array *'.PHP_EOL;
2588                            }
2589
2590                            // Make sure this closer actually belongs to us.
2591                            // Either the condition also has to think this is the
2592                            // closer, or it has to allow sharing with us.
2593                            $condition = $tokens[$tokens[$i]['scope_condition']]['code'];
2594                            if ($condition !== $oldCondition) {
2595                                if (isset($tokenizer->scopeOpeners[$oldCondition]['with'][$condition]) === false) {
2596                                    $badToken = $tokens[$oldOpener]['scope_condition'];
2597
2598                                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
2599                                        $type = token_name($oldCondition);
2600                                        echo str_repeat("\t", ($level + 1));
2601                                        echo "* scope closer was bad, cleaning up $badToken:$type *".PHP_EOL;
2602                                    }
2603
2604                                    for ($x = ($oldOpener + 1); $x <= $i; $x++) {
2605                                        $oldConditions = $tokens[$x]['conditions'];
2606                                        $oldLevel      = $tokens[$x]['level'];
2607                                        $tokens[$x]['level']--;
2608                                        unset($tokens[$x]['conditions'][$badToken]);
2609                                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2610                                            $type     = $tokens[$x]['type'];
2611                                            $oldConds = '';
2612                                            foreach ($oldConditions as $condition) {
2613                                                $oldConds .= token_name($condition).',';
2614                                            }
2615
2616                                            $oldConds = rtrim($oldConds, ',');
2617
2618                                            $newConds = '';
2619                                            foreach ($tokens[$x]['conditions'] as $condition) {
2620                                                $newConds .= token_name($condition).',';
2621                                            }
2622
2623                                            $newConds = rtrim($newConds, ',');
2624
2625                                            $newLevel = $tokens[$x]['level'];
2626                                            echo str_repeat("\t", ($level + 1));
2627                                            echo "* cleaned $x:$type *".PHP_EOL;
2628                                            echo str_repeat("\t", ($level + 2));
2629                                            echo "=> level changed from $oldLevel to $newLevel".PHP_EOL;
2630                                            echo str_repeat("\t", ($level + 2));
2631                                            echo "=> conditions changed from $oldConds to $newConds".PHP_EOL;
2632                                        }//end if
2633                                    }//end for
2634                                }//end if
2635                            }//end if
2636
2637                            $level--;
2638                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
2639                                echo str_repeat("\t", ($level + 2));
2640                                echo '* level decreased *'.PHP_EOL;
2641                            }
2642
2643                            $tokens[$i]['level']      = $level;
2644                            $tokens[$i]['conditions'] = $conditions;
2645                        }//end if
2646                    }//end foreach
2647                }//end if
2648            }//end if
2649        }//end for
2650
2651        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2652            echo "\t*** END LEVEL MAP ***".PHP_EOL;
2653        }
2654
2655    }//end _createLevelMap()
2656
2657
2658    /**
2659     * Returns the declaration names for classes, interfaces, and functions.
2660     *
2661     * @param int $stackPtr The position of the declaration token which
2662     *                      declared the class, interface, trait or function.
2663     *
2664     * @return string|null The name of the class, interface or function.
2665     *                     or NULL if the function or class is anonymous.
2666     * @throws PHP_CodeSniffer_Exception If the specified token is not of type
2667     *                                   T_FUNCTION, T_CLASS, T_ANON_CLASS,
2668     *                                   T_TRAIT or T_INTERFACE.
2669     */
2670    public function getDeclarationName($stackPtr)
2671    {
2672        $tokenCode = $this->_tokens[$stackPtr]['code'];
2673
2674        if ($tokenCode === T_ANON_CLASS) {
2675            return null;
2676        }
2677
2678        if ($tokenCode === T_CLOSURE) {
2679            return null;
2680        }
2681
2682        if ($tokenCode !== T_FUNCTION
2683            && $tokenCode !== T_CLASS
2684            && $tokenCode !== T_INTERFACE
2685            && $tokenCode !== T_TRAIT
2686        ) {
2687            throw new PHP_CodeSniffer_Exception('Token type "'.$this->_tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT');
2688        }
2689
2690        $content = null;
2691        for ($i = $stackPtr; $i < $this->numTokens; $i++) {
2692            if ($this->_tokens[$i]['code'] === T_STRING) {
2693                $content = $this->_tokens[$i]['content'];
2694                break;
2695            }
2696        }
2697
2698        return $content;
2699
2700    }//end getDeclarationName()
2701
2702
2703    /**
2704     * Check if the token at the specified position is a anonymous function.
2705     *
2706     * @param int $stackPtr The position of the declaration token which
2707     *                      declared the class, interface or function.
2708     *
2709     * @return boolean
2710     * @throws PHP_CodeSniffer_Exception If the specified token is not of type
2711     *                                   T_FUNCTION
2712     */
2713    public function isAnonymousFunction($stackPtr)
2714    {
2715        $tokenCode = $this->_tokens[$stackPtr]['code'];
2716        if ($tokenCode !== T_FUNCTION) {
2717            throw new PHP_CodeSniffer_Exception('Token type is not T_FUNCTION');
2718        }
2719
2720        if (isset($this->_tokens[$stackPtr]['parenthesis_opener']) === false) {
2721            // Something is not right with this function.
2722            return false;
2723        }
2724
2725        $name = false;
2726        for ($i = ($stackPtr + 1); $i < $this->numTokens; $i++) {
2727            if ($this->_tokens[$i]['code'] === T_STRING) {
2728                $name = $i;
2729                break;
2730            }
2731        }
2732
2733        if ($name === false) {
2734            // No name found.
2735            return true;
2736        }
2737
2738        $open = $this->_tokens[$stackPtr]['parenthesis_opener'];
2739        if ($name > $open) {
2740            return true;
2741        }
2742
2743        return false;
2744
2745    }//end isAnonymousFunction()
2746
2747
2748    /**
2749     * Returns the method parameters for the specified function token.
2750     *
2751     * Each parameter is in the following format:
2752     *
2753     * <code>
2754     *   0 => array(
2755     *         'token'             => int,     // The position of the var in the token stack.
2756     *         'name'              => '$var',  // The variable name.
2757     *         'content'           => string,  // The full content of the variable definition.
2758     *         'pass_by_reference' => boolean, // Is the variable passed by reference?
2759     *         'variable_length'   => boolean, // Is the param of variable length through use of `...` ?
2760     *         'type_hint'         => string,  // The type hint for the variable.
2761     *         'nullable_type'     => boolean, // Is the variable using a nullable type?
2762     *        )
2763     * </code>
2764     *
2765     * Parameters with default values have an additional array index of
2766     * 'default' with the value of the default as a string.
2767     *
2768     * @param int $stackPtr The position in the stack of the function token
2769     *                      to acquire the parameters for.
2770     *
2771     * @return array
2772     * @throws PHP_CodeSniffer_Exception If the specified $stackPtr is not of
2773     *                                   type T_FUNCTION or T_CLOSURE.
2774     */
2775    public function getMethodParameters($stackPtr)
2776    {
2777        if ($this->_tokens[$stackPtr]['code'] !== T_FUNCTION
2778            && $this->_tokens[$stackPtr]['code'] !== T_CLOSURE
2779        ) {
2780            throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_FUNCTION or T_CLOSURE');
2781        }
2782
2783        $opener = $this->_tokens[$stackPtr]['parenthesis_opener'];
2784        $closer = $this->_tokens[$stackPtr]['parenthesis_closer'];
2785
2786        $vars            = array();
2787        $currVar         = null;
2788        $paramStart      = ($opener + 1);
2789        $defaultStart    = null;
2790        $paramCount      = 0;
2791        $passByReference = false;
2792        $variableLength  = false;
2793        $typeHint        = '';
2794        $nullableType    = false;
2795
2796        for ($i = $paramStart; $i <= $closer; $i++) {
2797            // Check to see if this token has a parenthesis or bracket opener. If it does
2798            // it's likely to be an array which might have arguments in it. This
2799            // could cause problems in our parsing below, so lets just skip to the
2800            // end of it.
2801            if (isset($this->_tokens[$i]['parenthesis_opener']) === true) {
2802                // Don't do this if it's the close parenthesis for the method.
2803                if ($i !== $this->_tokens[$i]['parenthesis_closer']) {
2804                    $i = ($this->_tokens[$i]['parenthesis_closer'] + 1);
2805                }
2806            }
2807
2808            if (isset($this->_tokens[$i]['bracket_opener']) === true) {
2809                // Don't do this if it's the close parenthesis for the method.
2810                if ($i !== $this->_tokens[$i]['bracket_closer']) {
2811                    $i = ($this->_tokens[$i]['bracket_closer'] + 1);
2812                }
2813            }
2814
2815            switch ($this->_tokens[$i]['code']) {
2816            case T_BITWISE_AND:
2817                $passByReference = true;
2818                break;
2819            case T_VARIABLE:
2820                $currVar = $i;
2821                break;
2822            case T_ELLIPSIS:
2823                $variableLength = true;
2824                break;
2825            case T_ARRAY_HINT:
2826            case T_CALLABLE:
2827                $typeHint .= $this->_tokens[$i]['content'];
2828                break;
2829            case T_SELF:
2830            case T_PARENT:
2831            case T_STATIC:
2832                // Self is valid, the others invalid, but were probably intended as type hints.
2833                if (isset($defaultStart) === false) {
2834                    $typeHint .= $this->_tokens[$i]['content'];
2835                }
2836                break;
2837            case T_STRING:
2838                // This is a string, so it may be a type hint, but it could
2839                // also be a constant used as a default value.
2840                $prevComma = false;
2841                for ($t = $i; $t >= $opener; $t--) {
2842                    if ($this->_tokens[$t]['code'] === T_COMMA) {
2843                        $prevComma = $t;
2844                        break;
2845                    }
2846                }
2847
2848                if ($prevComma !== false) {
2849                    $nextEquals = false;
2850                    for ($t = $prevComma; $t < $i; $t++) {
2851                        if ($this->_tokens[$t]['code'] === T_EQUAL) {
2852                            $nextEquals = $t;
2853                            break;
2854                        }
2855                    }
2856
2857                    if ($nextEquals !== false) {
2858                        break;
2859                    }
2860                }
2861
2862                if ($defaultStart === null) {
2863                    $typeHint .= $this->_tokens[$i]['content'];
2864                }
2865                break;
2866            case T_NS_SEPARATOR:
2867                // Part of a type hint or default value.
2868                if ($defaultStart === null) {
2869                    $typeHint .= $this->_tokens[$i]['content'];
2870                }
2871                break;
2872            case T_NULLABLE:
2873                if ($defaultStart === null) {
2874                    $nullableType = true;
2875                    $typeHint    .= $this->_tokens[$i]['content'];
2876                }
2877                break;
2878            case T_CLOSE_PARENTHESIS:
2879            case T_COMMA:
2880                // If it's null, then there must be no parameters for this
2881                // method.
2882                if ($currVar === null) {
2883                    continue;
2884                }
2885
2886                $vars[$paramCount]            = array();
2887                $vars[$paramCount]['token']   = $currVar;
2888                $vars[$paramCount]['name']    = $this->_tokens[$currVar]['content'];
2889                $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
2890
2891                if ($defaultStart !== null) {
2892                    $vars[$paramCount]['default'] = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
2893                }
2894
2895                $vars[$paramCount]['pass_by_reference'] = $passByReference;
2896                $vars[$paramCount]['variable_length']   = $variableLength;
2897                $vars[$paramCount]['type_hint']         = $typeHint;
2898                $vars[$paramCount]['nullable_type']     = $nullableType;
2899
2900                // Reset the vars, as we are about to process the next parameter.
2901                $defaultStart    = null;
2902                $paramStart      = ($i + 1);
2903                $passByReference = false;
2904                $variableLength  = false;
2905                $typeHint        = '';
2906                $nullableType    = false;
2907
2908                $paramCount++;
2909                break;
2910            case T_EQUAL:
2911                $defaultStart = ($i + 1);
2912                break;
2913            }//end switch
2914        }//end for
2915
2916        return $vars;
2917
2918    }//end getMethodParameters()
2919
2920
2921    /**
2922     * Returns the visibility and implementation properties of a method.
2923     *
2924     * The format of the array is:
2925     * <code>
2926     *   array(
2927     *    'scope'           => 'public', // public private or protected
2928     *    'scope_specified' => true,     // true is scope keyword was found.
2929     *    'is_abstract'     => false,    // true if the abstract keyword was found.
2930     *    'is_final'        => false,    // true if the final keyword was found.
2931     *    'is_static'       => false,    // true if the static keyword was found.
2932     *    'is_closure'      => false,    // true if no name is found.
2933     *   );
2934     * </code>
2935     *
2936     * @param int $stackPtr The position in the stack of the T_FUNCTION token to
2937     *                      acquire the properties for.
2938     *
2939     * @return array
2940     * @throws PHP_CodeSniffer_Exception If the specified position is not a
2941     *                                   T_FUNCTION token.
2942     */
2943    public function getMethodProperties($stackPtr)
2944    {
2945        if ($this->_tokens[$stackPtr]['code'] !== T_FUNCTION) {
2946            throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_FUNCTION');
2947        }
2948
2949        $valid = array(
2950                  T_PUBLIC      => T_PUBLIC,
2951                  T_PRIVATE     => T_PRIVATE,
2952                  T_PROTECTED   => T_PROTECTED,
2953                  T_STATIC      => T_STATIC,
2954                  T_FINAL       => T_FINAL,
2955                  T_ABSTRACT    => T_ABSTRACT,
2956                  T_WHITESPACE  => T_WHITESPACE,
2957                  T_COMMENT     => T_COMMENT,
2958                  T_DOC_COMMENT => T_DOC_COMMENT,
2959                 );
2960
2961        $scope          = 'public';
2962        $scopeSpecified = false;
2963        $isAbstract     = false;
2964        $isFinal        = false;
2965        $isStatic       = false;
2966        $isClosure      = $this->isAnonymousFunction($stackPtr);
2967
2968        for ($i = ($stackPtr - 1); $i > 0; $i--) {
2969            if (isset($valid[$this->_tokens[$i]['code']]) === false) {
2970                break;
2971            }
2972
2973            switch ($this->_tokens[$i]['code']) {
2974            case T_PUBLIC:
2975                $scope          = 'public';
2976                $scopeSpecified = true;
2977                break;
2978            case T_PRIVATE:
2979                $scope          = 'private';
2980                $scopeSpecified = true;
2981                break;
2982            case T_PROTECTED:
2983                $scope          = 'protected';
2984                $scopeSpecified = true;
2985                break;
2986            case T_ABSTRACT:
2987                $isAbstract = true;
2988                break;
2989            case T_FINAL:
2990                $isFinal = true;
2991                break;
2992            case T_STATIC:
2993                $isStatic = true;
2994                break;
2995            }//end switch
2996        }//end for
2997
2998        return array(
2999                'scope'           => $scope,
3000                'scope_specified' => $scopeSpecified,
3001                'is_abstract'     => $isAbstract,
3002                'is_final'        => $isFinal,
3003                'is_static'       => $isStatic,
3004                'is_closure'      => $isClosure,
3005               );
3006
3007    }//end getMethodProperties()
3008
3009
3010    /**
3011     * Returns the visibility and implementation properties of the class member
3012     * variable found at the specified position in the stack.
3013     *
3014     * The format of the array is:
3015     *
3016     * <code>
3017     *   array(
3018     *    'scope'       => 'public', // public private or protected
3019     *    'is_static'   => false,    // true if the static keyword was found.
3020     *   );
3021     * </code>
3022     *
3023     * @param int $stackPtr The position in the stack of the T_VARIABLE token to
3024     *                      acquire the properties for.
3025     *
3026     * @return array
3027     * @throws PHP_CodeSniffer_Exception If the specified position is not a
3028     *                                   T_VARIABLE token, or if the position is not
3029     *                                   a class member variable.
3030     */
3031    public function getMemberProperties($stackPtr)
3032    {
3033        if ($this->_tokens[$stackPtr]['code'] !== T_VARIABLE) {
3034            throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_VARIABLE');
3035        }
3036
3037        $conditions = array_keys($this->_tokens[$stackPtr]['conditions']);
3038        $ptr        = array_pop($conditions);
3039        if (isset($this->_tokens[$ptr]) === false
3040            || ($this->_tokens[$ptr]['code'] !== T_CLASS
3041            && $this->_tokens[$ptr]['code'] !== T_ANON_CLASS
3042            && $this->_tokens[$ptr]['code'] !== T_TRAIT)
3043        ) {
3044            if (isset($this->_tokens[$ptr]) === true
3045                && $this->_tokens[$ptr]['code'] === T_INTERFACE
3046            ) {
3047                // T_VARIABLEs in interfaces can actually be method arguments
3048                // but they wont be seen as being inside the method because there
3049                // are no scope openers and closers for abstract methods. If it is in
3050                // parentheses, we can be pretty sure it is a method argument.
3051                if (isset($this->_tokens[$stackPtr]['nested_parenthesis']) === false
3052                    || empty($this->_tokens[$stackPtr]['nested_parenthesis']) === true
3053                ) {
3054                    $error = 'Possible parse error: interfaces may not include member vars';
3055                    $this->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar');
3056                    return array();
3057                }
3058            } else {
3059                throw new PHP_CodeSniffer_Exception('$stackPtr is not a class member var');
3060            }
3061        }
3062
3063        $valid = array(
3064                  T_PUBLIC      => T_PUBLIC,
3065                  T_PRIVATE     => T_PRIVATE,
3066                  T_PROTECTED   => T_PROTECTED,
3067                  T_STATIC      => T_STATIC,
3068                  T_WHITESPACE  => T_WHITESPACE,
3069                  T_COMMENT     => T_COMMENT,
3070                  T_DOC_COMMENT => T_DOC_COMMENT,
3071                  T_VARIABLE    => T_VARIABLE,
3072                  T_COMMA       => T_COMMA,
3073                 );
3074
3075        $scope          = 'public';
3076        $scopeSpecified = false;
3077        $isStatic       = false;
3078
3079        for ($i = ($stackPtr - 1); $i > 0; $i--) {
3080            if (isset($valid[$this->_tokens[$i]['code']]) === false) {
3081                break;
3082            }
3083
3084            switch ($this->_tokens[$i]['code']) {
3085            case T_PUBLIC:
3086                $scope          = 'public';
3087                $scopeSpecified = true;
3088                break;
3089            case T_PRIVATE:
3090                $scope          = 'private';
3091                $scopeSpecified = true;
3092                break;
3093            case T_PROTECTED:
3094                $scope          = 'protected';
3095                $scopeSpecified = true;
3096                break;
3097            case T_STATIC:
3098                $isStatic = true;
3099                break;
3100            }
3101        }//end for
3102
3103        return array(
3104                'scope'           => $scope,
3105                'scope_specified' => $scopeSpecified,
3106                'is_static'       => $isStatic,
3107               );
3108
3109    }//end getMemberProperties()
3110
3111
3112    /**
3113     * Returns the visibility and implementation properties of a class.
3114     *
3115     * The format of the array is:
3116     * <code>
3117     *   array(
3118     *    'is_abstract' => false, // true if the abstract keyword was found.
3119     *    'is_final'    => false, // true if the final keyword was found.
3120     *   );
3121     * </code>
3122     *
3123     * @param int $stackPtr The position in the stack of the T_CLASS token to
3124     *                      acquire the properties for.
3125     *
3126     * @return array
3127     * @throws PHP_CodeSniffer_Exception If the specified position is not a
3128     *                                   T_CLASS token.
3129     */
3130    public function getClassProperties($stackPtr)
3131    {
3132        if ($this->_tokens[$stackPtr]['code'] !== T_CLASS) {
3133            throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_CLASS');
3134        }
3135
3136        $valid = array(
3137                  T_FINAL       => T_FINAL,
3138                  T_ABSTRACT    => T_ABSTRACT,
3139                  T_WHITESPACE  => T_WHITESPACE,
3140                  T_COMMENT     => T_COMMENT,
3141                  T_DOC_COMMENT => T_DOC_COMMENT,
3142                 );
3143
3144        $isAbstract = false;
3145        $isFinal    = false;
3146
3147        for ($i = ($stackPtr - 1); $i > 0; $i--) {
3148            if (isset($valid[$this->_tokens[$i]['code']]) === false) {
3149                break;
3150            }
3151
3152            switch ($this->_tokens[$i]['code']) {
3153            case T_ABSTRACT:
3154                $isAbstract = true;
3155                break;
3156
3157            case T_FINAL:
3158                $isFinal = true;
3159                break;
3160            }
3161        }//end for
3162
3163        return array(
3164                'is_abstract' => $isAbstract,
3165                'is_final'    => $isFinal,
3166               );
3167
3168    }//end getClassProperties()
3169
3170
3171    /**
3172     * Determine if the passed token is a reference operator.
3173     *
3174     * Returns true if the specified token position represents a reference.
3175     * Returns false if the token represents a bitwise operator.
3176     *
3177     * @param int $stackPtr The position of the T_BITWISE_AND token.
3178     *
3179     * @return boolean
3180     */
3181    public function isReference($stackPtr)
3182    {
3183        if ($this->_tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
3184            return false;
3185        }
3186
3187        $tokenBefore = $this->findPrevious(
3188            PHP_CodeSniffer_Tokens::$emptyTokens,
3189            ($stackPtr - 1),
3190            null,
3191            true
3192        );
3193
3194        if ($this->_tokens[$tokenBefore]['code'] === T_FUNCTION) {
3195            // Function returns a reference.
3196            return true;
3197        }
3198
3199        if ($this->_tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
3200            // Inside a foreach loop, this is a reference.
3201            return true;
3202        }
3203
3204        if ($this->_tokens[$tokenBefore]['code'] === T_AS) {
3205            // Inside a foreach loop, this is a reference.
3206            return true;
3207        }
3208
3209        if ($this->_tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY) {
3210            // Inside an array declaration, this is a reference.
3211            return true;
3212        }
3213
3214        if (isset(PHP_CodeSniffer_Tokens::$assignmentTokens[$this->_tokens[$tokenBefore]['code']]) === true) {
3215            // This is directly after an assignment. It's a reference. Even if
3216            // it is part of an operation, the other tests will handle it.
3217            return true;
3218        }
3219
3220        if (isset($this->_tokens[$stackPtr]['nested_parenthesis']) === true) {
3221            $brackets    = $this->_tokens[$stackPtr]['nested_parenthesis'];
3222            $lastBracket = array_pop($brackets);
3223            if (isset($this->_tokens[$lastBracket]['parenthesis_owner']) === true) {
3224                $owner = $this->_tokens[$this->_tokens[$lastBracket]['parenthesis_owner']];
3225                if ($owner['code'] === T_FUNCTION
3226                    || $owner['code'] === T_CLOSURE
3227                    || $owner['code'] === T_ARRAY
3228                ) {
3229                    // Inside a function or array declaration, this is a reference.
3230                    return true;
3231                }
3232            } else {
3233                $prev = false;
3234                for ($t = ($this->_tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
3235                    if ($this->_tokens[$t]['code'] !== T_WHITESPACE) {
3236                        $prev = $t;
3237                        break;
3238                    }
3239                }
3240
3241                if ($prev !== false && $this->_tokens[$prev]['code'] === T_USE) {
3242                    return true;
3243                }
3244            }//end if
3245        }//end if
3246
3247        $tokenAfter = $this->findNext(
3248            PHP_CodeSniffer_Tokens::$emptyTokens,
3249            ($stackPtr + 1),
3250            null,
3251            true
3252        );
3253
3254        if ($this->_tokens[$tokenAfter]['code'] === T_VARIABLE
3255            && ($this->_tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
3256            || $this->_tokens[$tokenBefore]['code'] === T_COMMA)
3257        ) {
3258            return true;
3259        }
3260
3261        return false;
3262
3263    }//end isReference()
3264
3265
3266    /**
3267     * Returns the content of the tokens from the specified start position in
3268     * the token stack for the specified length.
3269     *
3270     * @param int $start  The position to start from in the token stack.
3271     * @param int $length The length of tokens to traverse from the start pos.
3272     *
3273     * @return string The token contents.
3274     */
3275    public function getTokensAsString($start, $length)
3276    {
3277        $str = '';
3278        $end = ($start + $length);
3279        if ($end > $this->numTokens) {
3280            $end = $this->numTokens;
3281        }
3282
3283        for ($i = $start; $i < $end; $i++) {
3284            $str .= $this->_tokens[$i]['content'];
3285        }
3286
3287        return $str;
3288
3289    }//end getTokensAsString()
3290
3291
3292    /**
3293     * Returns the position of the previous specified token(s).
3294     *
3295     * If a value is specified, the previous token of the specified type(s)
3296     * containing the specified value will be returned.
3297     *
3298     * Returns false if no token can be found.
3299     *
3300     * @param int|array $types   The type(s) of tokens to search for.
3301     * @param int       $start   The position to start searching from in the
3302     *                           token stack.
3303     * @param int       $end     The end position to fail if no token is found.
3304     *                           if not specified or null, end will default to
3305     *                           the start of the token stack.
3306     * @param bool      $exclude If true, find the previous token that are NOT of
3307     *                           the types specified in $types.
3308     * @param string    $value   The value that the token(s) must be equal to.
3309     *                           If value is omitted, tokens with any value will
3310     *                           be returned.
3311     * @param bool      $local   If true, tokens outside the current statement
3312     *                           will not be checked. IE. checking will stop
3313     *                           at the previous semi-colon found.
3314     *
3315     * @return int|bool
3316     * @see    findNext()
3317     */
3318    public function findPrevious(
3319        $types,
3320        $start,
3321        $end=null,
3322        $exclude=false,
3323        $value=null,
3324        $local=false
3325    ) {
3326        $types = (array) $types;
3327
3328        if ($end === null) {
3329            $end = 0;
3330        }
3331
3332        for ($i = $start; $i >= $end; $i--) {
3333            $found = (bool) $exclude;
3334            foreach ($types as $type) {
3335                if ($this->_tokens[$i]['code'] === $type) {
3336                    $found = !$exclude;
3337                    break;
3338                }
3339            }
3340
3341            if ($found === true) {
3342                if ($value === null) {
3343                    return $i;
3344                } else if ($this->_tokens[$i]['content'] === $value) {
3345                    return $i;
3346                }
3347            }
3348
3349            if ($local === true) {
3350                if (isset($this->_tokens[$i]['scope_opener']) === true
3351                    && $i === $this->_tokens[$i]['scope_closer']
3352                ) {
3353                    $i = $this->_tokens[$i]['scope_opener'];
3354                } else if (isset($this->_tokens[$i]['bracket_opener']) === true
3355                    && $i === $this->_tokens[$i]['bracket_closer']
3356                ) {
3357                    $i = $this->_tokens[$i]['bracket_opener'];
3358                } else if (isset($this->_tokens[$i]['parenthesis_opener']) === true
3359                    && $i === $this->_tokens[$i]['parenthesis_closer']
3360                ) {
3361                    $i = $this->_tokens[$i]['parenthesis_opener'];
3362                } else if ($this->_tokens[$i]['code'] === T_SEMICOLON) {
3363                    break;
3364                }
3365            }
3366        }//end for
3367
3368        return false;
3369
3370    }//end findPrevious()
3371
3372
3373    /**
3374     * Returns the position of the next specified token(s).
3375     *
3376     * If a value is specified, the next token of the specified type(s)
3377     * containing the specified value will be returned.
3378     *
3379     * Returns false if no token can be found.
3380     *
3381     * @param int|array $types   The type(s) of tokens to search for.
3382     * @param int       $start   The position to start searching from in the
3383     *                           token stack.
3384     * @param int       $end     The end position to fail if no token is found.
3385     *                           if not specified or null, end will default to
3386     *                           the end of the token stack.
3387     * @param bool      $exclude If true, find the next token that is NOT of
3388     *                           a type specified in $types.
3389     * @param string    $value   The value that the token(s) must be equal to.
3390     *                           If value is omitted, tokens with any value will
3391     *                           be returned.
3392     * @param bool      $local   If true, tokens outside the current statement
3393     *                           will not be checked. i.e., checking will stop
3394     *                           at the next semi-colon found.
3395     *
3396     * @return int|bool
3397     * @see    findPrevious()
3398     */
3399    public function findNext(
3400        $types,
3401        $start,
3402        $end=null,
3403        $exclude=false,
3404        $value=null,
3405        $local=false
3406    ) {
3407        $types = (array) $types;
3408
3409        if ($end === null || $end > $this->numTokens) {
3410            $end = $this->numTokens;
3411        }
3412
3413        for ($i = $start; $i < $end; $i++) {
3414            $found = (bool) $exclude;
3415            foreach ($types as $type) {
3416                if ($this->_tokens[$i]['code'] === $type) {
3417                    $found = !$exclude;
3418                    break;
3419                }
3420            }
3421
3422            if ($found === true) {
3423                if ($value === null) {
3424                    return $i;
3425                } else if ($this->_tokens[$i]['content'] === $value) {
3426                    return $i;
3427                }
3428            }
3429
3430            if ($local === true && $this->_tokens[$i]['code'] === T_SEMICOLON) {
3431                break;
3432            }
3433        }//end for
3434
3435        return false;
3436
3437    }//end findNext()
3438
3439
3440    /**
3441     * Returns the position of the first non-whitespace token in a statement.
3442     *
3443     * @param int       $start  The position to start searching from in the token stack.
3444     * @param int|array $ignore Token types that should not be considered stop points.
3445     *
3446     * @return int
3447     */
3448    public function findStartOfStatement($start, $ignore=null)
3449    {
3450        $endTokens = PHP_CodeSniffer_Tokens::$blockOpeners;
3451
3452        $endTokens[T_COLON]            = true;
3453        $endTokens[T_COMMA]            = true;
3454        $endTokens[T_DOUBLE_ARROW]     = true;
3455        $endTokens[T_SEMICOLON]        = true;
3456        $endTokens[T_OPEN_TAG]         = true;
3457        $endTokens[T_CLOSE_TAG]        = true;
3458        $endTokens[T_OPEN_SHORT_ARRAY] = true;
3459
3460        if ($ignore !== null) {
3461            $ignore = (array) $ignore;
3462            foreach ($ignore as $code) {
3463                if (isset($endTokens[$code]) === true) {
3464                    unset($endTokens[$code]);
3465                }
3466            }
3467        }
3468
3469        $lastNotEmpty = $start;
3470
3471        for ($i = $start; $i >= 0; $i--) {
3472            if (isset($endTokens[$this->_tokens[$i]['code']]) === true) {
3473                // Found the end of the previous statement.
3474                return $lastNotEmpty;
3475            }
3476
3477            if (isset($this->_tokens[$i]['scope_opener']) === true
3478                && $i === $this->_tokens[$i]['scope_closer']
3479            ) {
3480                // Found the end of the previous scope block.
3481                return $lastNotEmpty;
3482            }
3483
3484            // Skip nested statements.
3485            if (isset($this->_tokens[$i]['bracket_opener']) === true
3486                && $i === $this->_tokens[$i]['bracket_closer']
3487            ) {
3488                $i = $this->_tokens[$i]['bracket_opener'];
3489            } else if (isset($this->_tokens[$i]['parenthesis_opener']) === true
3490                && $i === $this->_tokens[$i]['parenthesis_closer']
3491            ) {
3492                $i = $this->_tokens[$i]['parenthesis_opener'];
3493            }
3494
3495            if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$this->_tokens[$i]['code']]) === false) {
3496                $lastNotEmpty = $i;
3497            }
3498        }//end for
3499
3500        return 0;
3501
3502    }//end findStartOfStatement()
3503
3504
3505    /**
3506     * Returns the position of the last non-whitespace token in a statement.
3507     *
3508     * @param int       $start  The position to start searching from in the token stack.
3509     * @param int|array $ignore Token types that should not be considered stop points.
3510     *
3511     * @return int
3512     */
3513    public function findEndOfStatement($start, $ignore=null)
3514    {
3515        $endTokens = array(
3516                      T_COLON                => true,
3517                      T_COMMA                => true,
3518                      T_DOUBLE_ARROW         => true,
3519                      T_SEMICOLON            => true,
3520                      T_CLOSE_PARENTHESIS    => true,
3521                      T_CLOSE_SQUARE_BRACKET => true,
3522                      T_CLOSE_CURLY_BRACKET  => true,
3523                      T_CLOSE_SHORT_ARRAY    => true,
3524                      T_OPEN_TAG             => true,
3525                      T_CLOSE_TAG            => true,
3526                     );
3527
3528        if ($ignore !== null) {
3529            $ignore = (array) $ignore;
3530            foreach ($ignore as $code) {
3531                if (isset($endTokens[$code]) === true) {
3532                    unset($endTokens[$code]);
3533                }
3534            }
3535        }
3536
3537        $lastNotEmpty = $start;
3538
3539        for ($i = $start; $i < $this->numTokens; $i++) {
3540            if ($i !== $start && isset($endTokens[$this->_tokens[$i]['code']]) === true) {
3541                // Found the end of the statement.
3542                if ($this->_tokens[$i]['code'] === T_CLOSE_PARENTHESIS
3543                    || $this->_tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
3544                    || $this->_tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
3545                    || $this->_tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
3546                    || $this->_tokens[$i]['code'] === T_OPEN_TAG
3547                    || $this->_tokens[$i]['code'] === T_CLOSE_TAG
3548                ) {
3549                    return $lastNotEmpty;
3550                }
3551
3552                return $i;
3553            }
3554
3555            // Skip nested statements.
3556            if (isset($this->_tokens[$i]['scope_closer']) === true
3557                && ($i === $this->_tokens[$i]['scope_opener']
3558                || $i === $this->_tokens[$i]['scope_condition'])
3559            ) {
3560                $i = $this->_tokens[$i]['scope_closer'];
3561            } else if (isset($this->_tokens[$i]['bracket_closer']) === true
3562                && $i === $this->_tokens[$i]['bracket_opener']
3563            ) {
3564                $i = $this->_tokens[$i]['bracket_closer'];
3565            } else if (isset($this->_tokens[$i]['parenthesis_closer']) === true
3566                && $i === $this->_tokens[$i]['parenthesis_opener']
3567            ) {
3568                $i = $this->_tokens[$i]['parenthesis_closer'];
3569            }
3570
3571            if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$this->_tokens[$i]['code']]) === false) {
3572                $lastNotEmpty = $i;
3573            }
3574        }//end for
3575
3576        return ($this->numTokens - 1);
3577
3578    }//end findEndOfStatement()
3579
3580
3581    /**
3582     * Returns the position of the first token on a line, matching given type.
3583     *
3584     * Returns false if no token can be found.
3585     *
3586     * @param int|array $types   The type(s) of tokens to search for.
3587     * @param int       $start   The position to start searching from in the
3588     *                           token stack. The first token matching on
3589     *                           this line before this token will be returned.
3590     * @param bool      $exclude If true, find the token that is NOT of
3591     *                           the types specified in $types.
3592     * @param string    $value   The value that the token must be equal to.
3593     *                           If value is omitted, tokens with any value will
3594     *                           be returned.
3595     *
3596     * @return int | bool
3597     */
3598    public function findFirstOnLine($types, $start, $exclude=false, $value=null)
3599    {
3600        if (is_array($types) === false) {
3601            $types = array($types);
3602        }
3603
3604        $foundToken = false;
3605
3606        for ($i = $start; $i >= 0; $i--) {
3607            if ($this->_tokens[$i]['line'] < $this->_tokens[$start]['line']) {
3608                break;
3609            }
3610
3611            $found = $exclude;
3612            foreach ($types as $type) {
3613                if ($exclude === false) {
3614                    if ($this->_tokens[$i]['code'] === $type) {
3615                        $found = true;
3616                        break;
3617                    }
3618                } else {
3619                    if ($this->_tokens[$i]['code'] === $type) {
3620                        $found = false;
3621                        break;
3622                    }
3623                }
3624            }
3625
3626            if ($found === true) {
3627                if ($value === null) {
3628                    $foundToken = $i;
3629                } else if ($this->_tokens[$i]['content'] === $value) {
3630                    $foundToken = $i;
3631                }
3632            }
3633        }//end for
3634
3635        return $foundToken;
3636
3637    }//end findFirstOnLine()
3638
3639
3640    /**
3641     * Determine if the passed token has a condition of one of the passed types.
3642     *
3643     * @param int       $stackPtr The position of the token we are checking.
3644     * @param int|array $types    The type(s) of tokens to search for.
3645     *
3646     * @return boolean
3647     */
3648    public function hasCondition($stackPtr, $types)
3649    {
3650        // Check for the existence of the token.
3651        if (isset($this->_tokens[$stackPtr]) === false) {
3652            return false;
3653        }
3654
3655        // Make sure the token has conditions.
3656        if (isset($this->_tokens[$stackPtr]['conditions']) === false) {
3657            return false;
3658        }
3659
3660        $types      = (array) $types;
3661        $conditions = $this->_tokens[$stackPtr]['conditions'];
3662
3663        foreach ($types as $type) {
3664            if (in_array($type, $conditions) === true) {
3665                // We found a token with the required type.
3666                return true;
3667            }
3668        }
3669
3670        return false;
3671
3672    }//end hasCondition()
3673
3674
3675    /**
3676     * Return the position of the condition for the passed token.
3677     *
3678     * Returns FALSE if the token does not have the condition.
3679     *
3680     * @param int $stackPtr The position of the token we are checking.
3681     * @param int $type     The type of token to search for.
3682     *
3683     * @return int
3684     */
3685    public function getCondition($stackPtr, $type)
3686    {
3687        // Check for the existence of the token.
3688        if (isset($this->_tokens[$stackPtr]) === false) {
3689            return false;
3690        }
3691
3692        // Make sure the token has conditions.
3693        if (isset($this->_tokens[$stackPtr]['conditions']) === false) {
3694            return false;
3695        }
3696
3697        $conditions = $this->_tokens[$stackPtr]['conditions'];
3698        foreach ($conditions as $token => $condition) {
3699            if ($condition === $type) {
3700                return $token;
3701            }
3702        }
3703
3704        return false;
3705
3706    }//end getCondition()
3707
3708
3709    /**
3710     * Returns the name of the class that the specified class extends.
3711     *
3712     * Returns FALSE on error or if there is no extended class name.
3713     *
3714     * @param int $stackPtr The stack position of the class.
3715     *
3716     * @return string
3717     */
3718    public function findExtendedClassName($stackPtr)
3719    {
3720        // Check for the existence of the token.
3721        if (isset($this->_tokens[$stackPtr]) === false) {
3722            return false;
3723        }
3724
3725        if ($this->_tokens[$stackPtr]['code'] !== T_CLASS
3726            && $this->_tokens[$stackPtr]['code'] !== T_ANON_CLASS
3727        ) {
3728            return false;
3729        }
3730
3731        if (isset($this->_tokens[$stackPtr]['scope_closer']) === false) {
3732            return false;
3733        }
3734
3735        $classCloserIndex = $this->_tokens[$stackPtr]['scope_closer'];
3736        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classCloserIndex);
3737        if (false === $extendsIndex) {
3738            return false;
3739        }
3740
3741        $find = array(
3742                 T_NS_SEPARATOR,
3743                 T_STRING,
3744                 T_WHITESPACE,
3745                );
3746
3747        $end  = $this->findNext($find, ($extendsIndex + 1), $classCloserIndex, true);
3748        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
3749        $name = trim($name);
3750
3751        if ($name === '') {
3752            return false;
3753        }
3754
3755        return $name;
3756
3757    }//end findExtendedClassName()
3758
3759
3760    /**
3761     * Returns the name(s) of the interface(s) that the specified class implements.
3762     *
3763     * Returns FALSE on error or if there are no implemented interface names.
3764     *
3765     * @param int $stackPtr The stack position of the class.
3766     *
3767     * @return array|false
3768     */
3769    public function findImplementedInterfaceNames($stackPtr)
3770    {
3771        // Check for the existence of the token.
3772        if (isset($this->_tokens[$stackPtr]) === false) {
3773            return false;
3774        }
3775
3776        if ($this->_tokens[$stackPtr]['code'] !== T_CLASS
3777            && $this->_tokens[$stackPtr]['code'] !== T_ANON_CLASS
3778        ) {
3779            return false;
3780        }
3781
3782        if (isset($this->_tokens[$stackPtr]['scope_closer']) === false) {
3783            return false;
3784        }
3785
3786        $classOpenerIndex = $this->_tokens[$stackPtr]['scope_opener'];
3787        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
3788        if ($implementsIndex === false) {
3789            return false;
3790        }
3791
3792        $find = array(
3793                 T_NS_SEPARATOR,
3794                 T_STRING,
3795                 T_WHITESPACE,
3796                 T_COMMA,
3797                );
3798
3799        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
3800        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
3801        $name = trim($name);
3802
3803        if ($name === '') {
3804            return false;
3805        } else {
3806            $names = explode(',', $name);
3807            $names = array_map('trim', $names);
3808            return $names;
3809        }
3810
3811    }//end findImplementedInterfaceNames()
3812
3813
3814}//end class
3815