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