comparison vendor/squizlabs/php_codesniffer/CodeSniffer/File.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 7a779792577d
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
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 */
117 class 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 % $tabWidth) + 1);
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 *'.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 is a
2273 // variable or object operator then this is an opener
2274 // for a string offset and not a scope.
2275 if ($tokens[$x]['code'] === T_VARIABLE
2276 || $tokens[$x]['code'] === T_OBJECT_OPERATOR
2277 ) {
2278 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2279 echo str_repeat("\t", $depth);
2280 echo '* ignoring curly brace *'.PHP_EOL;
2281 }
2282
2283 $ignore++;
2284 }//end if
2285
2286 break;
2287 }//end if
2288 }//end for
2289 }//end if
2290 }//end if
2291
2292 if ($ignore === 0 || $tokenType !== T_OPEN_CURLY_BRACKET) {
2293 // We found the opening scope token for $currType.
2294 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2295 $type = $tokens[$stackPtr]['type'];
2296 echo str_repeat("\t", $depth);
2297 echo "=> Found scope opener for $stackPtr:$type".PHP_EOL;
2298 }
2299
2300 $opener = $i;
2301 }
2302 } else if ($tokenType === T_OPEN_PARENTHESIS) {
2303 if (isset($tokens[$i]['parenthesis_owner']) === true) {
2304 $owner = $tokens[$i]['parenthesis_owner'];
2305 if (isset(PHP_CodeSniffer_Tokens::$scopeOpeners[$tokens[$owner]['code']]) === true
2306 && isset($tokens[$i]['parenthesis_closer']) === true
2307 ) {
2308 // If we get into here, then we opened a parenthesis for
2309 // a scope (eg. an if or else if) so we need to update the
2310 // start of the line so that when we check to see
2311 // if the closing parenthesis is more than 3 lines away from
2312 // the statement, we check from the closing parenthesis.
2313 $startLine = $tokens[$tokens[$i]['parenthesis_closer']]['line'];
2314 }
2315 }
2316 } else if ($tokenType === T_OPEN_CURLY_BRACKET && $opener !== null) {
2317 // We opened something that we don't have a scope opener for.
2318 // Examples of this are curly brackets for string offsets etc.
2319 // We want to ignore this so that we don't have an invalid scope
2320 // map.
2321 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2322 echo str_repeat("\t", $depth);
2323 echo '* ignoring curly brace *'.PHP_EOL;
2324 }
2325
2326 $ignore++;
2327 } else if ($tokenType === T_CLOSE_CURLY_BRACKET && $ignore > 0) {
2328 // We found the end token for the opener we were ignoring.
2329 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2330 echo str_repeat("\t", $depth);
2331 echo '* finished ignoring curly brace *'.PHP_EOL;
2332 }
2333
2334 $ignore--;
2335 } else if ($opener === null
2336 && isset($tokenizer->scopeOpeners[$currType]) === true
2337 ) {
2338 // If we still haven't found the opener after 3 lines,
2339 // we're not going to find it, unless we know it requires
2340 // an opener (in which case we better keep looking) or the last
2341 // token was empty (in which case we'll just confirm there is
2342 // more code in this file and not just a big comment).
2343 if ($tokens[$i]['line'] >= ($startLine + 3)
2344 && isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[($i - 1)]['code']]) === false
2345 ) {
2346 if ($tokenizer->scopeOpeners[$currType]['strict'] === true) {
2347 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2348 $type = $tokens[$stackPtr]['type'];
2349 $lines = ($tokens[$i]['line'] - $startLine);
2350 echo str_repeat("\t", $depth);
2351 echo "=> Still looking for $stackPtr:$type scope opener after $lines lines".PHP_EOL;
2352 }
2353 } else {
2354 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2355 $type = $tokens[$stackPtr]['type'];
2356 echo str_repeat("\t", $depth);
2357 echo "=> Couldn't find scope opener for $stackPtr:$type, bailing".PHP_EOL;
2358 }
2359
2360 return $stackPtr;
2361 }
2362 }
2363 } else if ($opener !== null
2364 && $tokenType !== T_BREAK
2365 && isset($tokenizer->endScopeTokens[$tokenType]) === true
2366 ) {
2367 if (isset($tokens[$i]['scope_condition']) === false) {
2368 if ($ignore > 0) {
2369 // We found the end token for the opener we were ignoring.
2370 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2371 echo str_repeat("\t", $depth);
2372 echo '* finished ignoring curly brace *'.PHP_EOL;
2373 }
2374
2375 $ignore--;
2376 } else {
2377 // We found a token that closes the scope but it doesn't
2378 // have a condition, so it belongs to another token and
2379 // our token doesn't have a closer, so pretend this is
2380 // the closer.
2381 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2382 $type = $tokens[$stackPtr]['type'];
2383 echo str_repeat("\t", $depth);
2384 echo "=> Found (unexpected) scope closer for $stackPtr:$type".PHP_EOL;
2385 }
2386
2387 foreach (array($stackPtr, $opener) as $token) {
2388 $tokens[$token]['scope_condition'] = $stackPtr;
2389 $tokens[$token]['scope_opener'] = $opener;
2390 $tokens[$token]['scope_closer'] = $i;
2391 }
2392
2393 return ($i - 1);
2394 }//end if
2395 }//end if
2396 }//end if
2397 }//end for
2398
2399 return $stackPtr;
2400
2401 }//end _recurseScopeMap()
2402
2403
2404 /**
2405 * Constructs the level map.
2406 *
2407 * The level map adds a 'level' index to each token which indicates the
2408 * depth that a token within a set of scope blocks. It also adds a
2409 * 'condition' index which is an array of the scope conditions that opened
2410 * each of the scopes - position 0 being the first scope opener.
2411 *
2412 * @param array $tokens The array of tokens to process.
2413 * @param object $tokenizer The tokenizer being used to process this file.
2414 * @param string $eolChar The EOL character to use for splitting strings.
2415 *
2416 * @return void
2417 */
2418 private static function _createLevelMap(&$tokens, $tokenizer, $eolChar)
2419 {
2420 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2421 echo "\t*** START LEVEL MAP ***".PHP_EOL;
2422 }
2423
2424 $numTokens = count($tokens);
2425 $level = 0;
2426 $conditions = array();
2427 $lastOpener = null;
2428 $openers = array();
2429
2430 for ($i = 0; $i < $numTokens; $i++) {
2431 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2432 $type = $tokens[$i]['type'];
2433 $line = $tokens[$i]['line'];
2434 $len = $tokens[$i]['length'];
2435 $col = $tokens[$i]['column'];
2436
2437 $content = PHP_CodeSniffer::prepareForOutput($tokens[$i]['content']);
2438
2439 echo str_repeat("\t", ($level + 1));
2440 echo "Process token $i on line $line [col:$col;len:$len;lvl:$level;";
2441 if (empty($conditions) !== true) {
2442 $condString = 'conds;';
2443 foreach ($conditions as $condition) {
2444 $condString .= token_name($condition).',';
2445 }
2446
2447 echo rtrim($condString, ',').';';
2448 }
2449
2450 echo "]: $type => $content".PHP_EOL;
2451 }//end if
2452
2453 $tokens[$i]['level'] = $level;
2454 $tokens[$i]['conditions'] = $conditions;
2455
2456 if (isset($tokens[$i]['scope_condition']) === true) {
2457 // Check to see if this token opened the scope.
2458 if ($tokens[$i]['scope_opener'] === $i) {
2459 $stackPtr = $tokens[$i]['scope_condition'];
2460 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2461 $type = $tokens[$stackPtr]['type'];
2462 echo str_repeat("\t", ($level + 1));
2463 echo "=> Found scope opener for $stackPtr:$type".PHP_EOL;
2464 }
2465
2466 $stackPtr = $tokens[$i]['scope_condition'];
2467
2468 // If we find a scope opener that has a shared closer,
2469 // then we need to go back over the condition map that we
2470 // just created and fix ourselves as we just added some
2471 // conditions where there was none. This happens for T_CASE
2472 // statements that are using the same break statement.
2473 if ($lastOpener !== null && $tokens[$lastOpener]['scope_closer'] === $tokens[$i]['scope_closer']) {
2474 // This opener shares its closer with the previous opener,
2475 // but we still need to check if the two openers share their
2476 // closer with each other directly (like CASE and DEFAULT)
2477 // or if they are just sharing because one doesn't have a
2478 // closer (like CASE with no BREAK using a SWITCHes closer).
2479 $thisType = $tokens[$tokens[$i]['scope_condition']]['code'];
2480 $opener = $tokens[$lastOpener]['scope_condition'];
2481
2482 $isShared = isset($tokenizer->scopeOpeners[$thisType]['with'][$tokens[$opener]['code']]);
2483
2484 reset($tokenizer->scopeOpeners[$thisType]['end']);
2485 reset($tokenizer->scopeOpeners[$tokens[$opener]['code']]['end']);
2486 $sameEnd = (current($tokenizer->scopeOpeners[$thisType]['end']) === current($tokenizer->scopeOpeners[$tokens[$opener]['code']]['end']));
2487
2488 if ($isShared === true && $sameEnd === true) {
2489 $badToken = $opener;
2490 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2491 $type = $tokens[$badToken]['type'];
2492 echo str_repeat("\t", ($level + 1));
2493 echo "* shared closer, cleaning up $badToken:$type *".PHP_EOL;
2494 }
2495
2496 for ($x = $tokens[$i]['scope_condition']; $x <= $i; $x++) {
2497 $oldConditions = $tokens[$x]['conditions'];
2498 $oldLevel = $tokens[$x]['level'];
2499 $tokens[$x]['level']--;
2500 unset($tokens[$x]['conditions'][$badToken]);
2501 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2502 $type = $tokens[$x]['type'];
2503 $oldConds = '';
2504 foreach ($oldConditions as $condition) {
2505 $oldConds .= token_name($condition).',';
2506 }
2507
2508 $oldConds = rtrim($oldConds, ',');
2509
2510 $newConds = '';
2511 foreach ($tokens[$x]['conditions'] as $condition) {
2512 $newConds .= token_name($condition).',';
2513 }
2514
2515 $newConds = rtrim($newConds, ',');
2516
2517 $newLevel = $tokens[$x]['level'];
2518 echo str_repeat("\t", ($level + 1));
2519 echo "* cleaned $x:$type *".PHP_EOL;
2520 echo str_repeat("\t", ($level + 2));
2521 echo "=> level changed from $oldLevel to $newLevel".PHP_EOL;
2522 echo str_repeat("\t", ($level + 2));
2523 echo "=> conditions changed from $oldConds to $newConds".PHP_EOL;
2524 }//end if
2525 }//end for
2526
2527 unset($conditions[$badToken]);
2528 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2529 $type = $tokens[$badToken]['type'];
2530 echo str_repeat("\t", ($level + 1));
2531 echo "* token $badToken:$type removed from conditions array *".PHP_EOL;
2532 }
2533
2534 unset($openers[$lastOpener]);
2535
2536 $level--;
2537 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2538 echo str_repeat("\t", ($level + 2));
2539 echo '* level decreased *'.PHP_EOL;
2540 }
2541 }//end if
2542 }//end if
2543
2544 $level++;
2545 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2546 echo str_repeat("\t", ($level + 1));
2547 echo '* level increased *'.PHP_EOL;
2548 }
2549
2550 $conditions[$stackPtr] = $tokens[$stackPtr]['code'];
2551 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2552 $type = $tokens[$stackPtr]['type'];
2553 echo str_repeat("\t", ($level + 1));
2554 echo "* token $stackPtr:$type added to conditions array *".PHP_EOL;
2555 }
2556
2557 $lastOpener = $tokens[$i]['scope_opener'];
2558 if ($lastOpener !== null) {
2559 $openers[$lastOpener] = $lastOpener;
2560 }
2561 } else if ($lastOpener !== null && $tokens[$lastOpener]['scope_closer'] === $i) {
2562 foreach (array_reverse($openers) as $opener) {
2563 if ($tokens[$opener]['scope_closer'] === $i) {
2564 $oldOpener = array_pop($openers);
2565 if (empty($openers) === false) {
2566 $lastOpener = array_pop($openers);
2567 $openers[$lastOpener] = $lastOpener;
2568 } else {
2569 $lastOpener = null;
2570 }
2571
2572 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2573 $type = $tokens[$oldOpener]['type'];
2574 echo str_repeat("\t", ($level + 1));
2575 echo "=> Found scope closer for $oldOpener:$type".PHP_EOL;
2576 }
2577
2578 $oldCondition = array_pop($conditions);
2579 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2580 echo str_repeat("\t", ($level + 1));
2581 echo '* token '.token_name($oldCondition).' removed from conditions array *'.PHP_EOL;
2582 }
2583
2584 // Make sure this closer actually belongs to us.
2585 // Either the condition also has to think this is the
2586 // closer, or it has to allow sharing with us.
2587 $condition = $tokens[$tokens[$i]['scope_condition']]['code'];
2588 if ($condition !== $oldCondition) {
2589 if (isset($tokenizer->scopeOpeners[$oldCondition]['with'][$condition]) === false) {
2590 $badToken = $tokens[$oldOpener]['scope_condition'];
2591
2592 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2593 $type = token_name($oldCondition);
2594 echo str_repeat("\t", ($level + 1));
2595 echo "* scope closer was bad, cleaning up $badToken:$type *".PHP_EOL;
2596 }
2597
2598 for ($x = ($oldOpener + 1); $x <= $i; $x++) {
2599 $oldConditions = $tokens[$x]['conditions'];
2600 $oldLevel = $tokens[$x]['level'];
2601 $tokens[$x]['level']--;
2602 unset($tokens[$x]['conditions'][$badToken]);
2603 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2604 $type = $tokens[$x]['type'];
2605 $oldConds = '';
2606 foreach ($oldConditions as $condition) {
2607 $oldConds .= token_name($condition).',';
2608 }
2609
2610 $oldConds = rtrim($oldConds, ',');
2611
2612 $newConds = '';
2613 foreach ($tokens[$x]['conditions'] as $condition) {
2614 $newConds .= token_name($condition).',';
2615 }
2616
2617 $newConds = rtrim($newConds, ',');
2618
2619 $newLevel = $tokens[$x]['level'];
2620 echo str_repeat("\t", ($level + 1));
2621 echo "* cleaned $x:$type *".PHP_EOL;
2622 echo str_repeat("\t", ($level + 2));
2623 echo "=> level changed from $oldLevel to $newLevel".PHP_EOL;
2624 echo str_repeat("\t", ($level + 2));
2625 echo "=> conditions changed from $oldConds to $newConds".PHP_EOL;
2626 }//end if
2627 }//end for
2628 }//end if
2629 }//end if
2630
2631 $level--;
2632 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2633 echo str_repeat("\t", ($level + 2));
2634 echo '* level decreased *'.PHP_EOL;
2635 }
2636
2637 $tokens[$i]['level'] = $level;
2638 $tokens[$i]['conditions'] = $conditions;
2639 }//end if
2640 }//end foreach
2641 }//end if
2642 }//end if
2643 }//end for
2644
2645 if (PHP_CODESNIFFER_VERBOSITY > 1) {
2646 echo "\t*** END LEVEL MAP ***".PHP_EOL;
2647 }
2648
2649 }//end _createLevelMap()
2650
2651
2652 /**
2653 * Returns the declaration names for classes, interfaces, and functions.
2654 *
2655 * @param int $stackPtr The position of the declaration token which
2656 * declared the class, interface or function.
2657 *
2658 * @return string|null The name of the class, interface or function.
2659 * or NULL if the function or class is anonymous.
2660 * @throws PHP_CodeSniffer_Exception If the specified token is not of type
2661 * T_FUNCTION, T_CLASS, T_ANON_CLASS,
2662 * or T_INTERFACE.
2663 */
2664 public function getDeclarationName($stackPtr)
2665 {
2666 $tokenCode = $this->_tokens[$stackPtr]['code'];
2667
2668 if ($tokenCode === T_ANON_CLASS) {
2669 return null;
2670 }
2671
2672 if ($tokenCode === T_FUNCTION
2673 && $this->isAnonymousFunction($stackPtr) === true
2674 ) {
2675 return null;
2676 }
2677
2678 if ($tokenCode !== T_FUNCTION
2679 && $tokenCode !== T_CLASS
2680 && $tokenCode !== T_INTERFACE
2681 && $tokenCode !== T_TRAIT
2682 ) {
2683 throw new PHP_CodeSniffer_Exception('Token type "'.$this->_tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT');
2684 }
2685
2686 $content = null;
2687 for ($i = $stackPtr; $i < $this->numTokens; $i++) {
2688 if ($this->_tokens[$i]['code'] === T_STRING) {
2689 $content = $this->_tokens[$i]['content'];
2690 break;
2691 }
2692 }
2693
2694 return $content;
2695
2696 }//end getDeclarationName()
2697
2698
2699 /**
2700 * Check if the token at the specified position is a anonymous function.
2701 *
2702 * @param int $stackPtr The position of the declaration token which
2703 * declared the class, interface or function.
2704 *
2705 * @return boolean
2706 * @throws PHP_CodeSniffer_Exception If the specified token is not of type
2707 * T_FUNCTION
2708 */
2709 public function isAnonymousFunction($stackPtr)
2710 {
2711 $tokenCode = $this->_tokens[$stackPtr]['code'];
2712 if ($tokenCode !== T_FUNCTION) {
2713 throw new PHP_CodeSniffer_Exception('Token type is not T_FUNCTION');
2714 }
2715
2716 if (isset($this->_tokens[$stackPtr]['parenthesis_opener']) === false) {
2717 // Something is not right with this function.
2718 return false;
2719 }
2720
2721 $name = false;
2722 for ($i = ($stackPtr + 1); $i < $this->numTokens; $i++) {
2723 if ($this->_tokens[$i]['code'] === T_STRING) {
2724 $name = $i;
2725 break;
2726 }
2727 }
2728
2729 if ($name === false) {
2730 // No name found.
2731 return true;
2732 }
2733
2734 $open = $this->_tokens[$stackPtr]['parenthesis_opener'];
2735 if ($name > $open) {
2736 return true;
2737 }
2738
2739 return false;
2740
2741 }//end isAnonymousFunction()
2742
2743
2744 /**
2745 * Returns the method parameters for the specified function token.
2746 *
2747 * Each parameter is in the following format:
2748 *
2749 * <code>
2750 * 0 => array(
2751 * 'token' => int, // The position of the var in the token stack.
2752 * 'name' => '$var', // The variable name.
2753 * 'content' => string, // The full content of the variable definition.
2754 * 'pass_by_reference' => boolean, // Is the variable passed by reference?
2755 * 'type_hint' => string, // The type hint for the variable.
2756 * 'nullable_type' => boolean, // Is the variable using a nullable type?
2757 * )
2758 * </code>
2759 *
2760 * Parameters with default values have an additional array index of
2761 * 'default' with the value of the default as a string.
2762 *
2763 * @param int $stackPtr The position in the stack of the function token
2764 * to acquire the parameters for.
2765 *
2766 * @return array
2767 * @throws PHP_CodeSniffer_Exception If the specified $stackPtr is not of
2768 * type T_FUNCTION or T_CLOSURE.
2769 */
2770 public function getMethodParameters($stackPtr)
2771 {
2772 if ($this->_tokens[$stackPtr]['code'] !== T_FUNCTION
2773 && $this->_tokens[$stackPtr]['code'] !== T_CLOSURE
2774 ) {
2775 throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_FUNCTION or T_CLOSURE');
2776 }
2777
2778 $opener = $this->_tokens[$stackPtr]['parenthesis_opener'];
2779 $closer = $this->_tokens[$stackPtr]['parenthesis_closer'];
2780
2781 $vars = array();
2782 $currVar = null;
2783 $paramStart = ($opener + 1);
2784 $defaultStart = null;
2785 $paramCount = 0;
2786 $passByReference = false;
2787 $variableLength = false;
2788 $typeHint = '';
2789 $nullableType = false;
2790
2791 for ($i = $paramStart; $i <= $closer; $i++) {
2792 // Check to see if this token has a parenthesis or bracket opener. If it does
2793 // it's likely to be an array which might have arguments in it. This
2794 // could cause problems in our parsing below, so lets just skip to the
2795 // end of it.
2796 if (isset($this->_tokens[$i]['parenthesis_opener']) === true) {
2797 // Don't do this if it's the close parenthesis for the method.
2798 if ($i !== $this->_tokens[$i]['parenthesis_closer']) {
2799 $i = ($this->_tokens[$i]['parenthesis_closer'] + 1);
2800 }
2801 }
2802
2803 if (isset($this->_tokens[$i]['bracket_opener']) === true) {
2804 // Don't do this if it's the close parenthesis for the method.
2805 if ($i !== $this->_tokens[$i]['bracket_closer']) {
2806 $i = ($this->_tokens[$i]['bracket_closer'] + 1);
2807 }
2808 }
2809
2810 switch ($this->_tokens[$i]['code']) {
2811 case T_BITWISE_AND:
2812 $passByReference = true;
2813 break;
2814 case T_VARIABLE:
2815 $currVar = $i;
2816 break;
2817 case T_ELLIPSIS:
2818 $variableLength = true;
2819 break;
2820 case T_ARRAY_HINT:
2821 case T_CALLABLE:
2822 $typeHint .= $this->_tokens[$i]['content'];
2823 break;
2824 case T_SELF:
2825 case T_PARENT:
2826 case T_STATIC:
2827 // Self is valid, the others invalid, but were probably intended as type hints.
2828 if (isset($defaultStart) === false) {
2829 $typeHint .= $this->_tokens[$i]['content'];
2830 }
2831 break;
2832 case T_STRING:
2833 // This is a string, so it may be a type hint, but it could
2834 // also be a constant used as a default value.
2835 $prevComma = false;
2836 for ($t = $i; $t >= $opener; $t--) {
2837 if ($this->_tokens[$t]['code'] === T_COMMA) {
2838 $prevComma = $t;
2839 break;
2840 }
2841 }
2842
2843 if ($prevComma !== false) {
2844 $nextEquals = false;
2845 for ($t = $prevComma; $t < $i; $t++) {
2846 if ($this->_tokens[$t]['code'] === T_EQUAL) {
2847 $nextEquals = $t;
2848 break;
2849 }
2850 }
2851
2852 if ($nextEquals !== false) {
2853 break;
2854 }
2855 }
2856
2857 if ($defaultStart === null) {
2858 $typeHint .= $this->_tokens[$i]['content'];
2859 }
2860 break;
2861 case T_NS_SEPARATOR:
2862 // Part of a type hint or default value.
2863 if ($defaultStart === null) {
2864 $typeHint .= $this->_tokens[$i]['content'];
2865 }
2866 break;
2867 case T_NULLABLE:
2868 if ($defaultStart === null) {
2869 $nullableType = true;
2870 $typeHint .= $this->_tokens[$i]['content'];
2871 }
2872 break;
2873 case T_CLOSE_PARENTHESIS:
2874 case T_COMMA:
2875 // If it's null, then there must be no parameters for this
2876 // method.
2877 if ($currVar === null) {
2878 continue;
2879 }
2880
2881 $vars[$paramCount] = array();
2882 $vars[$paramCount]['token'] = $currVar;
2883 $vars[$paramCount]['name'] = $this->_tokens[$currVar]['content'];
2884 $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
2885
2886 if ($defaultStart !== null) {
2887 $vars[$paramCount]['default'] = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
2888 }
2889
2890 $vars[$paramCount]['pass_by_reference'] = $passByReference;
2891 $vars[$paramCount]['variable_length'] = $variableLength;
2892 $vars[$paramCount]['type_hint'] = $typeHint;
2893 $vars[$paramCount]['nullable_type'] = $nullableType;
2894
2895 // Reset the vars, as we are about to process the next parameter.
2896 $defaultStart = null;
2897 $paramStart = ($i + 1);
2898 $passByReference = false;
2899 $variableLength = false;
2900 $typeHint = '';
2901 $nullableType = false;
2902
2903 $paramCount++;
2904 break;
2905 case T_EQUAL:
2906 $defaultStart = ($i + 1);
2907 break;
2908 }//end switch
2909 }//end for
2910
2911 return $vars;
2912
2913 }//end getMethodParameters()
2914
2915
2916 /**
2917 * Returns the visibility and implementation properties of a method.
2918 *
2919 * The format of the array is:
2920 * <code>
2921 * array(
2922 * 'scope' => 'public', // public private or protected
2923 * 'scope_specified' => true, // true is scope keyword was found.
2924 * 'is_abstract' => false, // true if the abstract keyword was found.
2925 * 'is_final' => false, // true if the final keyword was found.
2926 * 'is_static' => false, // true if the static keyword was found.
2927 * 'is_closure' => false, // true if no name is found.
2928 * );
2929 * </code>
2930 *
2931 * @param int $stackPtr The position in the stack of the T_FUNCTION token to
2932 * acquire the properties for.
2933 *
2934 * @return array
2935 * @throws PHP_CodeSniffer_Exception If the specified position is not a
2936 * T_FUNCTION token.
2937 */
2938 public function getMethodProperties($stackPtr)
2939 {
2940 if ($this->_tokens[$stackPtr]['code'] !== T_FUNCTION) {
2941 throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_FUNCTION');
2942 }
2943
2944 $valid = array(
2945 T_PUBLIC => T_PUBLIC,
2946 T_PRIVATE => T_PRIVATE,
2947 T_PROTECTED => T_PROTECTED,
2948 T_STATIC => T_STATIC,
2949 T_FINAL => T_FINAL,
2950 T_ABSTRACT => T_ABSTRACT,
2951 T_WHITESPACE => T_WHITESPACE,
2952 T_COMMENT => T_COMMENT,
2953 T_DOC_COMMENT => T_DOC_COMMENT,
2954 );
2955
2956 $scope = 'public';
2957 $scopeSpecified = false;
2958 $isAbstract = false;
2959 $isFinal = false;
2960 $isStatic = false;
2961 $isClosure = $this->isAnonymousFunction($stackPtr);
2962
2963 for ($i = ($stackPtr - 1); $i > 0; $i--) {
2964 if (isset($valid[$this->_tokens[$i]['code']]) === false) {
2965 break;
2966 }
2967
2968 switch ($this->_tokens[$i]['code']) {
2969 case T_PUBLIC:
2970 $scope = 'public';
2971 $scopeSpecified = true;
2972 break;
2973 case T_PRIVATE:
2974 $scope = 'private';
2975 $scopeSpecified = true;
2976 break;
2977 case T_PROTECTED:
2978 $scope = 'protected';
2979 $scopeSpecified = true;
2980 break;
2981 case T_ABSTRACT:
2982 $isAbstract = true;
2983 break;
2984 case T_FINAL:
2985 $isFinal = true;
2986 break;
2987 case T_STATIC:
2988 $isStatic = true;
2989 break;
2990 }//end switch
2991 }//end for
2992
2993 return array(
2994 'scope' => $scope,
2995 'scope_specified' => $scopeSpecified,
2996 'is_abstract' => $isAbstract,
2997 'is_final' => $isFinal,
2998 'is_static' => $isStatic,
2999 'is_closure' => $isClosure,
3000 );
3001
3002 }//end getMethodProperties()
3003
3004
3005 /**
3006 * Returns the visibility and implementation properties of the class member
3007 * variable found at the specified position in the stack.
3008 *
3009 * The format of the array is:
3010 *
3011 * <code>
3012 * array(
3013 * 'scope' => 'public', // public private or protected
3014 * 'is_static' => false, // true if the static keyword was found.
3015 * );
3016 * </code>
3017 *
3018 * @param int $stackPtr The position in the stack of the T_VARIABLE token to
3019 * acquire the properties for.
3020 *
3021 * @return array
3022 * @throws PHP_CodeSniffer_Exception If the specified position is not a
3023 * T_VARIABLE token, or if the position is not
3024 * a class member variable.
3025 */
3026 public function getMemberProperties($stackPtr)
3027 {
3028 if ($this->_tokens[$stackPtr]['code'] !== T_VARIABLE) {
3029 throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_VARIABLE');
3030 }
3031
3032 $conditions = array_keys($this->_tokens[$stackPtr]['conditions']);
3033 $ptr = array_pop($conditions);
3034 if (isset($this->_tokens[$ptr]) === false
3035 || ($this->_tokens[$ptr]['code'] !== T_CLASS
3036 && $this->_tokens[$ptr]['code'] !== T_ANON_CLASS
3037 && $this->_tokens[$ptr]['code'] !== T_TRAIT)
3038 ) {
3039 if (isset($this->_tokens[$ptr]) === true
3040 && $this->_tokens[$ptr]['code'] === T_INTERFACE
3041 ) {
3042 // T_VARIABLEs in interfaces can actually be method arguments
3043 // but they wont be seen as being inside the method because there
3044 // are no scope openers and closers for abstract methods. If it is in
3045 // parentheses, we can be pretty sure it is a method argument.
3046 if (isset($this->_tokens[$stackPtr]['nested_parenthesis']) === false
3047 || empty($this->_tokens[$stackPtr]['nested_parenthesis']) === true
3048 ) {
3049 $error = 'Possible parse error: interfaces may not include member vars';
3050 $this->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar');
3051 return array();
3052 }
3053 } else {
3054 throw new PHP_CodeSniffer_Exception('$stackPtr is not a class member var');
3055 }
3056 }
3057
3058 $valid = array(
3059 T_PUBLIC => T_PUBLIC,
3060 T_PRIVATE => T_PRIVATE,
3061 T_PROTECTED => T_PROTECTED,
3062 T_STATIC => T_STATIC,
3063 T_WHITESPACE => T_WHITESPACE,
3064 T_COMMENT => T_COMMENT,
3065 T_DOC_COMMENT => T_DOC_COMMENT,
3066 T_VARIABLE => T_VARIABLE,
3067 T_COMMA => T_COMMA,
3068 );
3069
3070 $scope = 'public';
3071 $scopeSpecified = false;
3072 $isStatic = false;
3073
3074 for ($i = ($stackPtr - 1); $i > 0; $i--) {
3075 if (isset($valid[$this->_tokens[$i]['code']]) === false) {
3076 break;
3077 }
3078
3079 switch ($this->_tokens[$i]['code']) {
3080 case T_PUBLIC:
3081 $scope = 'public';
3082 $scopeSpecified = true;
3083 break;
3084 case T_PRIVATE:
3085 $scope = 'private';
3086 $scopeSpecified = true;
3087 break;
3088 case T_PROTECTED:
3089 $scope = 'protected';
3090 $scopeSpecified = true;
3091 break;
3092 case T_STATIC:
3093 $isStatic = true;
3094 break;
3095 }
3096 }//end for
3097
3098 return array(
3099 'scope' => $scope,
3100 'scope_specified' => $scopeSpecified,
3101 'is_static' => $isStatic,
3102 );
3103
3104 }//end getMemberProperties()
3105
3106
3107 /**
3108 * Returns the visibility and implementation properties of a class.
3109 *
3110 * The format of the array is:
3111 * <code>
3112 * array(
3113 * 'is_abstract' => false, // true if the abstract keyword was found.
3114 * 'is_final' => false, // true if the final keyword was found.
3115 * );
3116 * </code>
3117 *
3118 * @param int $stackPtr The position in the stack of the T_CLASS token to
3119 * acquire the properties for.
3120 *
3121 * @return array
3122 * @throws PHP_CodeSniffer_Exception If the specified position is not a
3123 * T_CLASS token.
3124 */
3125 public function getClassProperties($stackPtr)
3126 {
3127 if ($this->_tokens[$stackPtr]['code'] !== T_CLASS) {
3128 throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_CLASS');
3129 }
3130
3131 $valid = array(
3132 T_FINAL => T_FINAL,
3133 T_ABSTRACT => T_ABSTRACT,
3134 T_WHITESPACE => T_WHITESPACE,
3135 T_COMMENT => T_COMMENT,
3136 T_DOC_COMMENT => T_DOC_COMMENT,
3137 );
3138
3139 $isAbstract = false;
3140 $isFinal = false;
3141
3142 for ($i = ($stackPtr - 1); $i > 0; $i--) {
3143 if (isset($valid[$this->_tokens[$i]['code']]) === false) {
3144 break;
3145 }
3146
3147 switch ($this->_tokens[$i]['code']) {
3148 case T_ABSTRACT:
3149 $isAbstract = true;
3150 break;
3151
3152 case T_FINAL:
3153 $isFinal = true;
3154 break;
3155 }
3156 }//end for
3157
3158 return array(
3159 'is_abstract' => $isAbstract,
3160 'is_final' => $isFinal,
3161 );
3162
3163 }//end getClassProperties()
3164
3165
3166 /**
3167 * Determine if the passed token is a reference operator.
3168 *
3169 * Returns true if the specified token position represents a reference.
3170 * Returns false if the token represents a bitwise operator.
3171 *
3172 * @param int $stackPtr The position of the T_BITWISE_AND token.
3173 *
3174 * @return boolean
3175 */
3176 public function isReference($stackPtr)
3177 {
3178 if ($this->_tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
3179 return false;
3180 }
3181
3182 $tokenBefore = $this->findPrevious(
3183 PHP_CodeSniffer_Tokens::$emptyTokens,
3184 ($stackPtr - 1),
3185 null,
3186 true
3187 );
3188
3189 if ($this->_tokens[$tokenBefore]['code'] === T_FUNCTION) {
3190 // Function returns a reference.
3191 return true;
3192 }
3193
3194 if ($this->_tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
3195 // Inside a foreach loop, this is a reference.
3196 return true;
3197 }
3198
3199 if ($this->_tokens[$tokenBefore]['code'] === T_AS) {
3200 // Inside a foreach loop, this is a reference.
3201 return true;
3202 }
3203
3204 if ($this->_tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY) {
3205 // Inside an array declaration, this is a reference.
3206 return true;
3207 }
3208
3209 if (isset(PHP_CodeSniffer_Tokens::$assignmentTokens[$this->_tokens[$tokenBefore]['code']]) === true) {
3210 // This is directly after an assignment. It's a reference. Even if
3211 // it is part of an operation, the other tests will handle it.
3212 return true;
3213 }
3214
3215 if (isset($this->_tokens[$stackPtr]['nested_parenthesis']) === true) {
3216 $brackets = $this->_tokens[$stackPtr]['nested_parenthesis'];
3217 $lastBracket = array_pop($brackets);
3218 if (isset($this->_tokens[$lastBracket]['parenthesis_owner']) === true) {
3219 $owner = $this->_tokens[$this->_tokens[$lastBracket]['parenthesis_owner']];
3220 if ($owner['code'] === T_FUNCTION
3221 || $owner['code'] === T_CLOSURE
3222 || $owner['code'] === T_ARRAY
3223 ) {
3224 // Inside a function or array declaration, this is a reference.
3225 return true;
3226 }
3227 } else {
3228 $prev = false;
3229 for ($t = ($this->_tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
3230 if ($this->_tokens[$t]['code'] !== T_WHITESPACE) {
3231 $prev = $t;
3232 break;
3233 }
3234 }
3235
3236 if ($prev !== false && $this->_tokens[$prev]['code'] === T_USE) {
3237 return true;
3238 }
3239 }//end if
3240 }//end if
3241
3242 $tokenAfter = $this->findNext(
3243 PHP_CodeSniffer_Tokens::$emptyTokens,
3244 ($stackPtr + 1),
3245 null,
3246 true
3247 );
3248
3249 if ($this->_tokens[$tokenAfter]['code'] === T_VARIABLE
3250 && ($this->_tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
3251 || $this->_tokens[$tokenBefore]['code'] === T_COMMA)
3252 ) {
3253 return true;
3254 }
3255
3256 return false;
3257
3258 }//end isReference()
3259
3260
3261 /**
3262 * Returns the content of the tokens from the specified start position in
3263 * the token stack for the specified length.
3264 *
3265 * @param int $start The position to start from in the token stack.
3266 * @param int $length The length of tokens to traverse from the start pos.
3267 *
3268 * @return string The token contents.
3269 */
3270 public function getTokensAsString($start, $length)
3271 {
3272 $str = '';
3273 $end = ($start + $length);
3274 if ($end > $this->numTokens) {
3275 $end = $this->numTokens;
3276 }
3277
3278 for ($i = $start; $i < $end; $i++) {
3279 $str .= $this->_tokens[$i]['content'];
3280 }
3281
3282 return $str;
3283
3284 }//end getTokensAsString()
3285
3286
3287 /**
3288 * Returns the position of the previous specified token(s).
3289 *
3290 * If a value is specified, the previous token of the specified type(s)
3291 * containing the specified value will be returned.
3292 *
3293 * Returns false if no token can be found.
3294 *
3295 * @param int|array $types The type(s) of tokens to search for.
3296 * @param int $start The position to start searching from in the
3297 * token stack.
3298 * @param int $end The end position to fail if no token is found.
3299 * if not specified or null, end will default to
3300 * the start of the token stack.
3301 * @param bool $exclude If true, find the previous token that are NOT of
3302 * the types specified in $types.
3303 * @param string $value The value that the token(s) must be equal to.
3304 * If value is omitted, tokens with any value will
3305 * be returned.
3306 * @param bool $local If true, tokens outside the current statement
3307 * will not be checked. IE. checking will stop
3308 * at the previous semi-colon found.
3309 *
3310 * @return int|bool
3311 * @see findNext()
3312 */
3313 public function findPrevious(
3314 $types,
3315 $start,
3316 $end=null,
3317 $exclude=false,
3318 $value=null,
3319 $local=false
3320 ) {
3321 $types = (array) $types;
3322
3323 if ($end === null) {
3324 $end = 0;
3325 }
3326
3327 for ($i = $start; $i >= $end; $i--) {
3328 $found = (bool) $exclude;
3329 foreach ($types as $type) {
3330 if ($this->_tokens[$i]['code'] === $type) {
3331 $found = !$exclude;
3332 break;
3333 }
3334 }
3335
3336 if ($found === true) {
3337 if ($value === null) {
3338 return $i;
3339 } else if ($this->_tokens[$i]['content'] === $value) {
3340 return $i;
3341 }
3342 }
3343
3344 if ($local === true) {
3345 if (isset($this->_tokens[$i]['scope_opener']) === true
3346 && $i === $this->_tokens[$i]['scope_closer']
3347 ) {
3348 $i = $this->_tokens[$i]['scope_opener'];
3349 } else if (isset($this->_tokens[$i]['bracket_opener']) === true
3350 && $i === $this->_tokens[$i]['bracket_closer']
3351 ) {
3352 $i = $this->_tokens[$i]['bracket_opener'];
3353 } else if (isset($this->_tokens[$i]['parenthesis_opener']) === true
3354 && $i === $this->_tokens[$i]['parenthesis_closer']
3355 ) {
3356 $i = $this->_tokens[$i]['parenthesis_opener'];
3357 } else if ($this->_tokens[$i]['code'] === T_SEMICOLON) {
3358 break;
3359 }
3360 }
3361 }//end for
3362
3363 return false;
3364
3365 }//end findPrevious()
3366
3367
3368 /**
3369 * Returns the position of the next specified token(s).
3370 *
3371 * If a value is specified, the next token of the specified type(s)
3372 * containing the specified value will be returned.
3373 *
3374 * Returns false if no token can be found.
3375 *
3376 * @param int|array $types The type(s) of tokens to search for.
3377 * @param int $start The position to start searching from in the
3378 * token stack.
3379 * @param int $end The end position to fail if no token is found.
3380 * if not specified or null, end will default to
3381 * the end of the token stack.
3382 * @param bool $exclude If true, find the next token that is NOT of
3383 * a type specified in $types.
3384 * @param string $value The value that the token(s) must be equal to.
3385 * If value is omitted, tokens with any value will
3386 * be returned.
3387 * @param bool $local If true, tokens outside the current statement
3388 * will not be checked. i.e., checking will stop
3389 * at the next semi-colon found.
3390 *
3391 * @return int|bool
3392 * @see findPrevious()
3393 */
3394 public function findNext(
3395 $types,
3396 $start,
3397 $end=null,
3398 $exclude=false,
3399 $value=null,
3400 $local=false
3401 ) {
3402 $types = (array) $types;
3403
3404 if ($end === null || $end > $this->numTokens) {
3405 $end = $this->numTokens;
3406 }
3407
3408 for ($i = $start; $i < $end; $i++) {
3409 $found = (bool) $exclude;
3410 foreach ($types as $type) {
3411 if ($this->_tokens[$i]['code'] === $type) {
3412 $found = !$exclude;
3413 break;
3414 }
3415 }
3416
3417 if ($found === true) {
3418 if ($value === null) {
3419 return $i;
3420 } else if ($this->_tokens[$i]['content'] === $value) {
3421 return $i;
3422 }
3423 }
3424
3425 if ($local === true && $this->_tokens[$i]['code'] === T_SEMICOLON) {
3426 break;
3427 }
3428 }//end for
3429
3430 return false;
3431
3432 }//end findNext()
3433
3434
3435 /**
3436 * Returns the position of the first non-whitespace token in a statement.
3437 *
3438 * @param int $start The position to start searching from in the token stack.
3439 * @param int|array $ignore Token types that should not be considered stop points.
3440 *
3441 * @return int
3442 */
3443 public function findStartOfStatement($start, $ignore=null)
3444 {
3445 $endTokens = PHP_CodeSniffer_Tokens::$blockOpeners;
3446
3447 $endTokens[T_COLON] = true;
3448 $endTokens[T_COMMA] = true;
3449 $endTokens[T_DOUBLE_ARROW] = true;
3450 $endTokens[T_SEMICOLON] = true;
3451 $endTokens[T_OPEN_TAG] = true;
3452 $endTokens[T_CLOSE_TAG] = true;
3453 $endTokens[T_OPEN_SHORT_ARRAY] = true;
3454
3455 if ($ignore !== null) {
3456 $ignore = (array) $ignore;
3457 foreach ($ignore as $code) {
3458 if (isset($endTokens[$code]) === true) {
3459 unset($endTokens[$code]);
3460 }
3461 }
3462 }
3463
3464 $lastNotEmpty = $start;
3465
3466 for ($i = $start; $i >= 0; $i--) {
3467 if (isset($endTokens[$this->_tokens[$i]['code']]) === true) {
3468 // Found the end of the previous statement.
3469 return $lastNotEmpty;
3470 }
3471
3472 if (isset($this->_tokens[$i]['scope_opener']) === true
3473 && $i === $this->_tokens[$i]['scope_closer']
3474 ) {
3475 // Found the end of the previous scope block.
3476 return $lastNotEmpty;
3477 }
3478
3479 // Skip nested statements.
3480 if (isset($this->_tokens[$i]['bracket_opener']) === true
3481 && $i === $this->_tokens[$i]['bracket_closer']
3482 ) {
3483 $i = $this->_tokens[$i]['bracket_opener'];
3484 } else if (isset($this->_tokens[$i]['parenthesis_opener']) === true
3485 && $i === $this->_tokens[$i]['parenthesis_closer']
3486 ) {
3487 $i = $this->_tokens[$i]['parenthesis_opener'];
3488 }
3489
3490 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$this->_tokens[$i]['code']]) === false) {
3491 $lastNotEmpty = $i;
3492 }
3493 }//end for
3494
3495 return 0;
3496
3497 }//end findStartOfStatement()
3498
3499
3500 /**
3501 * Returns the position of the last non-whitespace token in a statement.
3502 *
3503 * @param int $start The position to start searching from in the token stack.
3504 * @param int|array $ignore Token types that should not be considered stop points.
3505 *
3506 * @return int
3507 */
3508 public function findEndOfStatement($start, $ignore=null)
3509 {
3510 $endTokens = array(
3511 T_COLON => true,
3512 T_COMMA => true,
3513 T_DOUBLE_ARROW => true,
3514 T_SEMICOLON => true,
3515 T_CLOSE_PARENTHESIS => true,
3516 T_CLOSE_SQUARE_BRACKET => true,
3517 T_CLOSE_CURLY_BRACKET => true,
3518 T_CLOSE_SHORT_ARRAY => true,
3519 T_OPEN_TAG => true,
3520 T_CLOSE_TAG => true,
3521 );
3522
3523 if ($ignore !== null) {
3524 $ignore = (array) $ignore;
3525 foreach ($ignore as $code) {
3526 if (isset($endTokens[$code]) === true) {
3527 unset($endTokens[$code]);
3528 }
3529 }
3530 }
3531
3532 $lastNotEmpty = $start;
3533
3534 for ($i = $start; $i < $this->numTokens; $i++) {
3535 if ($i !== $start && isset($endTokens[$this->_tokens[$i]['code']]) === true) {
3536 // Found the end of the statement.
3537 if ($this->_tokens[$i]['code'] === T_CLOSE_PARENTHESIS
3538 || $this->_tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
3539 || $this->_tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
3540 || $this->_tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
3541 || $this->_tokens[$i]['code'] === T_OPEN_TAG
3542 || $this->_tokens[$i]['code'] === T_CLOSE_TAG
3543 ) {
3544 return $lastNotEmpty;
3545 }
3546
3547 return $i;
3548 }
3549
3550 // Skip nested statements.
3551 if (isset($this->_tokens[$i]['scope_closer']) === true
3552 && ($i === $this->_tokens[$i]['scope_opener']
3553 || $i === $this->_tokens[$i]['scope_condition'])
3554 ) {
3555 $i = $this->_tokens[$i]['scope_closer'];
3556 } else if (isset($this->_tokens[$i]['bracket_closer']) === true
3557 && $i === $this->_tokens[$i]['bracket_opener']
3558 ) {
3559 $i = $this->_tokens[$i]['bracket_closer'];
3560 } else if (isset($this->_tokens[$i]['parenthesis_closer']) === true
3561 && $i === $this->_tokens[$i]['parenthesis_opener']
3562 ) {
3563 $i = $this->_tokens[$i]['parenthesis_closer'];
3564 }
3565
3566 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$this->_tokens[$i]['code']]) === false) {
3567 $lastNotEmpty = $i;
3568 }
3569 }//end for
3570
3571 return ($this->numTokens - 1);
3572
3573 }//end findEndOfStatement()
3574
3575
3576 /**
3577 * Returns the position of the first token on a line, matching given type.
3578 *
3579 * Returns false if no token can be found.
3580 *
3581 * @param int|array $types The type(s) of tokens to search for.
3582 * @param int $start The position to start searching from in the
3583 * token stack. The first token matching on
3584 * this line before this token will be returned.
3585 * @param bool $exclude If true, find the token that is NOT of
3586 * the types specified in $types.
3587 * @param string $value The value that the token must be equal to.
3588 * If value is omitted, tokens with any value will
3589 * be returned.
3590 *
3591 * @return int | bool
3592 */
3593 public function findFirstOnLine($types, $start, $exclude=false, $value=null)
3594 {
3595 if (is_array($types) === false) {
3596 $types = array($types);
3597 }
3598
3599 $foundToken = false;
3600
3601 for ($i = $start; $i >= 0; $i--) {
3602 if ($this->_tokens[$i]['line'] < $this->_tokens[$start]['line']) {
3603 break;
3604 }
3605
3606 $found = $exclude;
3607 foreach ($types as $type) {
3608 if ($exclude === false) {
3609 if ($this->_tokens[$i]['code'] === $type) {
3610 $found = true;
3611 break;
3612 }
3613 } else {
3614 if ($this->_tokens[$i]['code'] === $type) {
3615 $found = false;
3616 break;
3617 }
3618 }
3619 }
3620
3621 if ($found === true) {
3622 if ($value === null) {
3623 $foundToken = $i;
3624 } else if ($this->_tokens[$i]['content'] === $value) {
3625 $foundToken = $i;
3626 }
3627 }
3628 }//end for
3629
3630 return $foundToken;
3631
3632 }//end findFirstOnLine()
3633
3634
3635 /**
3636 * Determine if the passed token has a condition of one of the passed types.
3637 *
3638 * @param int $stackPtr The position of the token we are checking.
3639 * @param int|array $types The type(s) of tokens to search for.
3640 *
3641 * @return boolean
3642 */
3643 public function hasCondition($stackPtr, $types)
3644 {
3645 // Check for the existence of the token.
3646 if (isset($this->_tokens[$stackPtr]) === false) {
3647 return false;
3648 }
3649
3650 // Make sure the token has conditions.
3651 if (isset($this->_tokens[$stackPtr]['conditions']) === false) {
3652 return false;
3653 }
3654
3655 $types = (array) $types;
3656 $conditions = $this->_tokens[$stackPtr]['conditions'];
3657
3658 foreach ($types as $type) {
3659 if (in_array($type, $conditions) === true) {
3660 // We found a token with the required type.
3661 return true;
3662 }
3663 }
3664
3665 return false;
3666
3667 }//end hasCondition()
3668
3669
3670 /**
3671 * Return the position of the condition for the passed token.
3672 *
3673 * Returns FALSE if the token does not have the condition.
3674 *
3675 * @param int $stackPtr The position of the token we are checking.
3676 * @param int $type The type of token to search for.
3677 *
3678 * @return int
3679 */
3680 public function getCondition($stackPtr, $type)
3681 {
3682 // Check for the existence of the token.
3683 if (isset($this->_tokens[$stackPtr]) === false) {
3684 return false;
3685 }
3686
3687 // Make sure the token has conditions.
3688 if (isset($this->_tokens[$stackPtr]['conditions']) === false) {
3689 return false;
3690 }
3691
3692 $conditions = $this->_tokens[$stackPtr]['conditions'];
3693 foreach ($conditions as $token => $condition) {
3694 if ($condition === $type) {
3695 return $token;
3696 }
3697 }
3698
3699 return false;
3700
3701 }//end getCondition()
3702
3703
3704 /**
3705 * Returns the name of the class that the specified class extends.
3706 *
3707 * Returns FALSE on error or if there is no extended class name.
3708 *
3709 * @param int $stackPtr The stack position of the class.
3710 *
3711 * @return string
3712 */
3713 public function findExtendedClassName($stackPtr)
3714 {
3715 // Check for the existence of the token.
3716 if (isset($this->_tokens[$stackPtr]) === false) {
3717 return false;
3718 }
3719
3720 if ($this->_tokens[$stackPtr]['code'] !== T_CLASS
3721 && $this->_tokens[$stackPtr]['code'] !== T_ANON_CLASS
3722 ) {
3723 return false;
3724 }
3725
3726 if (isset($this->_tokens[$stackPtr]['scope_closer']) === false) {
3727 return false;
3728 }
3729
3730 $classCloserIndex = $this->_tokens[$stackPtr]['scope_closer'];
3731 $extendsIndex = $this->findNext(T_EXTENDS, $stackPtr, $classCloserIndex);
3732 if (false === $extendsIndex) {
3733 return false;
3734 }
3735
3736 $find = array(
3737 T_NS_SEPARATOR,
3738 T_STRING,
3739 T_WHITESPACE,
3740 );
3741
3742 $end = $this->findNext($find, ($extendsIndex + 1), $classCloserIndex, true);
3743 $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
3744 $name = trim($name);
3745
3746 if ($name === '') {
3747 return false;
3748 }
3749
3750 return $name;
3751
3752 }//end findExtendedClassName()
3753
3754
3755 /**
3756 * Returns the name(s) of the interface(s) that the specified class implements.
3757 *
3758 * Returns FALSE on error or if there are no implemented interface names.
3759 *
3760 * @param int $stackPtr The stack position of the class.
3761 *
3762 * @return array|false
3763 */
3764 public function findImplementedInterfaceNames($stackPtr)
3765 {
3766 // Check for the existence of the token.
3767 if (isset($this->_tokens[$stackPtr]) === false) {
3768 return false;
3769 }
3770
3771 if ($this->_tokens[$stackPtr]['code'] !== T_CLASS
3772 && $this->_tokens[$stackPtr]['code'] !== T_ANON_CLASS
3773 ) {
3774 return false;
3775 }
3776
3777 if (isset($this->_tokens[$stackPtr]['scope_closer']) === false) {
3778 return false;
3779 }
3780
3781 $classOpenerIndex = $this->_tokens[$stackPtr]['scope_opener'];
3782 $implementsIndex = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
3783 if ($implementsIndex === false) {
3784 return false;
3785 }
3786
3787 $find = array(
3788 T_NS_SEPARATOR,
3789 T_STRING,
3790 T_WHITESPACE,
3791 T_COMMA,
3792 );
3793
3794 $end = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
3795 $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
3796 $name = trim($name);
3797
3798 if ($name === '') {
3799 return false;
3800 } else {
3801 $names = explode(',', $name);
3802 $names = array_map('trim', $names);
3803 return $names;
3804 }
3805
3806 }//end findImplementedInterfaceNames()
3807
3808
3809 }//end class