comparison vendor/squizlabs/php_codesniffer/src/Files/File.php @ 17:129ea1e6d783

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