annotate vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/AbstractPatternSniff.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 7a779792577d
rev   line source
Chris@0 1 <?php
Chris@0 2 /**
Chris@0 3 * Processes pattern strings and checks that the code conforms to the pattern.
Chris@0 4 *
Chris@0 5 * PHP version 5
Chris@0 6 *
Chris@0 7 * @category PHP
Chris@0 8 * @package PHP_CodeSniffer
Chris@0 9 * @author Greg Sherwood <gsherwood@squiz.net>
Chris@0 10 * @author Marc McIntyre <mmcintyre@squiz.net>
Chris@0 11 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
Chris@0 12 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
Chris@0 13 * @link http://pear.php.net/package/PHP_CodeSniffer
Chris@0 14 */
Chris@0 15
Chris@0 16 if (class_exists('PHP_CodeSniffer_Standards_IncorrectPatternException', true) === false) {
Chris@0 17 $error = 'Class PHP_CodeSniffer_Standards_IncorrectPatternException not found';
Chris@0 18 throw new PHP_CodeSniffer_Exception($error);
Chris@0 19 }
Chris@0 20
Chris@0 21 /**
Chris@0 22 * Processes pattern strings and checks that the code conforms to the pattern.
Chris@0 23 *
Chris@0 24 * This test essentially checks that code is correctly formatted with whitespace.
Chris@0 25 *
Chris@0 26 * @category PHP
Chris@0 27 * @package PHP_CodeSniffer
Chris@0 28 * @author Greg Sherwood <gsherwood@squiz.net>
Chris@0 29 * @author Marc McIntyre <mmcintyre@squiz.net>
Chris@0 30 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
Chris@0 31 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
Chris@0 32 * @version Release: @package_version@
Chris@0 33 * @link http://pear.php.net/package/PHP_CodeSniffer
Chris@0 34 */
Chris@0 35 abstract class PHP_CodeSniffer_Standards_AbstractPatternSniff implements PHP_CodeSniffer_Sniff
Chris@0 36 {
Chris@0 37
Chris@0 38 /**
Chris@0 39 * If true, comments will be ignored if they are found in the code.
Chris@0 40 *
Chris@0 41 * @var boolean
Chris@0 42 */
Chris@0 43 public $ignoreComments = false;
Chris@0 44
Chris@0 45 /**
Chris@0 46 * The current file being checked.
Chris@0 47 *
Chris@0 48 * @var string
Chris@0 49 */
Chris@0 50 protected $currFile = '';
Chris@0 51
Chris@0 52 /**
Chris@0 53 * The parsed patterns array.
Chris@0 54 *
Chris@0 55 * @var array
Chris@0 56 */
Chris@0 57 private $_parsedPatterns = array();
Chris@0 58
Chris@0 59 /**
Chris@0 60 * Tokens that this sniff wishes to process outside of the patterns.
Chris@0 61 *
Chris@0 62 * @var array(int)
Chris@0 63 * @see registerSupplementary()
Chris@0 64 * @see processSupplementary()
Chris@0 65 */
Chris@0 66 private $_supplementaryTokens = array();
Chris@0 67
Chris@0 68 /**
Chris@0 69 * Positions in the stack where errors have occurred.
Chris@0 70 *
Chris@0 71 * @var array()
Chris@0 72 */
Chris@0 73 private $_errorPos = array();
Chris@0 74
Chris@0 75
Chris@0 76 /**
Chris@0 77 * Constructs a PHP_CodeSniffer_Standards_AbstractPatternSniff.
Chris@0 78 *
Chris@0 79 * @param boolean $ignoreComments If true, comments will be ignored.
Chris@0 80 */
Chris@0 81 public function __construct($ignoreComments=null)
Chris@0 82 {
Chris@0 83 // This is here for backwards compatibility.
Chris@0 84 if ($ignoreComments !== null) {
Chris@0 85 $this->ignoreComments = $ignoreComments;
Chris@0 86 }
Chris@0 87
Chris@0 88 $this->_supplementaryTokens = $this->registerSupplementary();
Chris@0 89
Chris@0 90 }//end __construct()
Chris@0 91
Chris@0 92
Chris@0 93 /**
Chris@0 94 * Registers the tokens to listen to.
Chris@0 95 *
Chris@0 96 * Classes extending <i>AbstractPatternTest</i> should implement the
Chris@0 97 * <i>getPatterns()</i> method to register the patterns they wish to test.
Chris@0 98 *
Chris@0 99 * @return int[]
Chris@0 100 * @see process()
Chris@0 101 */
Chris@0 102 public final function register()
Chris@0 103 {
Chris@0 104 $listenTypes = array();
Chris@0 105 $patterns = $this->getPatterns();
Chris@0 106
Chris@0 107 foreach ($patterns as $pattern) {
Chris@0 108 $parsedPattern = $this->_parse($pattern);
Chris@0 109
Chris@0 110 // Find a token position in the pattern that we can use
Chris@0 111 // for a listener token.
Chris@0 112 $pos = $this->_getListenerTokenPos($parsedPattern);
Chris@0 113 $tokenType = $parsedPattern[$pos]['token'];
Chris@0 114 $listenTypes[] = $tokenType;
Chris@0 115
Chris@0 116 $patternArray = array(
Chris@0 117 'listen_pos' => $pos,
Chris@0 118 'pattern' => $parsedPattern,
Chris@0 119 'pattern_code' => $pattern,
Chris@0 120 );
Chris@0 121
Chris@0 122 if (isset($this->_parsedPatterns[$tokenType]) === false) {
Chris@0 123 $this->_parsedPatterns[$tokenType] = array();
Chris@0 124 }
Chris@0 125
Chris@0 126 $this->_parsedPatterns[$tokenType][] = $patternArray;
Chris@0 127 }//end foreach
Chris@0 128
Chris@0 129 return array_unique(array_merge($listenTypes, $this->_supplementaryTokens));
Chris@0 130
Chris@0 131 }//end register()
Chris@0 132
Chris@0 133
Chris@0 134 /**
Chris@0 135 * Returns the token types that the specified pattern is checking for.
Chris@0 136 *
Chris@0 137 * Returned array is in the format:
Chris@0 138 * <code>
Chris@0 139 * array(
Chris@0 140 * T_WHITESPACE => 0, // 0 is the position where the T_WHITESPACE token
Chris@0 141 * // should occur in the pattern.
Chris@0 142 * );
Chris@0 143 * </code>
Chris@0 144 *
Chris@0 145 * @param array $pattern The parsed pattern to find the acquire the token
Chris@0 146 * types from.
Chris@0 147 *
Chris@0 148 * @return array<int, int>
Chris@0 149 */
Chris@0 150 private function _getPatternTokenTypes($pattern)
Chris@0 151 {
Chris@0 152 $tokenTypes = array();
Chris@0 153 foreach ($pattern as $pos => $patternInfo) {
Chris@0 154 if ($patternInfo['type'] === 'token') {
Chris@0 155 if (isset($tokenTypes[$patternInfo['token']]) === false) {
Chris@0 156 $tokenTypes[$patternInfo['token']] = $pos;
Chris@0 157 }
Chris@0 158 }
Chris@0 159 }
Chris@0 160
Chris@0 161 return $tokenTypes;
Chris@0 162
Chris@0 163 }//end _getPatternTokenTypes()
Chris@0 164
Chris@0 165
Chris@0 166 /**
Chris@0 167 * Returns the position in the pattern that this test should register as
Chris@0 168 * a listener for the pattern.
Chris@0 169 *
Chris@0 170 * @param array $pattern The pattern to acquire the listener for.
Chris@0 171 *
Chris@0 172 * @return int The position in the pattern that this test should register
Chris@0 173 * as the listener.
Chris@0 174 * @throws PHP_CodeSniffer_Exception If we could not determine a token
Chris@0 175 * to listen for.
Chris@0 176 */
Chris@0 177 private function _getListenerTokenPos($pattern)
Chris@0 178 {
Chris@0 179 $tokenTypes = $this->_getPatternTokenTypes($pattern);
Chris@0 180 $tokenCodes = array_keys($tokenTypes);
Chris@0 181 $token = PHP_CodeSniffer_Tokens::getHighestWeightedToken($tokenCodes);
Chris@0 182
Chris@0 183 // If we could not get a token.
Chris@0 184 if ($token === false) {
Chris@0 185 $error = 'Could not determine a token to listen for';
Chris@0 186 throw new PHP_CodeSniffer_Exception($error);
Chris@0 187 }
Chris@0 188
Chris@0 189 return $tokenTypes[$token];
Chris@0 190
Chris@0 191 }//end _getListenerTokenPos()
Chris@0 192
Chris@0 193
Chris@0 194 /**
Chris@0 195 * Processes the test.
Chris@0 196 *
Chris@0 197 * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where the
Chris@0 198 * token occurred.
Chris@0 199 * @param int $stackPtr The position in the tokens stack
Chris@0 200 * where the listening token type was
Chris@0 201 * found.
Chris@0 202 *
Chris@0 203 * @return void
Chris@0 204 * @see register()
Chris@0 205 */
Chris@0 206 public final function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
Chris@0 207 {
Chris@0 208 $file = $phpcsFile->getFilename();
Chris@0 209 if ($this->currFile !== $file) {
Chris@0 210 // We have changed files, so clean up.
Chris@0 211 $this->_errorPos = array();
Chris@0 212 $this->currFile = $file;
Chris@0 213 }
Chris@0 214
Chris@0 215 $tokens = $phpcsFile->getTokens();
Chris@0 216
Chris@0 217 if (in_array($tokens[$stackPtr]['code'], $this->_supplementaryTokens) === true) {
Chris@0 218 $this->processSupplementary($phpcsFile, $stackPtr);
Chris@0 219 }
Chris@0 220
Chris@0 221 $type = $tokens[$stackPtr]['code'];
Chris@0 222
Chris@0 223 // If the type is not set, then it must have been a token registered
Chris@0 224 // with registerSupplementary().
Chris@0 225 if (isset($this->_parsedPatterns[$type]) === false) {
Chris@0 226 return;
Chris@0 227 }
Chris@0 228
Chris@0 229 $allErrors = array();
Chris@0 230
Chris@0 231 // Loop over each pattern that is listening to the current token type
Chris@0 232 // that we are processing.
Chris@0 233 foreach ($this->_parsedPatterns[$type] as $patternInfo) {
Chris@0 234 // If processPattern returns false, then the pattern that we are
Chris@0 235 // checking the code with must not be designed to check that code.
Chris@0 236 $errors = $this->processPattern($patternInfo, $phpcsFile, $stackPtr);
Chris@0 237 if ($errors === false) {
Chris@0 238 // The pattern didn't match.
Chris@0 239 continue;
Chris@0 240 } else if (empty($errors) === true) {
Chris@0 241 // The pattern matched, but there were no errors.
Chris@0 242 break;
Chris@0 243 }
Chris@0 244
Chris@0 245 foreach ($errors as $stackPtr => $error) {
Chris@0 246 if (isset($this->_errorPos[$stackPtr]) === false) {
Chris@0 247 $this->_errorPos[$stackPtr] = true;
Chris@0 248 $allErrors[$stackPtr] = $error;
Chris@0 249 }
Chris@0 250 }
Chris@0 251 }
Chris@0 252
Chris@0 253 foreach ($allErrors as $stackPtr => $error) {
Chris@0 254 $phpcsFile->addError($error, $stackPtr);
Chris@0 255 }
Chris@0 256
Chris@0 257 }//end process()
Chris@0 258
Chris@0 259
Chris@0 260 /**
Chris@0 261 * Processes the pattern and verifies the code at $stackPtr.
Chris@0 262 *
Chris@0 263 * @param array $patternInfo Information about the pattern used
Chris@0 264 * for checking, which includes are
Chris@0 265 * parsed token representation of the
Chris@0 266 * pattern.
Chris@0 267 * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where the
Chris@0 268 * token occurred.
Chris@0 269 * @param int $stackPtr The position in the tokens stack where
Chris@0 270 * the listening token type was found.
Chris@0 271 *
Chris@0 272 * @return array
Chris@0 273 */
Chris@0 274 protected function processPattern(
Chris@0 275 $patternInfo,
Chris@0 276 PHP_CodeSniffer_File $phpcsFile,
Chris@0 277 $stackPtr
Chris@0 278 ) {
Chris@0 279 $tokens = $phpcsFile->getTokens();
Chris@0 280 $pattern = $patternInfo['pattern'];
Chris@0 281 $patternCode = $patternInfo['pattern_code'];
Chris@0 282 $errors = array();
Chris@0 283 $found = '';
Chris@0 284
Chris@0 285 $ignoreTokens = array(T_WHITESPACE);
Chris@0 286 if ($this->ignoreComments === true) {
Chris@0 287 $ignoreTokens
Chris@0 288 = array_merge($ignoreTokens, PHP_CodeSniffer_Tokens::$commentTokens);
Chris@0 289 }
Chris@0 290
Chris@0 291 $origStackPtr = $stackPtr;
Chris@0 292 $hasError = false;
Chris@0 293
Chris@0 294 if ($patternInfo['listen_pos'] > 0) {
Chris@0 295 $stackPtr--;
Chris@0 296
Chris@0 297 for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
Chris@0 298 if ($pattern[$i]['type'] === 'token') {
Chris@0 299 if ($pattern[$i]['token'] === T_WHITESPACE) {
Chris@0 300 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
Chris@0 301 $found = $tokens[$stackPtr]['content'].$found;
Chris@0 302 }
Chris@0 303
Chris@0 304 // Only check the size of the whitespace if this is not
Chris@0 305 // the first token. We don't care about the size of
Chris@0 306 // leading whitespace, just that there is some.
Chris@0 307 if ($i !== 0) {
Chris@0 308 if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
Chris@0 309 $hasError = true;
Chris@0 310 }
Chris@0 311 }
Chris@0 312 } else {
Chris@0 313 // Check to see if this important token is the same as the
Chris@0 314 // previous important token in the pattern. If it is not,
Chris@0 315 // then the pattern cannot be for this piece of code.
Chris@0 316 $prev = $phpcsFile->findPrevious(
Chris@0 317 $ignoreTokens,
Chris@0 318 $stackPtr,
Chris@0 319 null,
Chris@0 320 true
Chris@0 321 );
Chris@0 322
Chris@0 323 if ($prev === false
Chris@0 324 || $tokens[$prev]['code'] !== $pattern[$i]['token']
Chris@0 325 ) {
Chris@0 326 return false;
Chris@0 327 }
Chris@0 328
Chris@0 329 // If we skipped past some whitespace tokens, then add them
Chris@0 330 // to the found string.
Chris@0 331 $tokenContent = $phpcsFile->getTokensAsString(
Chris@0 332 ($prev + 1),
Chris@0 333 ($stackPtr - $prev - 1)
Chris@0 334 );
Chris@0 335
Chris@0 336 $found = $tokens[$prev]['content'].$tokenContent.$found;
Chris@0 337
Chris@0 338 if (isset($pattern[($i - 1)]) === true
Chris@0 339 && $pattern[($i - 1)]['type'] === 'skip'
Chris@0 340 ) {
Chris@0 341 $stackPtr = $prev;
Chris@0 342 } else {
Chris@0 343 $stackPtr = ($prev - 1);
Chris@0 344 }
Chris@0 345 }//end if
Chris@0 346 } else if ($pattern[$i]['type'] === 'skip') {
Chris@0 347 // Skip to next piece of relevant code.
Chris@0 348 if ($pattern[$i]['to'] === 'parenthesis_closer') {
Chris@0 349 $to = 'parenthesis_opener';
Chris@0 350 } else {
Chris@0 351 $to = 'scope_opener';
Chris@0 352 }
Chris@0 353
Chris@0 354 // Find the previous opener.
Chris@0 355 $next = $phpcsFile->findPrevious(
Chris@0 356 $ignoreTokens,
Chris@0 357 $stackPtr,
Chris@0 358 null,
Chris@0 359 true
Chris@0 360 );
Chris@0 361
Chris@0 362 if ($next === false || isset($tokens[$next][$to]) === false) {
Chris@0 363 // If there was not opener, then we must be
Chris@0 364 // using the wrong pattern.
Chris@0 365 return false;
Chris@0 366 }
Chris@0 367
Chris@0 368 if ($to === 'parenthesis_opener') {
Chris@0 369 $found = '{'.$found;
Chris@0 370 } else {
Chris@0 371 $found = '('.$found;
Chris@0 372 }
Chris@0 373
Chris@0 374 $found = '...'.$found;
Chris@0 375
Chris@0 376 // Skip to the opening token.
Chris@0 377 $stackPtr = ($tokens[$next][$to] - 1);
Chris@0 378 } else if ($pattern[$i]['type'] === 'string') {
Chris@0 379 $found = 'abc';
Chris@0 380 } else if ($pattern[$i]['type'] === 'newline') {
Chris@0 381 if ($this->ignoreComments === true
Chris@0 382 && isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true
Chris@0 383 ) {
Chris@0 384 $startComment = $phpcsFile->findPrevious(
Chris@0 385 PHP_CodeSniffer_Tokens::$commentTokens,
Chris@0 386 ($stackPtr - 1),
Chris@0 387 null,
Chris@0 388 true
Chris@0 389 );
Chris@0 390
Chris@0 391 if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
Chris@0 392 $startComment++;
Chris@0 393 }
Chris@0 394
Chris@0 395 $tokenContent = $phpcsFile->getTokensAsString(
Chris@0 396 $startComment,
Chris@0 397 ($stackPtr - $startComment + 1)
Chris@0 398 );
Chris@0 399
Chris@0 400 $found = $tokenContent.$found;
Chris@0 401 $stackPtr = ($startComment - 1);
Chris@0 402 }
Chris@0 403
Chris@0 404 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
Chris@0 405 if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
Chris@0 406 $found = $tokens[$stackPtr]['content'].$found;
Chris@0 407
Chris@0 408 // This may just be an indent that comes after a newline
Chris@0 409 // so check the token before to make sure. If it is a newline, we
Chris@0 410 // can ignore the error here.
Chris@0 411 if (($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar)
Chris@0 412 && ($this->ignoreComments === true
Chris@0 413 && isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[($stackPtr - 1)]['code']]) === false)
Chris@0 414 ) {
Chris@0 415 $hasError = true;
Chris@0 416 } else {
Chris@0 417 $stackPtr--;
Chris@0 418 }
Chris@0 419 } else {
Chris@0 420 $found = 'EOL'.$found;
Chris@0 421 }
Chris@0 422 } else {
Chris@0 423 $found = $tokens[$stackPtr]['content'].$found;
Chris@0 424 $hasError = true;
Chris@0 425 }//end if
Chris@0 426
Chris@0 427 if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
Chris@0 428 // Make sure they only have 1 newline.
Chris@0 429 $prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
Chris@0 430 if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
Chris@0 431 $hasError = true;
Chris@0 432 }
Chris@0 433 }
Chris@0 434 }//end if
Chris@0 435 }//end for
Chris@0 436 }//end if
Chris@0 437
Chris@0 438 $stackPtr = $origStackPtr;
Chris@0 439 $lastAddedStackPtr = null;
Chris@0 440 $patternLen = count($pattern);
Chris@0 441
Chris@0 442 for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
Chris@0 443 if (isset($tokens[$stackPtr]) === false) {
Chris@0 444 break;
Chris@0 445 }
Chris@0 446
Chris@0 447 if ($pattern[$i]['type'] === 'token') {
Chris@0 448 if ($pattern[$i]['token'] === T_WHITESPACE) {
Chris@0 449 if ($this->ignoreComments === true) {
Chris@0 450 // If we are ignoring comments, check to see if this current
Chris@0 451 // token is a comment. If so skip it.
Chris@0 452 if (isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true) {
Chris@0 453 continue;
Chris@0 454 }
Chris@0 455
Chris@0 456 // If the next token is a comment, the we need to skip the
Chris@0 457 // current token as we should allow a space before a
Chris@0 458 // comment for readability.
Chris@0 459 if (isset($tokens[($stackPtr + 1)]) === true
Chris@0 460 && isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[($stackPtr + 1)]['code']]) === true
Chris@0 461 ) {
Chris@0 462 continue;
Chris@0 463 }
Chris@0 464 }
Chris@0 465
Chris@0 466 $tokenContent = '';
Chris@0 467 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
Chris@0 468 if (isset($pattern[($i + 1)]) === false) {
Chris@0 469 // This is the last token in the pattern, so just compare
Chris@0 470 // the next token of content.
Chris@0 471 $tokenContent = $tokens[$stackPtr]['content'];
Chris@0 472 } else {
Chris@0 473 // Get all the whitespace to the next token.
Chris@0 474 $next = $phpcsFile->findNext(
Chris@0 475 PHP_CodeSniffer_Tokens::$emptyTokens,
Chris@0 476 $stackPtr,
Chris@0 477 null,
Chris@0 478 true
Chris@0 479 );
Chris@0 480
Chris@0 481 $tokenContent = $phpcsFile->getTokensAsString(
Chris@0 482 $stackPtr,
Chris@0 483 ($next - $stackPtr)
Chris@0 484 );
Chris@0 485
Chris@0 486 $lastAddedStackPtr = $stackPtr;
Chris@0 487 $stackPtr = $next;
Chris@0 488 }//end if
Chris@0 489
Chris@0 490 if ($stackPtr !== $lastAddedStackPtr) {
Chris@0 491 $found .= $tokenContent;
Chris@0 492 }
Chris@0 493 } else {
Chris@0 494 if ($stackPtr !== $lastAddedStackPtr) {
Chris@0 495 $found .= $tokens[$stackPtr]['content'];
Chris@0 496 $lastAddedStackPtr = $stackPtr;
Chris@0 497 }
Chris@0 498 }//end if
Chris@0 499
Chris@0 500 if (isset($pattern[($i + 1)]) === true
Chris@0 501 && $pattern[($i + 1)]['type'] === 'skip'
Chris@0 502 ) {
Chris@0 503 // The next token is a skip token, so we just need to make
Chris@0 504 // sure the whitespace we found has *at least* the
Chris@0 505 // whitespace required.
Chris@0 506 if (strpos($tokenContent, $pattern[$i]['value']) !== 0) {
Chris@0 507 $hasError = true;
Chris@0 508 }
Chris@0 509 } else {
Chris@0 510 if ($tokenContent !== $pattern[$i]['value']) {
Chris@0 511 $hasError = true;
Chris@0 512 }
Chris@0 513 }
Chris@0 514 } else {
Chris@0 515 // Check to see if this important token is the same as the
Chris@0 516 // next important token in the pattern. If it is not, then
Chris@0 517 // the pattern cannot be for this piece of code.
Chris@0 518 $next = $phpcsFile->findNext(
Chris@0 519 $ignoreTokens,
Chris@0 520 $stackPtr,
Chris@0 521 null,
Chris@0 522 true
Chris@0 523 );
Chris@0 524
Chris@0 525 if ($next === false
Chris@0 526 || $tokens[$next]['code'] !== $pattern[$i]['token']
Chris@0 527 ) {
Chris@0 528 // The next important token did not match the pattern.
Chris@0 529 return false;
Chris@0 530 }
Chris@0 531
Chris@0 532 if ($lastAddedStackPtr !== null) {
Chris@0 533 if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
Chris@0 534 || $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
Chris@0 535 && isset($tokens[$next]['scope_condition']) === true
Chris@0 536 && $tokens[$next]['scope_condition'] > $lastAddedStackPtr
Chris@0 537 ) {
Chris@0 538 // This is a brace, but the owner of it is after the current
Chris@0 539 // token, which means it does not belong to any token in
Chris@0 540 // our pattern. This means the pattern is not for us.
Chris@0 541 return false;
Chris@0 542 }
Chris@0 543
Chris@0 544 if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
Chris@0 545 || $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
Chris@0 546 && isset($tokens[$next]['parenthesis_owner']) === true
Chris@0 547 && $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
Chris@0 548 ) {
Chris@0 549 // This is a bracket, but the owner of it is after the current
Chris@0 550 // token, which means it does not belong to any token in
Chris@0 551 // our pattern. This means the pattern is not for us.
Chris@0 552 return false;
Chris@0 553 }
Chris@0 554 }//end if
Chris@0 555
Chris@0 556 // If we skipped past some whitespace tokens, then add them
Chris@0 557 // to the found string.
Chris@0 558 if (($next - $stackPtr) > 0) {
Chris@0 559 $hasComment = false;
Chris@0 560 for ($j = $stackPtr; $j < $next; $j++) {
Chris@0 561 $found .= $tokens[$j]['content'];
Chris@0 562 if (isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$j]['code']]) === true) {
Chris@0 563 $hasComment = true;
Chris@0 564 }
Chris@0 565 }
Chris@0 566
Chris@0 567 // If we are not ignoring comments, this additional
Chris@0 568 // whitespace or comment is not allowed. If we are
Chris@0 569 // ignoring comments, there needs to be at least one
Chris@0 570 // comment for this to be allowed.
Chris@0 571 if ($this->ignoreComments === false
Chris@0 572 || ($this->ignoreComments === true
Chris@0 573 && $hasComment === false)
Chris@0 574 ) {
Chris@0 575 $hasError = true;
Chris@0 576 }
Chris@0 577
Chris@0 578 // Even when ignoring comments, we are not allowed to include
Chris@0 579 // newlines without the pattern specifying them, so
Chris@0 580 // everything should be on the same line.
Chris@0 581 if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
Chris@0 582 $hasError = true;
Chris@0 583 }
Chris@0 584 }//end if
Chris@0 585
Chris@0 586 if ($next !== $lastAddedStackPtr) {
Chris@0 587 $found .= $tokens[$next]['content'];
Chris@0 588 $lastAddedStackPtr = $next;
Chris@0 589 }
Chris@0 590
Chris@0 591 if (isset($pattern[($i + 1)]) === true
Chris@0 592 && $pattern[($i + 1)]['type'] === 'skip'
Chris@0 593 ) {
Chris@0 594 $stackPtr = $next;
Chris@0 595 } else {
Chris@0 596 $stackPtr = ($next + 1);
Chris@0 597 }
Chris@0 598 }//end if
Chris@0 599 } else if ($pattern[$i]['type'] === 'skip') {
Chris@0 600 if ($pattern[$i]['to'] === 'unknown') {
Chris@0 601 $next = $phpcsFile->findNext(
Chris@0 602 $pattern[($i + 1)]['token'],
Chris@0 603 $stackPtr
Chris@0 604 );
Chris@0 605
Chris@0 606 if ($next === false) {
Chris@0 607 // Couldn't find the next token, so we must
Chris@0 608 // be using the wrong pattern.
Chris@0 609 return false;
Chris@0 610 }
Chris@0 611
Chris@0 612 $found .= '...';
Chris@0 613 $stackPtr = $next;
Chris@0 614 } else {
Chris@0 615 // Find the previous opener.
Chris@0 616 $next = $phpcsFile->findPrevious(
Chris@0 617 PHP_CodeSniffer_Tokens::$blockOpeners,
Chris@0 618 $stackPtr
Chris@0 619 );
Chris@0 620
Chris@0 621 if ($next === false
Chris@0 622 || isset($tokens[$next][$pattern[$i]['to']]) === false
Chris@0 623 ) {
Chris@0 624 // If there was not opener, then we must
Chris@0 625 // be using the wrong pattern.
Chris@0 626 return false;
Chris@0 627 }
Chris@0 628
Chris@0 629 $found .= '...';
Chris@0 630 if ($pattern[$i]['to'] === 'parenthesis_closer') {
Chris@0 631 $found .= ')';
Chris@0 632 } else {
Chris@0 633 $found .= '}';
Chris@0 634 }
Chris@0 635
Chris@0 636 // Skip to the closing token.
Chris@0 637 $stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
Chris@0 638 }//end if
Chris@0 639 } else if ($pattern[$i]['type'] === 'string') {
Chris@0 640 if ($tokens[$stackPtr]['code'] !== T_STRING) {
Chris@0 641 $hasError = true;
Chris@0 642 }
Chris@0 643
Chris@0 644 if ($stackPtr !== $lastAddedStackPtr) {
Chris@0 645 $found .= 'abc';
Chris@0 646 $lastAddedStackPtr = $stackPtr;
Chris@0 647 }
Chris@0 648
Chris@0 649 $stackPtr++;
Chris@0 650 } else if ($pattern[$i]['type'] === 'newline') {
Chris@0 651 // Find the next token that contains a newline character.
Chris@0 652 $newline = 0;
Chris@0 653 for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
Chris@0 654 if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
Chris@0 655 $newline = $j;
Chris@0 656 break;
Chris@0 657 }
Chris@0 658 }
Chris@0 659
Chris@0 660 if ($newline === 0) {
Chris@0 661 // We didn't find a newline character in the rest of the file.
Chris@0 662 $next = ($phpcsFile->numTokens - 1);
Chris@0 663 $hasError = true;
Chris@0 664 } else {
Chris@0 665 if ($this->ignoreComments === false) {
Chris@0 666 // The newline character cannot be part of a comment.
Chris@0 667 if (isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$newline]['code']]) === true) {
Chris@0 668 $hasError = true;
Chris@0 669 }
Chris@0 670 }
Chris@0 671
Chris@0 672 if ($newline === $stackPtr) {
Chris@0 673 $next = ($stackPtr + 1);
Chris@0 674 } else {
Chris@0 675 // Check that there were no significant tokens that we
Chris@0 676 // skipped over to find our newline character.
Chris@0 677 $next = $phpcsFile->findNext(
Chris@0 678 $ignoreTokens,
Chris@0 679 $stackPtr,
Chris@0 680 null,
Chris@0 681 true
Chris@0 682 );
Chris@0 683
Chris@0 684 if ($next < $newline) {
Chris@0 685 // We skipped a non-ignored token.
Chris@0 686 $hasError = true;
Chris@0 687 } else {
Chris@0 688 $next = ($newline + 1);
Chris@0 689 }
Chris@0 690 }
Chris@0 691 }//end if
Chris@0 692
Chris@0 693 if ($stackPtr !== $lastAddedStackPtr) {
Chris@0 694 $found .= $phpcsFile->getTokensAsString(
Chris@0 695 $stackPtr,
Chris@0 696 ($next - $stackPtr)
Chris@0 697 );
Chris@0 698
Chris@0 699 $diff = ($next - $stackPtr);
Chris@0 700 $lastAddedStackPtr = ($next - 1);
Chris@0 701 }
Chris@0 702
Chris@0 703 $stackPtr = $next;
Chris@0 704 }//end if
Chris@0 705 }//end for
Chris@0 706
Chris@0 707 if ($hasError === true) {
Chris@0 708 $error = $this->prepareError($found, $patternCode);
Chris@0 709 $errors[$origStackPtr] = $error;
Chris@0 710 }
Chris@0 711
Chris@0 712 return $errors;
Chris@0 713
Chris@0 714 }//end processPattern()
Chris@0 715
Chris@0 716
Chris@0 717 /**
Chris@0 718 * Prepares an error for the specified patternCode.
Chris@0 719 *
Chris@0 720 * @param string $found The actual found string in the code.
Chris@0 721 * @param string $patternCode The expected pattern code.
Chris@0 722 *
Chris@0 723 * @return string The error message.
Chris@0 724 */
Chris@0 725 protected function prepareError($found, $patternCode)
Chris@0 726 {
Chris@0 727 $found = str_replace("\r\n", '\n', $found);
Chris@0 728 $found = str_replace("\n", '\n', $found);
Chris@0 729 $found = str_replace("\r", '\n', $found);
Chris@0 730 $found = str_replace("\t", '\t', $found);
Chris@0 731 $found = str_replace('EOL', '\n', $found);
Chris@0 732 $expected = str_replace('EOL', '\n', $patternCode);
Chris@0 733
Chris@0 734 $error = "Expected \"$expected\"; found \"$found\"";
Chris@0 735
Chris@0 736 return $error;
Chris@0 737
Chris@0 738 }//end prepareError()
Chris@0 739
Chris@0 740
Chris@0 741 /**
Chris@0 742 * Returns the patterns that should be checked.
Chris@0 743 *
Chris@0 744 * @return string[]
Chris@0 745 */
Chris@0 746 protected abstract function getPatterns();
Chris@0 747
Chris@0 748
Chris@0 749 /**
Chris@0 750 * Registers any supplementary tokens that this test might wish to process.
Chris@0 751 *
Chris@0 752 * A sniff may wish to register supplementary tests when it wishes to group
Chris@0 753 * an arbitrary validation that cannot be performed using a pattern, with
Chris@0 754 * other pattern tests.
Chris@0 755 *
Chris@0 756 * @return int[]
Chris@0 757 * @see processSupplementary()
Chris@0 758 */
Chris@0 759 protected function registerSupplementary()
Chris@0 760 {
Chris@0 761 return array();
Chris@0 762
Chris@0 763 }//end registerSupplementary()
Chris@0 764
Chris@0 765
Chris@0 766 /**
Chris@0 767 * Processes any tokens registered with registerSupplementary().
Chris@0 768 *
Chris@0 769 * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where to
Chris@0 770 * process the skip.
Chris@0 771 * @param int $stackPtr The position in the tokens stack to
Chris@0 772 * process.
Chris@0 773 *
Chris@0 774 * @return void
Chris@0 775 * @see registerSupplementary()
Chris@0 776 */
Chris@0 777 protected function processSupplementary(
Chris@0 778 PHP_CodeSniffer_File $phpcsFile,
Chris@0 779 $stackPtr
Chris@0 780 ) {
Chris@0 781
Chris@0 782 }//end processSupplementary()
Chris@0 783
Chris@0 784
Chris@0 785 /**
Chris@0 786 * Parses a pattern string into an array of pattern steps.
Chris@0 787 *
Chris@0 788 * @param string $pattern The pattern to parse.
Chris@0 789 *
Chris@0 790 * @return array The parsed pattern array.
Chris@0 791 * @see _createSkipPattern()
Chris@0 792 * @see _createTokenPattern()
Chris@0 793 */
Chris@0 794 private function _parse($pattern)
Chris@0 795 {
Chris@0 796 $patterns = array();
Chris@0 797 $length = strlen($pattern);
Chris@0 798 $lastToken = 0;
Chris@0 799 $firstToken = 0;
Chris@0 800
Chris@0 801 for ($i = 0; $i < $length; $i++) {
Chris@0 802 $specialPattern = false;
Chris@0 803 $isLastChar = ($i === ($length - 1));
Chris@0 804 $oldFirstToken = $firstToken;
Chris@0 805
Chris@0 806 if (substr($pattern, $i, 3) === '...') {
Chris@0 807 // It's a skip pattern. The skip pattern requires the
Chris@0 808 // content of the token in the "from" position and the token
Chris@0 809 // to skip to.
Chris@0 810 $specialPattern = $this->_createSkipPattern($pattern, ($i - 1));
Chris@0 811 $lastToken = ($i - $firstToken);
Chris@0 812 $firstToken = ($i + 3);
Chris@0 813 $i = ($i + 2);
Chris@0 814
Chris@0 815 if ($specialPattern['to'] !== 'unknown') {
Chris@0 816 $firstToken++;
Chris@0 817 }
Chris@0 818 } else if (substr($pattern, $i, 3) === 'abc') {
Chris@0 819 $specialPattern = array('type' => 'string');
Chris@0 820 $lastToken = ($i - $firstToken);
Chris@0 821 $firstToken = ($i + 3);
Chris@0 822 $i = ($i + 2);
Chris@0 823 } else if (substr($pattern, $i, 3) === 'EOL') {
Chris@0 824 $specialPattern = array('type' => 'newline');
Chris@0 825 $lastToken = ($i - $firstToken);
Chris@0 826 $firstToken = ($i + 3);
Chris@0 827 $i = ($i + 2);
Chris@0 828 }//end if
Chris@0 829
Chris@0 830 if ($specialPattern !== false || $isLastChar === true) {
Chris@0 831 // If we are at the end of the string, don't worry about a limit.
Chris@0 832 if ($isLastChar === true) {
Chris@0 833 // Get the string from the end of the last skip pattern, if any,
Chris@0 834 // to the end of the pattern string.
Chris@0 835 $str = substr($pattern, $oldFirstToken);
Chris@0 836 } else {
Chris@0 837 // Get the string from the end of the last special pattern,
Chris@0 838 // if any, to the start of this special pattern.
Chris@0 839 if ($lastToken === 0) {
Chris@0 840 // Note that if the last special token was zero characters ago,
Chris@0 841 // there will be nothing to process so we can skip this bit.
Chris@0 842 // This happens if you have something like: EOL... in your pattern.
Chris@0 843 $str = '';
Chris@0 844 } else {
Chris@0 845 $str = substr($pattern, $oldFirstToken, $lastToken);
Chris@0 846 }
Chris@0 847 }
Chris@0 848
Chris@0 849 if ($str !== '') {
Chris@0 850 $tokenPatterns = $this->_createTokenPattern($str);
Chris@0 851 foreach ($tokenPatterns as $tokenPattern) {
Chris@0 852 $patterns[] = $tokenPattern;
Chris@0 853 }
Chris@0 854 }
Chris@0 855
Chris@0 856 // Make sure we don't skip the last token.
Chris@0 857 if ($isLastChar === false && $i === ($length - 1)) {
Chris@0 858 $i--;
Chris@0 859 }
Chris@0 860 }//end if
Chris@0 861
Chris@0 862 // Add the skip pattern *after* we have processed
Chris@0 863 // all the tokens from the end of the last skip pattern
Chris@0 864 // to the start of this skip pattern.
Chris@0 865 if ($specialPattern !== false) {
Chris@0 866 $patterns[] = $specialPattern;
Chris@0 867 }
Chris@0 868 }//end for
Chris@0 869
Chris@0 870 return $patterns;
Chris@0 871
Chris@0 872 }//end _parse()
Chris@0 873
Chris@0 874
Chris@0 875 /**
Chris@0 876 * Creates a skip pattern.
Chris@0 877 *
Chris@0 878 * @param string $pattern The pattern being parsed.
Chris@0 879 * @param string $from The token content that the skip pattern starts from.
Chris@0 880 *
Chris@0 881 * @return array The pattern step.
Chris@0 882 * @see _createTokenPattern()
Chris@0 883 * @see _parse()
Chris@0 884 */
Chris@0 885 private function _createSkipPattern($pattern, $from)
Chris@0 886 {
Chris@0 887 $skip = array('type' => 'skip');
Chris@0 888
Chris@0 889 $nestedParenthesis = 0;
Chris@0 890 $nestedBraces = 0;
Chris@0 891 for ($start = $from; $start >= 0; $start--) {
Chris@0 892 switch ($pattern[$start]) {
Chris@0 893 case '(':
Chris@0 894 if ($nestedParenthesis === 0) {
Chris@0 895 $skip['to'] = 'parenthesis_closer';
Chris@0 896 }
Chris@0 897
Chris@0 898 $nestedParenthesis--;
Chris@0 899 break;
Chris@0 900 case '{':
Chris@0 901 if ($nestedBraces === 0) {
Chris@0 902 $skip['to'] = 'scope_closer';
Chris@0 903 }
Chris@0 904
Chris@0 905 $nestedBraces--;
Chris@0 906 break;
Chris@0 907 case '}':
Chris@0 908 $nestedBraces++;
Chris@0 909 break;
Chris@0 910 case ')':
Chris@0 911 $nestedParenthesis++;
Chris@0 912 break;
Chris@0 913 }//end switch
Chris@0 914
Chris@0 915 if (isset($skip['to']) === true) {
Chris@0 916 break;
Chris@0 917 }
Chris@0 918 }//end for
Chris@0 919
Chris@0 920 if (isset($skip['to']) === false) {
Chris@0 921 $skip['to'] = 'unknown';
Chris@0 922 }
Chris@0 923
Chris@0 924 return $skip;
Chris@0 925
Chris@0 926 }//end _createSkipPattern()
Chris@0 927
Chris@0 928
Chris@0 929 /**
Chris@0 930 * Creates a token pattern.
Chris@0 931 *
Chris@0 932 * @param string $str The tokens string that the pattern should match.
Chris@0 933 *
Chris@0 934 * @return array The pattern step.
Chris@0 935 * @see _createSkipPattern()
Chris@0 936 * @see _parse()
Chris@0 937 */
Chris@0 938 private function _createTokenPattern($str)
Chris@0 939 {
Chris@0 940 // Don't add a space after the closing php tag as it will add a new
Chris@0 941 // whitespace token.
Chris@0 942 $tokenizer = new PHP_CodeSniffer_Tokenizers_PHP();
Chris@0 943 $tokens = $tokenizer->tokenizeString('<?php '.$str.'?>');
Chris@0 944
Chris@0 945 // Remove the <?php tag from the front and the end php tag from the back.
Chris@0 946 $tokens = array_slice($tokens, 1, (count($tokens) - 2));
Chris@0 947
Chris@0 948 $patterns = array();
Chris@0 949 foreach ($tokens as $patternInfo) {
Chris@0 950 $patterns[] = array(
Chris@0 951 'type' => 'token',
Chris@0 952 'token' => $patternInfo['code'],
Chris@0 953 'value' => $patternInfo['content'],
Chris@0 954 );
Chris@0 955 }
Chris@0 956
Chris@0 957 return $patterns;
Chris@0 958
Chris@0 959 }//end _createTokenPattern()
Chris@0 960
Chris@0 961
Chris@0 962 }//end class