annotate vendor/squizlabs/php_codesniffer/src/Sniffs/AbstractPatternSniff.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
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@18 156 * @throws \PHP_CodeSniffer\Exceptions\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@18 198 if (in_array($tokens[$stackPtr]['code'], $this->supplementaryTokens, true) === 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@18 263 $ignoreTokens = [T_WHITESPACE => T_WHITESPACE];
Chris@17 264 if ($this->ignoreComments === true) {
Chris@18 265 $ignoreTokens += Tokens::$commentTokens;
Chris@17 266 }
Chris@17 267
Chris@17 268 $origStackPtr = $stackPtr;
Chris@17 269 $hasError = false;
Chris@17 270
Chris@17 271 if ($patternInfo['listen_pos'] > 0) {
Chris@17 272 $stackPtr--;
Chris@17 273
Chris@17 274 for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
Chris@17 275 if ($pattern[$i]['type'] === 'token') {
Chris@17 276 if ($pattern[$i]['token'] === T_WHITESPACE) {
Chris@17 277 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
Chris@17 278 $found = $tokens[$stackPtr]['content'].$found;
Chris@17 279 }
Chris@17 280
Chris@17 281 // Only check the size of the whitespace if this is not
Chris@17 282 // the first token. We don't care about the size of
Chris@17 283 // leading whitespace, just that there is some.
Chris@17 284 if ($i !== 0) {
Chris@17 285 if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
Chris@17 286 $hasError = true;
Chris@17 287 }
Chris@17 288 }
Chris@17 289 } else {
Chris@17 290 // Check to see if this important token is the same as the
Chris@17 291 // previous important token in the pattern. If it is not,
Chris@17 292 // then the pattern cannot be for this piece of code.
Chris@17 293 $prev = $phpcsFile->findPrevious(
Chris@17 294 $ignoreTokens,
Chris@17 295 $stackPtr,
Chris@17 296 null,
Chris@17 297 true
Chris@17 298 );
Chris@17 299
Chris@17 300 if ($prev === false
Chris@17 301 || $tokens[$prev]['code'] !== $pattern[$i]['token']
Chris@17 302 ) {
Chris@17 303 return false;
Chris@17 304 }
Chris@17 305
Chris@17 306 // If we skipped past some whitespace tokens, then add them
Chris@17 307 // to the found string.
Chris@17 308 $tokenContent = $phpcsFile->getTokensAsString(
Chris@17 309 ($prev + 1),
Chris@17 310 ($stackPtr - $prev - 1)
Chris@17 311 );
Chris@17 312
Chris@17 313 $found = $tokens[$prev]['content'].$tokenContent.$found;
Chris@17 314
Chris@17 315 if (isset($pattern[($i - 1)]) === true
Chris@17 316 && $pattern[($i - 1)]['type'] === 'skip'
Chris@17 317 ) {
Chris@17 318 $stackPtr = $prev;
Chris@17 319 } else {
Chris@17 320 $stackPtr = ($prev - 1);
Chris@17 321 }
Chris@17 322 }//end if
Chris@17 323 } else if ($pattern[$i]['type'] === 'skip') {
Chris@17 324 // Skip to next piece of relevant code.
Chris@17 325 if ($pattern[$i]['to'] === 'parenthesis_closer') {
Chris@17 326 $to = 'parenthesis_opener';
Chris@17 327 } else {
Chris@17 328 $to = 'scope_opener';
Chris@17 329 }
Chris@17 330
Chris@17 331 // Find the previous opener.
Chris@17 332 $next = $phpcsFile->findPrevious(
Chris@17 333 $ignoreTokens,
Chris@17 334 $stackPtr,
Chris@17 335 null,
Chris@17 336 true
Chris@17 337 );
Chris@17 338
Chris@17 339 if ($next === false || isset($tokens[$next][$to]) === false) {
Chris@17 340 // If there was not opener, then we must be
Chris@17 341 // using the wrong pattern.
Chris@17 342 return false;
Chris@17 343 }
Chris@17 344
Chris@17 345 if ($to === 'parenthesis_opener') {
Chris@17 346 $found = '{'.$found;
Chris@17 347 } else {
Chris@17 348 $found = '('.$found;
Chris@17 349 }
Chris@17 350
Chris@17 351 $found = '...'.$found;
Chris@17 352
Chris@17 353 // Skip to the opening token.
Chris@17 354 $stackPtr = ($tokens[$next][$to] - 1);
Chris@17 355 } else if ($pattern[$i]['type'] === 'string') {
Chris@17 356 $found = 'abc';
Chris@17 357 } else if ($pattern[$i]['type'] === 'newline') {
Chris@17 358 if ($this->ignoreComments === true
Chris@17 359 && isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true
Chris@17 360 ) {
Chris@17 361 $startComment = $phpcsFile->findPrevious(
Chris@17 362 Tokens::$commentTokens,
Chris@17 363 ($stackPtr - 1),
Chris@17 364 null,
Chris@17 365 true
Chris@17 366 );
Chris@17 367
Chris@17 368 if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
Chris@17 369 $startComment++;
Chris@17 370 }
Chris@17 371
Chris@17 372 $tokenContent = $phpcsFile->getTokensAsString(
Chris@17 373 $startComment,
Chris@17 374 ($stackPtr - $startComment + 1)
Chris@17 375 );
Chris@17 376
Chris@17 377 $found = $tokenContent.$found;
Chris@17 378 $stackPtr = ($startComment - 1);
Chris@17 379 }
Chris@17 380
Chris@17 381 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
Chris@17 382 if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
Chris@17 383 $found = $tokens[$stackPtr]['content'].$found;
Chris@17 384
Chris@17 385 // This may just be an indent that comes after a newline
Chris@17 386 // so check the token before to make sure. If it is a newline, we
Chris@17 387 // can ignore the error here.
Chris@17 388 if (($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar)
Chris@17 389 && ($this->ignoreComments === true
Chris@17 390 && isset(Tokens::$commentTokens[$tokens[($stackPtr - 1)]['code']]) === false)
Chris@17 391 ) {
Chris@17 392 $hasError = true;
Chris@17 393 } else {
Chris@17 394 $stackPtr--;
Chris@17 395 }
Chris@17 396 } else {
Chris@17 397 $found = 'EOL'.$found;
Chris@17 398 }
Chris@17 399 } else {
Chris@17 400 $found = $tokens[$stackPtr]['content'].$found;
Chris@17 401 $hasError = true;
Chris@17 402 }//end if
Chris@17 403
Chris@17 404 if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
Chris@17 405 // Make sure they only have 1 newline.
Chris@17 406 $prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
Chris@17 407 if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
Chris@17 408 $hasError = true;
Chris@17 409 }
Chris@17 410 }
Chris@17 411 }//end if
Chris@17 412 }//end for
Chris@17 413 }//end if
Chris@17 414
Chris@17 415 $stackPtr = $origStackPtr;
Chris@17 416 $lastAddedStackPtr = null;
Chris@17 417 $patternLen = count($pattern);
Chris@17 418
Chris@17 419 for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
Chris@17 420 if (isset($tokens[$stackPtr]) === false) {
Chris@17 421 break;
Chris@17 422 }
Chris@17 423
Chris@17 424 if ($pattern[$i]['type'] === 'token') {
Chris@17 425 if ($pattern[$i]['token'] === T_WHITESPACE) {
Chris@17 426 if ($this->ignoreComments === true) {
Chris@17 427 // If we are ignoring comments, check to see if this current
Chris@17 428 // token is a comment. If so skip it.
Chris@17 429 if (isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true) {
Chris@17 430 continue;
Chris@17 431 }
Chris@17 432
Chris@17 433 // If the next token is a comment, the we need to skip the
Chris@17 434 // current token as we should allow a space before a
Chris@17 435 // comment for readability.
Chris@17 436 if (isset($tokens[($stackPtr + 1)]) === true
Chris@17 437 && isset(Tokens::$commentTokens[$tokens[($stackPtr + 1)]['code']]) === true
Chris@17 438 ) {
Chris@17 439 continue;
Chris@17 440 }
Chris@17 441 }
Chris@17 442
Chris@17 443 $tokenContent = '';
Chris@17 444 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
Chris@17 445 if (isset($pattern[($i + 1)]) === false) {
Chris@17 446 // This is the last token in the pattern, so just compare
Chris@17 447 // the next token of content.
Chris@17 448 $tokenContent = $tokens[$stackPtr]['content'];
Chris@17 449 } else {
Chris@17 450 // Get all the whitespace to the next token.
Chris@17 451 $next = $phpcsFile->findNext(
Chris@17 452 Tokens::$emptyTokens,
Chris@17 453 $stackPtr,
Chris@17 454 null,
Chris@17 455 true
Chris@17 456 );
Chris@17 457
Chris@17 458 $tokenContent = $phpcsFile->getTokensAsString(
Chris@17 459 $stackPtr,
Chris@17 460 ($next - $stackPtr)
Chris@17 461 );
Chris@17 462
Chris@17 463 $lastAddedStackPtr = $stackPtr;
Chris@17 464 $stackPtr = $next;
Chris@17 465 }//end if
Chris@17 466
Chris@17 467 if ($stackPtr !== $lastAddedStackPtr) {
Chris@17 468 $found .= $tokenContent;
Chris@17 469 }
Chris@17 470 } else {
Chris@17 471 if ($stackPtr !== $lastAddedStackPtr) {
Chris@17 472 $found .= $tokens[$stackPtr]['content'];
Chris@17 473 $lastAddedStackPtr = $stackPtr;
Chris@17 474 }
Chris@17 475 }//end if
Chris@17 476
Chris@17 477 if (isset($pattern[($i + 1)]) === true
Chris@17 478 && $pattern[($i + 1)]['type'] === 'skip'
Chris@17 479 ) {
Chris@17 480 // The next token is a skip token, so we just need to make
Chris@17 481 // sure the whitespace we found has *at least* the
Chris@17 482 // whitespace required.
Chris@17 483 if (strpos($tokenContent, $pattern[$i]['value']) !== 0) {
Chris@17 484 $hasError = true;
Chris@17 485 }
Chris@17 486 } else {
Chris@17 487 if ($tokenContent !== $pattern[$i]['value']) {
Chris@17 488 $hasError = true;
Chris@17 489 }
Chris@17 490 }
Chris@17 491 } else {
Chris@17 492 // Check to see if this important token is the same as the
Chris@17 493 // next important token in the pattern. If it is not, then
Chris@17 494 // the pattern cannot be for this piece of code.
Chris@17 495 $next = $phpcsFile->findNext(
Chris@17 496 $ignoreTokens,
Chris@17 497 $stackPtr,
Chris@17 498 null,
Chris@17 499 true
Chris@17 500 );
Chris@17 501
Chris@17 502 if ($next === false
Chris@17 503 || $tokens[$next]['code'] !== $pattern[$i]['token']
Chris@17 504 ) {
Chris@17 505 // The next important token did not match the pattern.
Chris@17 506 return false;
Chris@17 507 }
Chris@17 508
Chris@17 509 if ($lastAddedStackPtr !== null) {
Chris@17 510 if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
Chris@17 511 || $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
Chris@17 512 && isset($tokens[$next]['scope_condition']) === true
Chris@17 513 && $tokens[$next]['scope_condition'] > $lastAddedStackPtr
Chris@17 514 ) {
Chris@17 515 // This is a brace, but the owner of it is after the current
Chris@17 516 // token, which means it does not belong to any token in
Chris@17 517 // our pattern. This means the pattern is not for us.
Chris@17 518 return false;
Chris@17 519 }
Chris@17 520
Chris@17 521 if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
Chris@17 522 || $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
Chris@17 523 && isset($tokens[$next]['parenthesis_owner']) === true
Chris@17 524 && $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
Chris@17 525 ) {
Chris@17 526 // This is a bracket, but the owner of it is after the current
Chris@17 527 // token, which means it does not belong to any token in
Chris@17 528 // our pattern. This means the pattern is not for us.
Chris@17 529 return false;
Chris@17 530 }
Chris@17 531 }//end if
Chris@17 532
Chris@17 533 // If we skipped past some whitespace tokens, then add them
Chris@17 534 // to the found string.
Chris@17 535 if (($next - $stackPtr) > 0) {
Chris@17 536 $hasComment = false;
Chris@17 537 for ($j = $stackPtr; $j < $next; $j++) {
Chris@17 538 $found .= $tokens[$j]['content'];
Chris@17 539 if (isset(Tokens::$commentTokens[$tokens[$j]['code']]) === true) {
Chris@17 540 $hasComment = true;
Chris@17 541 }
Chris@17 542 }
Chris@17 543
Chris@17 544 // If we are not ignoring comments, this additional
Chris@17 545 // whitespace or comment is not allowed. If we are
Chris@17 546 // ignoring comments, there needs to be at least one
Chris@17 547 // comment for this to be allowed.
Chris@17 548 if ($this->ignoreComments === false
Chris@17 549 || ($this->ignoreComments === true
Chris@17 550 && $hasComment === false)
Chris@17 551 ) {
Chris@17 552 $hasError = true;
Chris@17 553 }
Chris@17 554
Chris@17 555 // Even when ignoring comments, we are not allowed to include
Chris@17 556 // newlines without the pattern specifying them, so
Chris@17 557 // everything should be on the same line.
Chris@17 558 if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
Chris@17 559 $hasError = true;
Chris@17 560 }
Chris@17 561 }//end if
Chris@17 562
Chris@17 563 if ($next !== $lastAddedStackPtr) {
Chris@17 564 $found .= $tokens[$next]['content'];
Chris@17 565 $lastAddedStackPtr = $next;
Chris@17 566 }
Chris@17 567
Chris@17 568 if (isset($pattern[($i + 1)]) === true
Chris@17 569 && $pattern[($i + 1)]['type'] === 'skip'
Chris@17 570 ) {
Chris@17 571 $stackPtr = $next;
Chris@17 572 } else {
Chris@17 573 $stackPtr = ($next + 1);
Chris@17 574 }
Chris@17 575 }//end if
Chris@17 576 } else if ($pattern[$i]['type'] === 'skip') {
Chris@17 577 if ($pattern[$i]['to'] === 'unknown') {
Chris@17 578 $next = $phpcsFile->findNext(
Chris@17 579 $pattern[($i + 1)]['token'],
Chris@17 580 $stackPtr
Chris@17 581 );
Chris@17 582
Chris@17 583 if ($next === false) {
Chris@17 584 // Couldn't find the next token, so we must
Chris@17 585 // be using the wrong pattern.
Chris@17 586 return false;
Chris@17 587 }
Chris@17 588
Chris@17 589 $found .= '...';
Chris@17 590 $stackPtr = $next;
Chris@17 591 } else {
Chris@17 592 // Find the previous opener.
Chris@17 593 $next = $phpcsFile->findPrevious(
Chris@17 594 Tokens::$blockOpeners,
Chris@17 595 $stackPtr
Chris@17 596 );
Chris@17 597
Chris@17 598 if ($next === false
Chris@17 599 || isset($tokens[$next][$pattern[$i]['to']]) === false
Chris@17 600 ) {
Chris@17 601 // If there was not opener, then we must
Chris@17 602 // be using the wrong pattern.
Chris@17 603 return false;
Chris@17 604 }
Chris@17 605
Chris@17 606 $found .= '...';
Chris@17 607 if ($pattern[$i]['to'] === 'parenthesis_closer') {
Chris@17 608 $found .= ')';
Chris@17 609 } else {
Chris@17 610 $found .= '}';
Chris@17 611 }
Chris@17 612
Chris@17 613 // Skip to the closing token.
Chris@17 614 $stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
Chris@17 615 }//end if
Chris@17 616 } else if ($pattern[$i]['type'] === 'string') {
Chris@17 617 if ($tokens[$stackPtr]['code'] !== T_STRING) {
Chris@17 618 $hasError = true;
Chris@17 619 }
Chris@17 620
Chris@17 621 if ($stackPtr !== $lastAddedStackPtr) {
Chris@17 622 $found .= 'abc';
Chris@17 623 $lastAddedStackPtr = $stackPtr;
Chris@17 624 }
Chris@17 625
Chris@17 626 $stackPtr++;
Chris@17 627 } else if ($pattern[$i]['type'] === 'newline') {
Chris@17 628 // Find the next token that contains a newline character.
Chris@17 629 $newline = 0;
Chris@17 630 for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
Chris@17 631 if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
Chris@17 632 $newline = $j;
Chris@17 633 break;
Chris@17 634 }
Chris@17 635 }
Chris@17 636
Chris@17 637 if ($newline === 0) {
Chris@17 638 // We didn't find a newline character in the rest of the file.
Chris@17 639 $next = ($phpcsFile->numTokens - 1);
Chris@17 640 $hasError = true;
Chris@17 641 } else {
Chris@17 642 if ($this->ignoreComments === false) {
Chris@17 643 // The newline character cannot be part of a comment.
Chris@17 644 if (isset(Tokens::$commentTokens[$tokens[$newline]['code']]) === true) {
Chris@17 645 $hasError = true;
Chris@17 646 }
Chris@17 647 }
Chris@17 648
Chris@17 649 if ($newline === $stackPtr) {
Chris@17 650 $next = ($stackPtr + 1);
Chris@17 651 } else {
Chris@17 652 // Check that there were no significant tokens that we
Chris@17 653 // skipped over to find our newline character.
Chris@17 654 $next = $phpcsFile->findNext(
Chris@17 655 $ignoreTokens,
Chris@17 656 $stackPtr,
Chris@17 657 null,
Chris@17 658 true
Chris@17 659 );
Chris@17 660
Chris@17 661 if ($next < $newline) {
Chris@17 662 // We skipped a non-ignored token.
Chris@17 663 $hasError = true;
Chris@17 664 } else {
Chris@17 665 $next = ($newline + 1);
Chris@17 666 }
Chris@17 667 }
Chris@17 668 }//end if
Chris@17 669
Chris@17 670 if ($stackPtr !== $lastAddedStackPtr) {
Chris@17 671 $found .= $phpcsFile->getTokensAsString(
Chris@17 672 $stackPtr,
Chris@17 673 ($next - $stackPtr)
Chris@17 674 );
Chris@17 675
Chris@17 676 $lastAddedStackPtr = ($next - 1);
Chris@17 677 }
Chris@17 678
Chris@17 679 $stackPtr = $next;
Chris@17 680 }//end if
Chris@17 681 }//end for
Chris@17 682
Chris@17 683 if ($hasError === true) {
Chris@17 684 $error = $this->prepareError($found, $patternCode);
Chris@17 685 $errors[$origStackPtr] = $error;
Chris@17 686 }
Chris@17 687
Chris@17 688 return $errors;
Chris@17 689
Chris@17 690 }//end processPattern()
Chris@17 691
Chris@17 692
Chris@17 693 /**
Chris@17 694 * Prepares an error for the specified patternCode.
Chris@17 695 *
Chris@17 696 * @param string $found The actual found string in the code.
Chris@17 697 * @param string $patternCode The expected pattern code.
Chris@17 698 *
Chris@17 699 * @return string The error message.
Chris@17 700 */
Chris@17 701 protected function prepareError($found, $patternCode)
Chris@17 702 {
Chris@17 703 $found = str_replace("\r\n", '\n', $found);
Chris@17 704 $found = str_replace("\n", '\n', $found);
Chris@17 705 $found = str_replace("\r", '\n', $found);
Chris@17 706 $found = str_replace("\t", '\t', $found);
Chris@17 707 $found = str_replace('EOL', '\n', $found);
Chris@17 708 $expected = str_replace('EOL', '\n', $patternCode);
Chris@17 709
Chris@17 710 $error = "Expected \"$expected\"; found \"$found\"";
Chris@17 711
Chris@17 712 return $error;
Chris@17 713
Chris@17 714 }//end prepareError()
Chris@17 715
Chris@17 716
Chris@17 717 /**
Chris@17 718 * Returns the patterns that should be checked.
Chris@17 719 *
Chris@17 720 * @return string[]
Chris@17 721 */
Chris@17 722 abstract protected function getPatterns();
Chris@17 723
Chris@17 724
Chris@17 725 /**
Chris@17 726 * Registers any supplementary tokens that this test might wish to process.
Chris@17 727 *
Chris@17 728 * A sniff may wish to register supplementary tests when it wishes to group
Chris@17 729 * an arbitrary validation that cannot be performed using a pattern, with
Chris@17 730 * other pattern tests.
Chris@17 731 *
Chris@17 732 * @return int[]
Chris@17 733 * @see processSupplementary()
Chris@17 734 */
Chris@17 735 protected function registerSupplementary()
Chris@17 736 {
Chris@17 737 return [];
Chris@17 738
Chris@17 739 }//end registerSupplementary()
Chris@17 740
Chris@17 741
Chris@17 742 /**
Chris@17 743 * Processes any tokens registered with registerSupplementary().
Chris@17 744 *
Chris@17 745 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where to
Chris@17 746 * process the skip.
Chris@17 747 * @param int $stackPtr The position in the tokens stack to
Chris@17 748 * process.
Chris@17 749 *
Chris@17 750 * @return void
Chris@17 751 * @see registerSupplementary()
Chris@17 752 */
Chris@17 753 protected function processSupplementary(File $phpcsFile, $stackPtr)
Chris@17 754 {
Chris@17 755
Chris@17 756 }//end processSupplementary()
Chris@17 757
Chris@17 758
Chris@17 759 /**
Chris@17 760 * Parses a pattern string into an array of pattern steps.
Chris@17 761 *
Chris@17 762 * @param string $pattern The pattern to parse.
Chris@17 763 *
Chris@17 764 * @return array The parsed pattern array.
Chris@17 765 * @see createSkipPattern()
Chris@17 766 * @see createTokenPattern()
Chris@17 767 */
Chris@17 768 private function parse($pattern)
Chris@17 769 {
Chris@17 770 $patterns = [];
Chris@17 771 $length = strlen($pattern);
Chris@17 772 $lastToken = 0;
Chris@17 773 $firstToken = 0;
Chris@17 774
Chris@17 775 for ($i = 0; $i < $length; $i++) {
Chris@17 776 $specialPattern = false;
Chris@17 777 $isLastChar = ($i === ($length - 1));
Chris@17 778 $oldFirstToken = $firstToken;
Chris@17 779
Chris@17 780 if (substr($pattern, $i, 3) === '...') {
Chris@17 781 // It's a skip pattern. The skip pattern requires the
Chris@17 782 // content of the token in the "from" position and the token
Chris@17 783 // to skip to.
Chris@17 784 $specialPattern = $this->createSkipPattern($pattern, ($i - 1));
Chris@17 785 $lastToken = ($i - $firstToken);
Chris@17 786 $firstToken = ($i + 3);
Chris@17 787 $i += 2;
Chris@17 788
Chris@17 789 if ($specialPattern['to'] !== 'unknown') {
Chris@17 790 $firstToken++;
Chris@17 791 }
Chris@17 792 } else if (substr($pattern, $i, 3) === 'abc') {
Chris@17 793 $specialPattern = ['type' => 'string'];
Chris@17 794 $lastToken = ($i - $firstToken);
Chris@17 795 $firstToken = ($i + 3);
Chris@17 796 $i += 2;
Chris@17 797 } else if (substr($pattern, $i, 3) === 'EOL') {
Chris@17 798 $specialPattern = ['type' => 'newline'];
Chris@17 799 $lastToken = ($i - $firstToken);
Chris@17 800 $firstToken = ($i + 3);
Chris@17 801 $i += 2;
Chris@17 802 }//end if
Chris@17 803
Chris@17 804 if ($specialPattern !== false || $isLastChar === true) {
Chris@17 805 // If we are at the end of the string, don't worry about a limit.
Chris@17 806 if ($isLastChar === true) {
Chris@17 807 // Get the string from the end of the last skip pattern, if any,
Chris@17 808 // to the end of the pattern string.
Chris@17 809 $str = substr($pattern, $oldFirstToken);
Chris@17 810 } else {
Chris@17 811 // Get the string from the end of the last special pattern,
Chris@17 812 // if any, to the start of this special pattern.
Chris@17 813 if ($lastToken === 0) {
Chris@17 814 // Note that if the last special token was zero characters ago,
Chris@17 815 // there will be nothing to process so we can skip this bit.
Chris@17 816 // This happens if you have something like: EOL... in your pattern.
Chris@17 817 $str = '';
Chris@17 818 } else {
Chris@17 819 $str = substr($pattern, $oldFirstToken, $lastToken);
Chris@17 820 }
Chris@17 821 }
Chris@17 822
Chris@17 823 if ($str !== '') {
Chris@17 824 $tokenPatterns = $this->createTokenPattern($str);
Chris@17 825 foreach ($tokenPatterns as $tokenPattern) {
Chris@17 826 $patterns[] = $tokenPattern;
Chris@17 827 }
Chris@17 828 }
Chris@17 829
Chris@17 830 // Make sure we don't skip the last token.
Chris@17 831 if ($isLastChar === false && $i === ($length - 1)) {
Chris@17 832 $i--;
Chris@17 833 }
Chris@17 834 }//end if
Chris@17 835
Chris@17 836 // Add the skip pattern *after* we have processed
Chris@17 837 // all the tokens from the end of the last skip pattern
Chris@17 838 // to the start of this skip pattern.
Chris@17 839 if ($specialPattern !== false) {
Chris@17 840 $patterns[] = $specialPattern;
Chris@17 841 }
Chris@17 842 }//end for
Chris@17 843
Chris@17 844 return $patterns;
Chris@17 845
Chris@17 846 }//end parse()
Chris@17 847
Chris@17 848
Chris@17 849 /**
Chris@17 850 * Creates a skip pattern.
Chris@17 851 *
Chris@17 852 * @param string $pattern The pattern being parsed.
Chris@17 853 * @param string $from The token content that the skip pattern starts from.
Chris@17 854 *
Chris@17 855 * @return array The pattern step.
Chris@17 856 * @see createTokenPattern()
Chris@17 857 * @see parse()
Chris@17 858 */
Chris@17 859 private function createSkipPattern($pattern, $from)
Chris@17 860 {
Chris@17 861 $skip = ['type' => 'skip'];
Chris@17 862
Chris@17 863 $nestedParenthesis = 0;
Chris@17 864 $nestedBraces = 0;
Chris@17 865 for ($start = $from; $start >= 0; $start--) {
Chris@17 866 switch ($pattern[$start]) {
Chris@17 867 case '(':
Chris@17 868 if ($nestedParenthesis === 0) {
Chris@17 869 $skip['to'] = 'parenthesis_closer';
Chris@17 870 }
Chris@17 871
Chris@17 872 $nestedParenthesis--;
Chris@17 873 break;
Chris@17 874 case '{':
Chris@17 875 if ($nestedBraces === 0) {
Chris@17 876 $skip['to'] = 'scope_closer';
Chris@17 877 }
Chris@17 878
Chris@17 879 $nestedBraces--;
Chris@17 880 break;
Chris@17 881 case '}':
Chris@17 882 $nestedBraces++;
Chris@17 883 break;
Chris@17 884 case ')':
Chris@17 885 $nestedParenthesis++;
Chris@17 886 break;
Chris@17 887 }//end switch
Chris@17 888
Chris@17 889 if (isset($skip['to']) === true) {
Chris@17 890 break;
Chris@17 891 }
Chris@17 892 }//end for
Chris@17 893
Chris@17 894 if (isset($skip['to']) === false) {
Chris@17 895 $skip['to'] = 'unknown';
Chris@17 896 }
Chris@17 897
Chris@17 898 return $skip;
Chris@17 899
Chris@17 900 }//end createSkipPattern()
Chris@17 901
Chris@17 902
Chris@17 903 /**
Chris@17 904 * Creates a token pattern.
Chris@17 905 *
Chris@17 906 * @param string $str The tokens string that the pattern should match.
Chris@17 907 *
Chris@17 908 * @return array The pattern step.
Chris@17 909 * @see createSkipPattern()
Chris@17 910 * @see parse()
Chris@17 911 */
Chris@17 912 private function createTokenPattern($str)
Chris@17 913 {
Chris@17 914 // Don't add a space after the closing php tag as it will add a new
Chris@17 915 // whitespace token.
Chris@17 916 $tokenizer = new PHP('<?php '.$str.'?>', null);
Chris@17 917
Chris@17 918 // Remove the <?php tag from the front and the end php tag from the back.
Chris@17 919 $tokens = $tokenizer->getTokens();
Chris@17 920 $tokens = array_slice($tokens, 1, (count($tokens) - 2));
Chris@17 921
Chris@17 922 $patterns = [];
Chris@17 923 foreach ($tokens as $patternInfo) {
Chris@17 924 $patterns[] = [
Chris@17 925 'type' => 'token',
Chris@17 926 'token' => $patternInfo['code'],
Chris@17 927 'value' => $patternInfo['content'],
Chris@17 928 ];
Chris@17 929 }
Chris@17 930
Chris@17 931 return $patterns;
Chris@17 932
Chris@17 933 }//end createTokenPattern()
Chris@17 934
Chris@17 935
Chris@17 936 }//end class