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