PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / extra / php-codesniffer-php-token / Tokenizers / Tokenizer.php

Tokenizer.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.2, at src/extra/php-codesniffer-php-token/Tokenizers/Tokenizer.php

1,674 lines 56.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The base tokenizer class.
4 *
5 * @author Greg Sherwood <[email protected]>
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\Tokenizers;
11
12 use PHP_CodeSniffer\Exceptions\TokenizerException;
13 use PHP_CodeSniffer\Util;
14
15 abstract class Tokenizer {
16
17 /**
18 * The config data for the run.
19 *
20 * @var \PHP_CodeSniffer\Config
21 */
22 protected $config = null;
23
24 /**
25 * The EOL char used in the content.
26 *
27 * @var string
28 */
29 protected $eolChar = [];
30
31 /**
32 * A token-based representation of the content.
33 *
34 * @var array
35 */
36 protected $tokens = [];
37
38 /**
39 * The number of tokens in the tokens array.
40 *
41 * @var integer
42 */
43 protected $numTokens = 0;
44
45 /**
46 * A list of tokens that are allowed to open a scope.
47 *
48 * @var array
49 */
50 public $scopeOpeners = [];
51
52 /**
53 * A list of tokens that end the scope.
54 *
55 * @var array
56 */
57 public $endScopeTokens = [];
58
59 /**
60 * Known lengths of tokens.
61 *
62 * @var array<int, int>
63 */
64 public $knownLengths = [];
65
66 /**
67 * A list of lines being ignored due to error suppression comments.
68 *
69 * @var array
70 */
71 public $ignoredLines = [];
72
73
74 /**
75 * Initialise and run the tokenizer.
76 *
77 * @param string $content The content to tokenize,
78 * @param \PHP_CodeSniffer\Config | null $config The config data for the run.
79 * @param string $eolChar The EOL char used in the content.
80 *
81 * @return void
82 * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the file appears to be minified.
83 */
84 public function __construct( $content, $config, $eolChar = '\n' ) {
85 $this->eolChar = $eolChar;
86
87 $this->config = $config;
88 $this->tokens = $this->tokenize( $content );
89
90 if ( $config === null ) {
91 return;
92 }
93
94 $this->createPositionMap();
95 $this->createTokenMap();
96 $this->createParenthesisNestingMap();
97 $this->createScopeMap();
98 $this->createLevelMap();
99
100 // Allow the tokenizer to do additional processing if required.
101 $this->processAdditional();
102
103 }//end __construct()
104
105
106 /**
107 * Checks the content to see if it looks minified.
108 *
109 * @param string $content The content to tokenize.
110 * @param string $eolChar The EOL char used in the content.
111 *
112 * @return boolean
113 */
114 protected function isMinifiedContent( $content, $eolChar = '\n' ) {
115 // Minified files often have a very large number of characters per line
116 // and cause issues when tokenizing.
117 $numChars = strlen( $content );
118 $numLines = ( substr_count( $content, $eolChar ) + 1 );
119 $average = ( $numChars / $numLines );
120 if ( $average > 100 ) {
121 return true;
122 }
123
124 return false;
125
126 }//end isMinifiedContent()
127
128
129 /**
130 * Gets the array of tokens.
131 *
132 * @return array
133 */
134 public function getTokens() {
135 return $this->tokens;
136
137 }//end getTokens()
138
139
140 /**
141 * Creates an array of tokens when given some content.
142 *
143 * @param string $string The string to tokenize.
144 *
145 * @return array
146 */
147 abstract protected function tokenize( $string );
148
149
150 /**
151 * Performs additional processing after main tokenizing.
152 *
153 * @return void
154 */
155 abstract protected function processAdditional();
156
157
158 /**
159 * Sets token position information.
160 *
161 * Can also convert tabs into spaces. Each tab can represent between
162 * 1 and $width spaces, so this cannot be a straight string replace.
163 *
164 * @return void
165 */
166 private function createPositionMap() {
167 $currColumn = 1;
168 $lineNumber = 1;
169 $eolLen = strlen( $this->eolChar );
170 $ignoring = null;
171 $inTests = defined( 'PHP_CODESNIFFER_IN_TESTS' );
172
173 $checkEncoding = false;
174 if ( function_exists( 'iconv_strlen' ) === true ) {
175 $checkEncoding = true;
176 }
177
178 $checkAnnotations = $this->config->annotations;
179 $encoding = $this->config->encoding;
180 $tabWidth = $this->config->tabWidth;
181
182 $tokensWithTabs = [
183 T_WHITESPACE => true,
184 T_COMMENT => true,
185 T_DOC_COMMENT => true,
186 T_DOC_COMMENT_WHITESPACE => true,
187 T_DOC_COMMENT_STRING => true,
188 T_CONSTANT_ENCAPSED_STRING => true,
189 T_DOUBLE_QUOTED_STRING => true,
190 T_HEREDOC => true,
191 T_NOWDOC => true,
192 T_INLINE_HTML => true,
193 ];
194
195 $this->numTokens = count( $this->tokens );
196 for ( $i = 0; $i < $this->numTokens; $i ++ ) {
197 $this->tokens[ $i ]['line'] = $lineNumber;
198 $this->tokens[ $i ]['column'] = $currColumn;
199
200 if ( isset( $this->knownLengths[ $this->tokens[ $i ]['code'] ] ) === true ) {
201 // There are no tabs in the tokens we know the length of.
202 $length = $this->knownLengths[ $this->tokens[ $i ]['code'] ];
203 $currColumn += $length;
204 } else if ( $tabWidth === 0
205 || isset( $tokensWithTabs[ $this->tokens[ $i ]['code'] ] ) === false
206 || strpos( $this->tokens[ $i ]['content'], "\t" ) === false
207 ) {
208 // There are no tabs in this content, or we aren't replacing them.
209 if ( $checkEncoding === true ) {
210 // Not using the default encoding, so take a bit more care.
211 $oldLevel = error_reporting();
212 error_reporting( 0 );
213 $length = iconv_strlen( $this->tokens[ $i ]['content'], $encoding );
214 error_reporting( $oldLevel );
215
216 if ( $length === false ) {
217 // String contained invalid characters, so revert to default.
218 $length = strlen( $this->tokens[ $i ]['content'] );
219 }
220 } else {
221 $length = strlen( $this->tokens[ $i ]['content'] );
222 }
223
224 $currColumn += $length;
225 } else {
226 $this->replaceTabsInToken( $this->tokens[ $i ] );
227 $length = $this->tokens[ $i ]['length'];
228 $currColumn += $length;
229 }//end if
230
231 $this->tokens[ $i ]['length'] = $length;
232
233 if ( isset( $this->knownLengths[ $this->tokens[ $i ]['code'] ] ) === false
234 && strpos( $this->tokens[ $i ]['content'], $this->eolChar ) !== false
235 ) {
236 $lineNumber ++;
237 $currColumn = 1;
238
239 // Newline chars are not counted in the token length.
240 $this->tokens[ $i ]['length'] -= $eolLen;
241 }
242
243 if ( $this->tokens[ $i ]['code'] === T_COMMENT
244 || $this->tokens[ $i ]['code'] === T_DOC_COMMENT_STRING
245 || $this->tokens[ $i ]['code'] === T_DOC_COMMENT_TAG
246 || ( $inTests === true && $this->tokens[ $i ]['code'] === T_INLINE_HTML )
247 ) {
248 $commentText = ltrim( $this->tokens[ $i ]['content'], " \t/*" );
249 $commentText = rtrim( $commentText, " */\t\r\n" );
250 $commentTextLower = strtolower( $commentText );
251 if ( strpos( $commentText, '@codingStandards' ) !== false ) {
252 // If this comment is the only thing on the line, it tells us
253 // to ignore the following line. If the line contains other content
254 // then we are just ignoring this one single line.
255 $ownLine = false;
256 if ( $i > 0 ) {
257 for ( $prev = ( $i - 1 ); $prev >= 0; $prev -- ) {
258 if ( $this->tokens[ $prev ]['code'] === T_WHITESPACE ) {
259 continue;
260 }
261
262 break;
263 }
264
265 if ( $this->tokens[ $prev ]['line'] !== $this->tokens[ $i ]['line'] ) {
266 $ownLine = true;
267 }
268 }
269
270 if ( $ignoring === null
271 && strpos( $commentText, '@codingStandardsIgnoreStart' ) !== false
272 ) {
273 $ignoring = [ '.all' => true ];
274 if ( $ownLine === true ) {
275 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
276 }
277 } else if ( $ignoring !== null
278 && strpos( $commentText, '@codingStandardsIgnoreEnd' ) !== false
279 ) {
280 if ( $ownLine === true ) {
281 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
282 } else {
283 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
284 }
285
286 $ignoring = null;
287 } else if ( $ignoring === null
288 && strpos( $commentText, '@codingStandardsIgnoreLine' ) !== false
289 ) {
290 $ignoring = [ '.all' => true ];
291 if ( $ownLine === true ) {
292 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
293 $this->ignoredLines[ ( $this->tokens[ $i ]['line'] + 1 ) ] = $ignoring;
294 } else {
295 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
296 }
297
298 $ignoring = null;
299 }//end if
300 } else if ( substr( $commentTextLower, 0, 6 ) === 'phpcs:'
301 || substr( $commentTextLower, 0, 7 ) === '@phpcs:'
302 ) {
303 // If the @phpcs: syntax is being used, strip the @ to make
304 // comparisons easier.
305 if ( $commentText[0] === '@' ) {
306 $commentText = substr( $commentText, 1 );
307 $commentTextLower = strtolower( $commentText );
308 }
309
310 // If there is a comment on the end, strip it off.
311 $commentStart = strpos( $commentTextLower, ' --' );
312 if ( $commentStart !== false ) {
313 $commentText = substr( $commentText, 0, $commentStart );
314 $commentTextLower = strtolower( $commentText );
315 }
316
317 // If this comment is the only thing on the line, it tells us
318 // to ignore the following line. If the line contains other content
319 // then we are just ignoring this one single line.
320 $lineHasOtherContent = false;
321 $lineHasOtherTokens = false;
322 if ( $i > 0 ) {
323 for ( $prev = ( $i - 1 ); $prev > 0; $prev -- ) {
324 if ( $this->tokens[ $prev ]['line'] !== $this->tokens[ $i ]['line'] ) {
325 // Changed lines.
326 break;
327 }
328
329 if ( $this->tokens[ $prev ]['code'] === T_WHITESPACE
330 || ( $this->tokens[ $prev ]['code'] === T_INLINE_HTML
331 && trim( $this->tokens[ $prev ]['content'] ) === '' )
332 ) {
333 continue;
334 }
335
336 $lineHasOtherTokens = true;
337
338 if ( $this->tokens[ $prev ]['code'] === T_OPEN_TAG ) {
339 continue;
340 }
341
342 $lineHasOtherContent = true;
343 break;
344 }//end for
345
346 $changedLines = false;
347 for ( $next = $i; $next < $this->numTokens; $next ++ ) {
348 if ( $changedLines === true ) {
349 // Changed lines.
350 break;
351 }
352
353 if ( isset( $this->knownLengths[ $this->tokens[ $next ]['code'] ] ) === false
354 && strpos( $this->tokens[ $next ]['content'], $this->eolChar ) !== false
355 ) {
356 // Last token on the current line.
357 $changedLines = true;
358 }
359
360 if ( $next === $i ) {
361 continue;
362 }
363
364 if ( $this->tokens[ $next ]['code'] === T_WHITESPACE
365 || ( $this->tokens[ $next ]['code'] === T_INLINE_HTML
366 && trim( $this->tokens[ $next ]['content'] ) === '' )
367 ) {
368 continue;
369 }
370
371 $lineHasOtherTokens = true;
372
373 if ( $this->tokens[ $next ]['code'] === T_CLOSE_TAG ) {
374 continue;
375 }
376
377 $lineHasOtherContent = true;
378 break;
379 }//end for
380 }//end if
381
382 if ( substr( $commentTextLower, 0, 9 ) === 'phpcs:set' ) {
383 // Ignore standards for complete lines that change sniff settings.
384 if ( $lineHasOtherTokens === false ) {
385 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
386 }
387
388 // Need to maintain case here, to get the correct sniff code.
389 $parts = explode( ' ', substr( $commentText, 10 ) );
390 if ( count( $parts ) >= 2 ) {
391 $sniffParts = explode( '.', $parts[0] );
392 if ( count( $sniffParts ) >= 3 ) {
393 $this->tokens[ $i ]['sniffCode'] = array_shift( $parts );
394 $this->tokens[ $i ]['sniffProperty'] = array_shift( $parts );
395 $this->tokens[ $i ]['sniffPropertyValue'] = rtrim( implode( ' ', $parts ), " */\r\n" );
396 }
397 }
398
399 $this->tokens[ $i ]['code'] = T_PHPCS_SET;
400 $this->tokens[ $i ]['type'] = 'T_PHPCS_SET';
401 } else if ( substr( $commentTextLower, 0, 16 ) === 'phpcs:ignorefile' ) {
402 // The whole file will be ignored, but at least set the correct token.
403 $this->tokens[ $i ]['code'] = T_PHPCS_IGNORE_FILE;
404 $this->tokens[ $i ]['type'] = 'T_PHPCS_IGNORE_FILE';
405 } else if ( substr( $commentTextLower, 0, 13 ) === 'phpcs:disable' ) {
406 if ( $lineHasOtherContent === false ) {
407 // Completely ignore the comment line.
408 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
409 }
410
411 if ( $ignoring === null ) {
412 $ignoring = [];
413 }
414
415 $disabledSniffs = [];
416
417 $additionalText = substr( $commentText, 14 );
418 if ( $additionalText === false ) {
419 $ignoring = [ '.all' => true ];
420 } else {
421 $parts = explode( ',', substr( $commentText, 13 ) );
422 foreach ( $parts as $sniffCode ) {
423 $sniffCode = trim( $sniffCode );
424 $disabledSniffs[ $sniffCode ] = true;
425 $ignoring[ $sniffCode ] = true;
426
427 // This newly disabled sniff might be disabling an existing
428 // enabled exception that we are tracking.
429 if ( isset( $ignoring['.except'] ) === true ) {
430 foreach ( array_keys( $ignoring['.except'] ) as $ignoredSniffCode ) {
431 if ( $ignoredSniffCode === $sniffCode
432 || strpos( $ignoredSniffCode, $sniffCode . '.' ) === 0
433 ) {
434 unset( $ignoring['.except'][ $ignoredSniffCode ] );
435 }
436 }
437
438 if ( empty( $ignoring['.except'] ) === true ) {
439 unset( $ignoring['.except'] );
440 }
441 }
442 }//end foreach
443 }//end if
444
445 $this->tokens[ $i ]['code'] = T_PHPCS_DISABLE;
446 $this->tokens[ $i ]['type'] = 'T_PHPCS_DISABLE';
447 $this->tokens[ $i ]['sniffCodes'] = $disabledSniffs;
448 } else if ( substr( $commentTextLower, 0, 12 ) === 'phpcs:enable' ) {
449 if ( $ignoring !== null ) {
450 $enabledSniffs = [];
451
452 $additionalText = substr( $commentText, 13 );
453 if ( $additionalText === false ) {
454 $ignoring = null;
455 } else {
456 $parts = explode( ',', substr( $commentText, 13 ) );
457 foreach ( $parts as $sniffCode ) {
458 $sniffCode = trim( $sniffCode );
459 $enabledSniffs[ $sniffCode ] = true;
460
461 // This new enabled sniff might remove previously disabled
462 // sniffs if it is actually a standard or category of sniffs.
463 foreach ( array_keys( $ignoring ) as $ignoredSniffCode ) {
464 if ( $ignoredSniffCode === $sniffCode
465 || strpos( $ignoredSniffCode, $sniffCode . '.' ) === 0
466 ) {
467 unset( $ignoring[ $ignoredSniffCode ] );
468 }
469 }
470
471 // This new enabled sniff might be able to clear up
472 // previously enabled sniffs if it is actually a standard or
473 // category of sniffs.
474 if ( isset( $ignoring['.except'] ) === true ) {
475 foreach ( array_keys( $ignoring['.except'] ) as $ignoredSniffCode ) {
476 if ( $ignoredSniffCode === $sniffCode
477 || strpos( $ignoredSniffCode, $sniffCode . '.' ) === 0
478 ) {
479 unset( $ignoring['.except'][ $ignoredSniffCode ] );
480 }
481 }
482 }
483 }//end foreach
484
485 if ( empty( $ignoring ) === true ) {
486 $ignoring = null;
487 } else {
488 if ( isset( $ignoring['.except'] ) === true ) {
489 $ignoring['.except'] += $enabledSniffs;
490 } else {
491 $ignoring['.except'] = $enabledSniffs;
492 }
493 }
494 }//end if
495
496 if ( $lineHasOtherContent === false ) {
497 // Completely ignore the comment line.
498 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
499 } else {
500 // The comment is on the same line as the code it is ignoring,
501 // so respect the new ignore rules.
502 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
503 }
504
505 $this->tokens[ $i ]['sniffCodes'] = $enabledSniffs;
506 }//end if
507
508 $this->tokens[ $i ]['code'] = T_PHPCS_ENABLE;
509 $this->tokens[ $i ]['type'] = 'T_PHPCS_ENABLE';
510 } else if ( substr( $commentTextLower, 0, 12 ) === 'phpcs:ignore' ) {
511 $ignoreRules = [];
512
513 $additionalText = substr( $commentText, 13 );
514 if ( $additionalText === false ) {
515 $ignoreRules = [ '.all' => true ];
516 } else {
517 $parts = explode( ',', substr( $commentText, 13 ) );
518 foreach ( $parts as $sniffCode ) {
519 $ignoreRules[ trim( $sniffCode ) ] = true;
520 }
521 }
522
523 $this->tokens[ $i ]['code'] = T_PHPCS_IGNORE;
524 $this->tokens[ $i ]['type'] = 'T_PHPCS_IGNORE';
525 $this->tokens[ $i ]['sniffCodes'] = $ignoreRules;
526
527 if ( $ignoring !== null ) {
528 $ignoreRules += $ignoring;
529 }
530
531 if ( $lineHasOtherContent === false ) {
532 // Completely ignore the comment line, and set the following
533 // line to include the ignore rules we've set.
534 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
535 $this->ignoredLines[ ( $this->tokens[ $i ]['line'] + 1 ) ] = $ignoreRules;
536 } else {
537 // The comment is on the same line as the code it is ignoring,
538 // so respect the ignore rules it set.
539 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoreRules;
540 }
541 }//end if
542 }//end if
543 }//end if
544
545 if ( $ignoring !== null && isset( $this->ignoredLines[ $this->tokens[ $i ]['line'] ] ) === false ) {
546 $this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
547 }
548 }//end for
549
550 // If annotations are being ignored, we clear out all the ignore rules
551 // but leave the annotations tokenized as normal.
552 if ( $checkAnnotations === false ) {
553 $this->ignoredLines = [];
554 }
555
556 }//end createPositionMap()
557
558
559 /**
560 * Replaces tabs in original token content with spaces.
561 *
562 * Each tab can represent between 1 and $config->tabWidth spaces,
563 * so this cannot be a straight string replace. The original content
564 * is placed into an orig_content index and the new token length is also
565 * set in the length index.
566 *
567 * @param array $token The token to replace tabs inside.
568 * @param string $prefix The character to use to represent the start of a tab.
569 * @param string $padding The character to use to represent the end of a tab.
570 * @param int $tabWidth The number of spaces each tab represents.
571 *
572 * @return void
573 */
574 public function replaceTabsInToken( &$token, $prefix = ' ', $padding = ' ', $tabWidth = null ) {
575 $checkEncoding = false;
576 if ( function_exists( 'iconv_strlen' ) === true ) {
577 $checkEncoding = true;
578 }
579
580 $currColumn = $token['column'];
581 if ( $tabWidth === null ) {
582 $tabWidth = $this->config->tabWidth;
583 if ( $tabWidth === 0 ) {
584 $tabWidth = 1;
585 }
586 }
587
588 if ( rtrim( $token['content'], "\t" ) === '' ) {
589 // String only contains tabs, so we can shortcut the process.
590 $numTabs = strlen( $token['content'] );
591
592 $firstTabSize = ( $tabWidth - ( ( $currColumn - 1 ) % $tabWidth ) );
593 $length = ( $firstTabSize + ( $tabWidth * ( $numTabs - 1 ) ) );
594 $newContent = $prefix . str_repeat( $padding, ( $length - 1 ) );
595 } else {
596 // We need to determine the length of each tab.
597 $tabs = explode( "\t", $token['content'] );
598
599 $numTabs = ( count( $tabs ) - 1 );
600 $tabNum = 0;
601 $newContent = '';
602 $length = 0;
603
604 foreach ( $tabs as $content ) {
605 if ( $content !== '' ) {
606 $newContent .= $content;
607 if ( $checkEncoding === true ) {
608 // Not using the default encoding, so take a bit more care.
609 $oldLevel = error_reporting();
610 error_reporting( 0 );
611 $contentLength = iconv_strlen( $content, $this->config->encoding );
612 error_reporting( $oldLevel );
613 if ( $contentLength === false ) {
614 // String contained invalid characters, so revert to default.
615 $contentLength = strlen( $content );
616 }
617 } else {
618 $contentLength = strlen( $content );
619 }
620
621 $currColumn += $contentLength;
622 $length += $contentLength;
623 }
624
625 // The last piece of content does not have a tab after it.
626 if ( $tabNum === $numTabs ) {
627 break;
628 }
629
630 // Process the tab that comes after the content.
631 $lastCurrColumn = $currColumn;
632 $tabNum ++;
633
634 // Move the pointer to the next tab stop.
635 if ( ( $currColumn % $tabWidth ) === 0 ) {
636 // This is the first tab, and we are already at a
637 // tab stop, so this tab counts as a single space.
638 $currColumn ++;
639 } else {
640 $currColumn ++;
641 while ( ( $currColumn % $tabWidth ) !== 0 ) {
642 $currColumn ++;
643 }
644
645 $currColumn ++;
646 }
647
648 $length += ( $currColumn - $lastCurrColumn );
649 $newContent .= $prefix . str_repeat( $padding, ( $currColumn - $lastCurrColumn - 1 ) );
650 }//end foreach
651 }//end if
652
653 $token['orig_content'] = $token['content'];
654 $token['content'] = $newContent;
655 $token['length'] = $length;
656
657 }//end replaceTabsInToken()
658
659
660 /**
661 * Creates a map of brackets positions.
662 *
663 * @return void
664 */
665 private function createTokenMap() {
666 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
667 echo "\t*** START TOKEN MAP ***" . PHP_EOL;
668 }
669
670 $squareOpeners = [];
671 $curlyOpeners = [];
672 $this->numTokens = count( $this->tokens );
673
674 $openers = [];
675 $openOwner = null;
676
677 for ( $i = 0; $i < $this->numTokens; $i ++ ) {
678 /*
679 Parenthesis mapping.
680 */
681
682 if ( isset( Util\Tokens::$parenthesisOpeners[ $this->tokens[ $i ]['code'] ] ) === true ) {
683 $this->tokens[ $i ]['parenthesis_opener'] = null;
684 $this->tokens[ $i ]['parenthesis_closer'] = null;
685 $this->tokens[ $i ]['parenthesis_owner'] = $i;
686 $openOwner = $i;
687
688 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
689 echo str_repeat( "\t", ( count( $openers ) + 1 ) );
690 echo "=> Found parenthesis owner at $i" . PHP_EOL;
691 }
692 } else if ( $this->tokens[ $i ]['code'] === T_OPEN_PARENTHESIS ) {
693 $openers[] = $i;
694 $this->tokens[ $i ]['parenthesis_opener'] = $i;
695 if ( $openOwner !== null ) {
696 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
697 echo str_repeat( "\t", count( $openers ) );
698 echo "=> Found parenthesis opener at $i for $openOwner" . PHP_EOL;
699 }
700
701 $this->tokens[ $openOwner ]['parenthesis_opener'] = $i;
702 $this->tokens[ $i ]['parenthesis_owner'] = $openOwner;
703 $openOwner = null;
704 } else if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
705 echo str_repeat( "\t", count( $openers ) );
706 echo "=> Found unowned parenthesis opener at $i" . PHP_EOL;
707 }
708 } else if ( $this->tokens[ $i ]['code'] === T_CLOSE_PARENTHESIS ) {
709 // Did we set an owner for this set of parenthesis?
710 $numOpeners = count( $openers );
711 if ( $numOpeners !== 0 ) {
712 $opener = array_pop( $openers );
713 if ( isset( $this->tokens[ $opener ]['parenthesis_owner'] ) === true ) {
714 $owner = $this->tokens[ $opener ]['parenthesis_owner'];
715
716 $this->tokens[ $owner ]['parenthesis_closer'] = $i;
717 $this->tokens[ $i ]['parenthesis_owner'] = $owner;
718
719 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
720 echo str_repeat( "\t", ( count( $openers ) + 1 ) );
721 echo "=> Found parenthesis closer at $i for $owner" . PHP_EOL;
722 }
723 } else if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
724 echo str_repeat( "\t", ( count( $openers ) + 1 ) );
725 echo "=> Found unowned parenthesis closer at $i for $opener" . PHP_EOL;
726 }
727
728 $this->tokens[ $i ]['parenthesis_opener'] = $opener;
729 $this->tokens[ $i ]['parenthesis_closer'] = $i;
730 $this->tokens[ $opener ]['parenthesis_closer'] = $i;
731 }//end if
732 }//end if
733
734 /*
735 Bracket mapping.
736 */
737
738 switch ( $this->tokens[ $i ]['code'] ) {
739 case T_OPEN_SQUARE_BRACKET:
740 $squareOpeners[] = $i;
741
742 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
743 echo str_repeat( "\t", count( $squareOpeners ) );
744 echo str_repeat( "\t", count( $curlyOpeners ) );
745 echo "=> Found square bracket opener at $i" . PHP_EOL;
746 }
747 break;
748 case T_OPEN_CURLY_BRACKET:
749 if ( isset( $this->tokens[ $i ]['scope_closer'] ) === false ) {
750 $curlyOpeners[] = $i;
751
752 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
753 echo str_repeat( "\t", count( $squareOpeners ) );
754 echo str_repeat( "\t", count( $curlyOpeners ) );
755 echo "=> Found curly bracket opener at $i" . PHP_EOL;
756 }
757 }
758 break;
759 case T_CLOSE_SQUARE_BRACKET:
760 if ( empty( $squareOpeners ) === false ) {
761 $opener = array_pop( $squareOpeners );
762 $this->tokens[ $i ]['bracket_opener'] = $opener;
763 $this->tokens[ $i ]['bracket_closer'] = $i;
764 $this->tokens[ $opener ]['bracket_opener'] = $opener;
765 $this->tokens[ $opener ]['bracket_closer'] = $i;
766
767 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
768 echo str_repeat( "\t", count( $squareOpeners ) );
769 echo str_repeat( "\t", count( $curlyOpeners ) );
770 echo "\t=> Found square bracket closer at $i for $opener" . PHP_EOL;
771 }
772 }
773 break;
774 case T_CLOSE_CURLY_BRACKET:
775 if ( empty( $curlyOpeners ) === false
776 && isset( $this->tokens[ $i ]['scope_opener'] ) === false
777 ) {
778 $opener = array_pop( $curlyOpeners );
779 $this->tokens[ $i ]['bracket_opener'] = $opener;
780 $this->tokens[ $i ]['bracket_closer'] = $i;
781 $this->tokens[ $opener ]['bracket_opener'] = $opener;
782 $this->tokens[ $opener ]['bracket_closer'] = $i;
783
784 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
785 echo str_repeat( "\t", count( $squareOpeners ) );
786 echo str_repeat( "\t", count( $curlyOpeners ) );
787 echo "\t=> Found curly bracket closer at $i for $opener" . PHP_EOL;
788 }
789 }
790 break;
791 default:
792 continue 2;
793 }//end switch
794 }//end for
795
796 // Cleanup for any openers that we didn't find closers for.
797 // This typically means there was a syntax error breaking things.
798 foreach ( $openers as $opener ) {
799 unset( $this->tokens[ $opener ]['parenthesis_opener'] );
800 unset( $this->tokens[ $opener ]['parenthesis_owner'] );
801 }
802
803 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
804 echo "\t*** END TOKEN MAP ***" . PHP_EOL;
805 }
806
807 }//end createTokenMap()
808
809
810 /**
811 * Creates a map for the parenthesis tokens that surround other tokens.
812 *
813 * @return void
814 */
815 private function createParenthesisNestingMap() {
816 $map = [];
817 for ( $i = 0; $i < $this->numTokens; $i ++ ) {
818 if ( isset( $this->tokens[ $i ]['parenthesis_opener'] ) === true
819 && $i === $this->tokens[ $i ]['parenthesis_opener']
820 ) {
821 if ( empty( $map ) === false ) {
822 $this->tokens[ $i ]['nested_parenthesis'] = $map;
823 }
824
825 if ( isset( $this->tokens[ $i ]['parenthesis_closer'] ) === true ) {
826 $map[ $this->tokens[ $i ]['parenthesis_opener'] ]
827 = $this->tokens[ $i ]['parenthesis_closer'];
828 }
829 } else if ( isset( $this->tokens[ $i ]['parenthesis_closer'] ) === true
830 && $i === $this->tokens[ $i ]['parenthesis_closer']
831 ) {
832 array_pop( $map );
833 if ( empty( $map ) === false ) {
834 $this->tokens[ $i ]['nested_parenthesis'] = $map;
835 }
836 } else {
837 if ( empty( $map ) === false ) {
838 $this->tokens[ $i ]['nested_parenthesis'] = $map;
839 }
840 }//end if
841 }//end for
842
843 }//end createParenthesisNestingMap()
844
845
846 /**
847 * Creates a scope map of tokens that open scopes.
848 *
849 * @return void
850 * @see recurseScopeMap()
851 */
852 private function createScopeMap() {
853 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
854 echo "\t*** START SCOPE MAP ***" . PHP_EOL;
855 }
856
857 for ( $i = 0; $i < $this->numTokens; $i ++ ) {
858 // Check to see if the current token starts a new scope.
859 if ( isset( $this->scopeOpeners[ $this->tokens[ $i ]['code'] ] ) === true ) {
860 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
861 $type = $this->tokens[ $i ]['type'];
862 $content = Util\Common::prepareForOutput( $this->tokens[ $i ]['content'] );
863 echo "\tStart scope map at $i:$type => $content" . PHP_EOL;
864 }
865
866 if ( isset( $this->tokens[ $i ]['scope_condition'] ) === true ) {
867 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
868 echo "\t* already processed, skipping *" . PHP_EOL;
869 }
870
871 continue;
872 }
873
874 $i = $this->recurseScopeMap( $i );
875 }//end if
876 }//end for
877
878 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
879 echo "\t*** END SCOPE MAP ***" . PHP_EOL;
880 }
881
882 }//end createScopeMap()
883
884
885 /**
886 * Recurses though the scope openers to build a scope map.
887 *
888 * @param int $stackPtr The position in the stack of the token that
889 * opened the scope (eg. an IF token or FOR token).
890 * @param int $depth How many scope levels down we are.
891 * @param int $ignore How many curly braces we are ignoring.
892 *
893 * @return int The position in the stack that closed the scope.
894 * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the nesting level gets too deep.
895 */
896 private function recurseScopeMap( $stackPtr, $depth = 1, &$ignore = 0 ) {
897 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
898 echo str_repeat( "\t", $depth );
899 echo "=> Begin scope map recursion at token $stackPtr with depth $depth" . PHP_EOL;
900 }
901
902 $opener = null;
903 $currType = $this->tokens[ $stackPtr ]['code'];
904 $startLine = $this->tokens[ $stackPtr ]['line'];
905
906 // We will need this to restore the value if we end up
907 // returning a token ID that causes our calling function to go back
908 // over already ignored braces.
909 $originalIgnore = $ignore;
910
911 // If the start token for this scope opener is the same as
912 // the scope token, we have already found our opener.
913 if ( isset( $this->scopeOpeners[ $currType ]['start'][ $currType ] ) === true ) {
914 $opener = $stackPtr;
915 }
916
917 for ( $i = ( $stackPtr + 1 ); $i < $this->numTokens; $i ++ ) {
918 $tokenType = $this->tokens[ $i ]['code'];
919
920 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
921 $type = $this->tokens[ $i ]['type'];
922 $line = $this->tokens[ $i ]['line'];
923 $content = Util\Common::prepareForOutput( $this->tokens[ $i ]['content'] );
924
925 echo str_repeat( "\t", $depth );
926 echo "Process token $i on line $line [";
927 if ( $opener !== null ) {
928 echo "opener:$opener;";
929 }
930
931 if ( $ignore > 0 ) {
932 echo "ignore=$ignore;";
933 }
934
935 echo "]: $type => $content" . PHP_EOL;
936 }//end if
937
938 // Very special case for IF statements in PHP that can be defined without
939 // scope tokens. E.g., if (1) 1; 1 ? (1 ? 1 : 1) : 1;
940 // If an IF statement below this one has an opener but no
941 // keyword, the opener will be incorrectly assigned to this IF statement.
942 // The same case also applies to USE statements, which don't have to have
943 // openers, so a following USE statement can cause an incorrect brace match.
944 if ( ( $currType === T_IF || $currType === T_ELSE || $currType === T_USE )
945 && $opener === null
946 && ( $this->tokens[ $i ]['code'] === T_SEMICOLON
947 || $this->tokens[ $i ]['code'] === T_CLOSE_TAG )
948 ) {
949 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
950 $type = $this->tokens[ $stackPtr ]['type'];
951 echo str_repeat( "\t", $depth );
952 if ( $this->tokens[ $i ]['code'] === T_SEMICOLON ) {
953 $closerType = 'semicolon';
954 } else {
955 $closerType = 'close tag';
956 }
957
958 echo "=> Found $closerType before scope opener for $stackPtr:$type, bailing" . PHP_EOL;
959 }
960
961 return $i;
962 }
963
964 // Special case for PHP control structures that have no braces.
965 // If we find a curly brace closer before we find the opener,
966 // we're not going to find an opener. That closer probably belongs to
967 // a control structure higher up.
968 if ( $opener === null
969 && $ignore === 0
970 && $tokenType === T_CLOSE_CURLY_BRACKET
971 && isset( $this->scopeOpeners[ $currType ]['end'][ $tokenType ] ) === true
972 ) {
973 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
974 $type = $this->tokens[ $stackPtr ]['type'];
975 echo str_repeat( "\t", $depth );
976 echo "=> Found curly brace closer before scope opener for $stackPtr:$type, bailing" . PHP_EOL;
977 }
978
979 return ( $i - 1 );
980 }
981
982 if ( $opener !== null
983 && ( isset( $this->tokens[ $i ]['scope_opener'] ) === false
984 || $this->scopeOpeners[ $this->tokens[ $stackPtr ]['code'] ]['shared'] === true )
985 && isset( $this->scopeOpeners[ $currType ]['end'][ $tokenType ] ) === true
986 ) {
987 if ( $ignore > 0 && $tokenType === T_CLOSE_CURLY_BRACKET ) {
988 // The last opening bracket must have been for a string
989 // offset or alike, so let's ignore it.
990 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
991 echo str_repeat( "\t", $depth );
992 echo '* finished ignoring curly brace *' . PHP_EOL;
993 }
994
995 $ignore --;
996 continue;
997 } else if ( $this->tokens[ $opener ]['code'] === T_OPEN_CURLY_BRACKET
998 && $tokenType !== T_CLOSE_CURLY_BRACKET
999 ) {
1000 // The opener is a curly bracket so the closer must be a curly bracket as well.
1001 // We ignore this closer to handle cases such as T_ELSE or T_ELSEIF being considered
1002 // a closer of T_IF when it should not.
1003 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1004 $type = $this->tokens[ $stackPtr ]['type'];
1005 echo str_repeat( "\t", $depth );
1006 echo "=> Ignoring non-curly scope closer for $stackPtr:$type" . PHP_EOL;
1007 }
1008 } else {
1009 $scopeCloser = $i;
1010 $todo = [
1011 $stackPtr,
1012 $opener,
1013 ];
1014
1015 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1016 $type = $this->tokens[ $stackPtr ]['type'];
1017 $closerType = $this->tokens[ $scopeCloser ]['type'];
1018 echo str_repeat( "\t", $depth );
1019 echo "=> Found scope closer ($scopeCloser:$closerType) for $stackPtr:$type" . PHP_EOL;
1020 }
1021
1022 $validCloser = true;
1023 if ( ( $this->tokens[ $stackPtr ]['code'] === T_IF || $this->tokens[ $stackPtr ]['code'] === T_ELSEIF )
1024 && ( $tokenType === T_ELSE || $tokenType === T_ELSEIF )
1025 ) {
1026 // To be a closer, this token must have an opener.
1027 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1028 echo str_repeat( "\t", $depth );
1029 echo "* closer needs to be tested *" . PHP_EOL;
1030 }
1031
1032 $i = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );
1033
1034 if ( isset( $this->tokens[ $scopeCloser ]['scope_opener'] ) === false ) {
1035 $validCloser = false;
1036 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1037 echo str_repeat( "\t", $depth );
1038 echo "* closer is not valid (no opener found) *" . PHP_EOL;
1039 }
1040 } else if ( $this->tokens[ $this->tokens[ $scopeCloser ]['scope_opener'] ]['code'] !== $this->tokens[ $opener ]['code'] ) {
1041 $validCloser = false;
1042 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1043 echo str_repeat( "\t", $depth );
1044 $type = $this->tokens[ $this->tokens[ $scopeCloser ]['scope_opener'] ]['type'];
1045 $openerType = $this->tokens[ $opener ]['type'];
1046 echo "* closer is not valid (mismatched opener type; $type != $openerType) *" . PHP_EOL;
1047 }
1048 } else if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1049 echo str_repeat( "\t", $depth );
1050 echo "* closer was valid *" . PHP_EOL;
1051 }
1052 } else {
1053 // The closer was not processed, so we need to
1054 // complete that token as well.
1055 $todo[] = $scopeCloser;
1056 }//end if
1057
1058 if ( $validCloser === true ) {
1059 foreach ( $todo as $token ) {
1060 $this->tokens[ $token ]['scope_condition'] = $stackPtr;
1061 $this->tokens[ $token ]['scope_opener'] = $opener;
1062 $this->tokens[ $token ]['scope_closer'] = $scopeCloser;
1063 }
1064
1065 if ( $this->scopeOpeners[ $this->tokens[ $stackPtr ]['code'] ]['shared'] === true ) {
1066 // As we are going back to where we started originally, restore
1067 // the ignore value back to its original value.
1068 $ignore = $originalIgnore;
1069
1070 return $opener;
1071 } else if ( $scopeCloser === $i
1072 && isset( $this->scopeOpeners[ $tokenType ] ) === true
1073 ) {
1074 // Unset scope_condition here or else the token will appear to have
1075 // already been processed, and it will be skipped. Normally we want that,
1076 // but in this case, the token is both a closer and an opener, so
1077 // it needs to act like an opener. This is also why we return the
1078 // token before this one; so the closer has a chance to be processed
1079 // a second time, but as an opener.
1080 unset( $this->tokens[ $scopeCloser ]['scope_condition'] );
1081
1082 return ( $i - 1 );
1083 } else {
1084 return $i;
1085 }
1086 } else {
1087 continue;
1088 }//end if
1089 }//end if
1090 }//end if
1091
1092 // Is this an opening condition ?
1093 if ( isset( $this->scopeOpeners[ $tokenType ] ) === true ) {
1094 if ( $opener === null ) {
1095 if ( $tokenType === T_USE ) {
1096 // PHP use keywords are special because they can be
1097 // used as blocks but also inline in function definitions.
1098 // So if we find them nested inside another opener, just skip them.
1099 continue;
1100 }
1101
1102 if ( $tokenType === T_FUNCTION
1103 && $this->tokens[ $stackPtr ]['code'] !== T_FUNCTION
1104 ) {
1105 // Probably a closure, so process it manually.
1106 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1107 $type = $this->tokens[ $stackPtr ]['type'];
1108 echo str_repeat( "\t", $depth );
1109 echo "=> Found function before scope opener for $stackPtr:$type, processing manually" . PHP_EOL;
1110 }
1111
1112 if ( isset( $this->tokens[ $i ]['scope_closer'] ) === true ) {
1113 // We've already processed this closure.
1114 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1115 echo str_repeat( "\t", $depth );
1116 echo '* already processed, skipping *' . PHP_EOL;
1117 }
1118
1119 $i = $this->tokens[ $i ]['scope_closer'];
1120 continue;
1121 }
1122
1123 $i = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );
1124 continue;
1125 }//end if
1126
1127 if ( $tokenType === T_CLASS ) {
1128 // Probably an anonymous class inside another anonymous class,
1129 // so process it manually.
1130 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1131 $type = $this->tokens[ $stackPtr ]['type'];
1132 echo str_repeat( "\t", $depth );
1133 echo "=> Found class before scope opener for $stackPtr:$type, processing manually" . PHP_EOL;
1134 }
1135
1136 if ( isset( $this->tokens[ $i ]['scope_closer'] ) === true ) {
1137 // We've already processed this anon class.
1138 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1139 echo str_repeat( "\t", $depth );
1140 echo '* already processed, skipping *' . PHP_EOL;
1141 }
1142
1143 $i = $this->tokens[ $i ]['scope_closer'];
1144 continue;
1145 }
1146
1147 $i = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );
1148 continue;
1149 }//end if
1150
1151 // Found another opening condition but still haven't
1152 // found our opener, so we are never going to find one.
1153 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1154 $type = $this->tokens[ $stackPtr ]['type'];
1155 echo str_repeat( "\t", $depth );
1156 echo "=> Found new opening condition before scope opener for $stackPtr:$type, ";
1157 }
1158
1159 if ( ( $this->tokens[ $stackPtr ]['code'] === T_IF
1160 || $this->tokens[ $stackPtr ]['code'] === T_ELSEIF
1161 || $this->tokens[ $stackPtr ]['code'] === T_ELSE )
1162 && ( $this->tokens[ $i ]['code'] === T_ELSE
1163 || $this->tokens[ $i ]['code'] === T_ELSEIF )
1164 ) {
1165 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1166 echo "continuing" . PHP_EOL;
1167 }
1168
1169 return ( $i - 1 );
1170 } else {
1171 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1172 echo "backtracking" . PHP_EOL;
1173 }
1174
1175 return $stackPtr;
1176 }
1177 }//end if
1178
1179 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1180 echo str_repeat( "\t", $depth );
1181 echo '* token is an opening condition *' . PHP_EOL;
1182 }
1183
1184 $isShared = ( $this->scopeOpeners[ $tokenType ]['shared'] === true );
1185
1186 if ( isset( $this->tokens[ $i ]['scope_condition'] ) === true ) {
1187 // We've been here before.
1188 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1189 echo str_repeat( "\t", $depth );
1190 echo '* already processed, skipping *' . PHP_EOL;
1191 }
1192
1193 if ( $isShared === false
1194 && isset( $this->tokens[ $i ]['scope_closer'] ) === true
1195 ) {
1196 $i = $this->tokens[ $i ]['scope_closer'];
1197 }
1198
1199 continue;
1200 } else if ( $currType === $tokenType
1201 && $isShared === false
1202 && $opener === null
1203 ) {
1204 // We haven't yet found our opener, but we have found another
1205 // scope opener which is the same type as us, and we don't
1206 // share openers, so we will never find one.
1207 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1208 echo str_repeat( "\t", $depth );
1209 echo '* it was another token\'s opener, bailing *' . PHP_EOL;
1210 }
1211
1212 return $stackPtr;
1213 } else {
1214 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1215 echo str_repeat( "\t", $depth );
1216 echo '* searching for opener *' . PHP_EOL;
1217 }
1218
1219 if ( isset( $this->scopeOpeners[ $tokenType ]['end'][ T_CLOSE_CURLY_BRACKET ] ) === true ) {
1220 $oldIgnore = $ignore;
1221 $ignore = 0;
1222 }
1223
1224 // PHP has a max nesting level for functions. Stop before we hit that limit
1225 // because too many loops means we've run into trouble anyway.
1226 if ( $depth > 50 ) {
1227 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1228 echo str_repeat( "\t", $depth );
1229 echo '* reached maximum nesting level; aborting *' . PHP_EOL;
1230 }
1231
1232 throw new TokenizerException( 'Maximum nesting level reached; file could not be processed' );
1233 }
1234
1235 $oldDepth = $depth;
1236 if ( $isShared === true
1237 && isset( $this->scopeOpeners[ $tokenType ]['with'][ $currType ] ) === true
1238 ) {
1239 // Don't allow the depth to increment because this is
1240 // possibly not a true nesting if we are sharing our closer.
1241 // This can happen, for example, when a SWITCH has a large
1242 // number of CASE statements with the same shared BREAK.
1243 $depth --;
1244 }
1245
1246 $i = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );
1247 $depth = $oldDepth;
1248
1249 if ( isset( $this->scopeOpeners[ $tokenType ]['end'][ T_CLOSE_CURLY_BRACKET ] ) === true ) {
1250 $ignore = $oldIgnore;
1251 }
1252 }//end if
1253 }//end if
1254
1255 if ( isset( $this->scopeOpeners[ $currType ]['start'][ $tokenType ] ) === true
1256 && $opener === null
1257 ) {
1258 if ( $tokenType === T_OPEN_CURLY_BRACKET ) {
1259 if ( isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) === true
1260 && $i < $this->tokens[ $stackPtr ]['parenthesis_closer']
1261 ) {
1262 // We found a curly brace inside the condition of the
1263 // current scope opener, so it must be a string offset.
1264 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1265 echo str_repeat( "\t", $depth );
1266 echo '* ignoring curly brace inside condition *' . PHP_EOL;
1267 }
1268
1269 $ignore ++;
1270 } else {
1271 // Make sure this is actually an opener and not a
1272 // string offset (e.g., $var{0}).
1273 for ( $x = ( $i - 1 ); $x > 0; $x -- ) {
1274 if ( isset( Util\Tokens::$emptyTokens[ $this->tokens[ $x ]['code'] ] ) === true ) {
1275 continue;
1276 } else {
1277 // If the first non-whitespace/comment token looks like this
1278 // brace is a string offset, or this brace is mid-way through
1279 // a new statement, it isn't a scope opener.
1280 $disallowed = Util\Tokens::$assignmentTokens;
1281 $disallowed += [
1282 T_DOLLAR => true,
1283 T_VARIABLE => true,
1284 T_OBJECT_OPERATOR => true,
1285 T_COMMA => true,
1286 T_OPEN_PARENTHESIS => true,
1287 ];
1288
1289 if ( isset( $disallowed[ $this->tokens[ $x ]['code'] ] ) === true ) {
1290 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1291 echo str_repeat( "\t", $depth );
1292 echo '* ignoring curly brace *' . PHP_EOL;
1293 }
1294
1295 $ignore ++;
1296 }
1297
1298 break;
1299 }//end if
1300 }//end for
1301 }//end if
1302 }//end if
1303
1304 if ( $ignore === 0 || $tokenType !== T_OPEN_CURLY_BRACKET ) {
1305 // We found the opening scope token for $currType.
1306 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1307 $type = $this->tokens[ $stackPtr ]['type'];
1308 echo str_repeat( "\t", $depth );
1309 echo "=> Found scope opener for $stackPtr:$type" . PHP_EOL;
1310 }
1311
1312 $opener = $i;
1313 }
1314 } else if ( $tokenType === T_SEMICOLON
1315 && $opener === null
1316 && ( isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) === false
1317 || $i > $this->tokens[ $stackPtr ]['parenthesis_closer'] )
1318 ) {
1319 // Found the end of a statement but still haven't
1320 // found our opener, so we are never going to find one.
1321 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1322 $type = $this->tokens[ $stackPtr ]['type'];
1323 echo str_repeat( "\t", $depth );
1324 echo "=> Found end of statement before scope opener for $stackPtr:$type, continuing" . PHP_EOL;
1325 }
1326
1327 return ( $i - 1 );
1328 } else if ( $tokenType === T_OPEN_PARENTHESIS ) {
1329 if ( isset( $this->tokens[ $i ]['parenthesis_owner'] ) === true ) {
1330 $owner = $this->tokens[ $i ]['parenthesis_owner'];
1331 if ( isset( Util\Tokens::$scopeOpeners[ $this->tokens[ $owner ]['code'] ] ) === true
1332 && isset( $this->tokens[ $i ]['parenthesis_closer'] ) === true
1333 ) {
1334 // If we get into here, then we opened a parenthesis for
1335 // a scope (eg. an if or else if) so we need to update the
1336 // start of the line so that when we check to see
1337 // if the closing parenthesis is more than n lines away from
1338 // the statement, we check from the closing parenthesis.
1339 $startLine = $this->tokens[ $this->tokens[ $i ]['parenthesis_closer'] ]['line'];
1340 }
1341 }
1342 } else if ( $tokenType === T_OPEN_CURLY_BRACKET && $opener !== null ) {
1343 // We opened something that we don't have a scope opener for.
1344 // Examples of this are curly brackets for string offsets etc.
1345 // We want to ignore this so that we don't have an invalid scope
1346 // map.
1347 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1348 echo str_repeat( "\t", $depth );
1349 echo '* ignoring curly brace *' . PHP_EOL;
1350 }
1351
1352 $ignore ++;
1353 } else if ( $tokenType === T_CLOSE_CURLY_BRACKET && $ignore > 0 ) {
1354 // We found the end token for the opener we were ignoring.
1355 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1356 echo str_repeat( "\t", $depth );
1357 echo '* finished ignoring curly brace *' . PHP_EOL;
1358 }
1359
1360 $ignore --;
1361 } else if ( $opener === null
1362 && isset( $this->scopeOpeners[ $currType ] ) === true
1363 ) {
1364 // If we still haven't found the opener after 30 lines,
1365 // we're not going to find it, unless we know it requires
1366 // an opener (in which case we better keep looking) or the last
1367 // token was empty (in which case we'll just confirm there is
1368 // more code in this file and not just a big comment).
1369 if ( $this->tokens[ $i ]['line'] >= ( $startLine + 30 )
1370 && isset( Util\Tokens::$emptyTokens[ $this->tokens[ ( $i - 1 ) ]['code'] ] ) === false
1371 ) {
1372 if ( $this->scopeOpeners[ $currType ]['strict'] === true ) {
1373 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1374 $type = $this->tokens[ $stackPtr ]['type'];
1375 $lines = ( $this->tokens[ $i ]['line'] - $startLine );
1376 echo str_repeat( "\t", $depth );
1377 echo "=> Still looking for $stackPtr:$type scope opener after $lines lines" . PHP_EOL;
1378 }
1379 } else {
1380 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1381 $type = $this->tokens[ $stackPtr ]['type'];
1382 echo str_repeat( "\t", $depth );
1383 echo "=> Couldn't find scope opener for $stackPtr:$type, bailing" . PHP_EOL;
1384 }
1385
1386 return $stackPtr;
1387 }
1388 }
1389 } else if ( $opener !== null
1390 && $tokenType !== T_BREAK
1391 && isset( $this->endScopeTokens[ $tokenType ] ) === true
1392 ) {
1393 if ( isset( $this->tokens[ $i ]['scope_condition'] ) === false ) {
1394 if ( $ignore > 0 ) {
1395 // We found the end token for the opener we were ignoring.
1396 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1397 echo str_repeat( "\t", $depth );
1398 echo '* finished ignoring curly brace *' . PHP_EOL;
1399 }
1400
1401 $ignore --;
1402 } else {
1403 // We found a token that closes the scope but it doesn't
1404 // have a condition, so it belongs to another token and
1405 // our token doesn't have a closer, so pretend this is
1406 // the closer.
1407 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1408 $type = $this->tokens[ $stackPtr ]['type'];
1409 echo str_repeat( "\t", $depth );
1410 echo "=> Found (unexpected) scope closer for $stackPtr:$type" . PHP_EOL;
1411 }
1412
1413 foreach ( [ $stackPtr, $opener ] as $token ) {
1414 $this->tokens[ $token ]['scope_condition'] = $stackPtr;
1415 $this->tokens[ $token ]['scope_opener'] = $opener;
1416 $this->tokens[ $token ]['scope_closer'] = $i;
1417 }
1418
1419 return ( $i - 1 );
1420 }//end if
1421 }//end if
1422 }//end if
1423 }//end for
1424
1425 return $stackPtr;
1426
1427 }//end recurseScopeMap()
1428
1429
1430 /**
1431 * Constructs the level map.
1432 *
1433 * The level map adds a 'level' index to each token which indicates the
1434 * depth that a token within a set of scope blocks. It also adds a
1435 * 'conditions' index which is an array of the scope conditions that opened
1436 * each of the scopes - position 0 being the first scope opener.
1437 *
1438 * @return void
1439 */
1440 private function createLevelMap() {
1441 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1442 echo "\t*** START LEVEL MAP ***" . PHP_EOL;
1443 }
1444
1445 $this->numTokens = count( $this->tokens );
1446 $level = 0;
1447 $conditions = [];
1448 $lastOpener = null;
1449 $openers = [];
1450
1451 for ( $i = 0; $i < $this->numTokens; $i ++ ) {
1452 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1453 $type = $this->tokens[ $i ]['type'];
1454 $line = $this->tokens[ $i ]['line'];
1455 $len = $this->tokens[ $i ]['length'];
1456 $col = $this->tokens[ $i ]['column'];
1457
1458 $content = Util\Common::prepareForOutput( $this->tokens[ $i ]['content'] );
1459
1460 echo str_repeat( "\t", ( $level + 1 ) );
1461 echo "Process token $i on line $line [col:$col;len:$len;lvl:$level;";
1462 if ( empty( $conditions ) !== true ) {
1463 $conditionString = 'conds;';
1464 foreach ( $conditions as $condition ) {
1465 $conditionString .= Util\Tokens::tokenName( $condition ) . ',';
1466 }
1467
1468 echo rtrim( $conditionString, ',' ) . ';';
1469 }
1470
1471 echo "]: $type => $content" . PHP_EOL;
1472 }//end if
1473
1474 $this->tokens[ $i ]['level'] = $level;
1475 $this->tokens[ $i ]['conditions'] = $conditions;
1476
1477 if ( isset( $this->tokens[ $i ]['scope_condition'] ) === true ) {
1478 // Check to see if this token opened the scope.
1479 if ( $this->tokens[ $i ]['scope_opener'] === $i ) {
1480 $stackPtr = $this->tokens[ $i ]['scope_condition'];
1481 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1482 $type = $this->tokens[ $stackPtr ]['type'];
1483 echo str_repeat( "\t", ( $level + 1 ) );
1484 echo "=> Found scope opener for $stackPtr:$type" . PHP_EOL;
1485 }
1486
1487 $stackPtr = $this->tokens[ $i ]['scope_condition'];
1488
1489 // If we find a scope opener that has a shared closer,
1490 // then we need to go back over the condition map that we
1491 // just created and fix ourselves as we just added some
1492 // conditions where there was none. This happens for T_CASE
1493 // statements that are using the same break statement.
1494 if ( $lastOpener !== null && $this->tokens[ $lastOpener ]['scope_closer'] === $this->tokens[ $i ]['scope_closer'] ) {
1495 // This opener shares its closer with the previous opener,
1496 // but we still need to check if the two openers share their
1497 // closer with each other directly (like CASE and DEFAULT)
1498 // or if they are just sharing because one doesn't have a
1499 // closer (like CASE with no BREAK using a SWITCHes closer).
1500 $thisType = $this->tokens[ $this->tokens[ $i ]['scope_condition'] ]['code'];
1501 $opener = $this->tokens[ $lastOpener ]['scope_condition'];
1502
1503 $isShared = isset( $this->scopeOpeners[ $thisType ]['with'][ $this->tokens[ $opener ]['code'] ] );
1504
1505 reset( $this->scopeOpeners[ $thisType ]['end'] );
1506 reset( $this->scopeOpeners[ $this->tokens[ $opener ]['code'] ]['end'] );
1507 $sameEnd = ( current( $this->scopeOpeners[ $thisType ]['end'] ) === current( $this->scopeOpeners[ $this->tokens[ $opener ]['code'] ]['end'] ) );
1508
1509 if ( $isShared === true && $sameEnd === true ) {
1510 $badToken = $opener;
1511 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1512 $type = $this->tokens[ $badToken ]['type'];
1513 echo str_repeat( "\t", ( $level + 1 ) );
1514 echo "* shared closer, cleaning up $badToken:$type *" . PHP_EOL;
1515 }
1516
1517 for ( $x = $this->tokens[ $i ]['scope_condition']; $x <= $i; $x ++ ) {
1518 $oldConditions = $this->tokens[ $x ]['conditions'];
1519 $oldLevel = $this->tokens[ $x ]['level'];
1520 $this->tokens[ $x ]['level'] --;
1521 unset( $this->tokens[ $x ]['conditions'][ $badToken ] );
1522 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1523 $type = $this->tokens[ $x ]['type'];
1524 $oldConds = '';
1525 foreach ( $oldConditions as $condition ) {
1526 $oldConds .= Util\Tokens::tokenName( $condition ) . ',';
1527 }
1528
1529 $oldConds = rtrim( $oldConds, ',' );
1530
1531 $newConds = '';
1532 foreach ( $this->tokens[ $x ]['conditions'] as $condition ) {
1533 $newConds .= Util\Tokens::tokenName( $condition ) . ',';
1534 }
1535
1536 $newConds = rtrim( $newConds, ',' );
1537
1538 $newLevel = $this->tokens[ $x ]['level'];
1539 echo str_repeat( "\t", ( $level + 1 ) );
1540 echo "* cleaned $x:$type *" . PHP_EOL;
1541 echo str_repeat( "\t", ( $level + 2 ) );
1542 echo "=> level changed from $oldLevel to $newLevel" . PHP_EOL;
1543 echo str_repeat( "\t", ( $level + 2 ) );
1544 echo "=> conditions changed from $oldConds to $newConds" . PHP_EOL;
1545 }//end if
1546 }//end for
1547
1548 unset( $conditions[ $badToken ] );
1549 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1550 $type = $this->tokens[ $badToken ]['type'];
1551 echo str_repeat( "\t", ( $level + 1 ) );
1552 echo "* token $badToken:$type removed from conditions array *" . PHP_EOL;
1553 }
1554
1555 unset( $openers[ $lastOpener ] );
1556
1557 $level --;
1558 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1559 echo str_repeat( "\t", ( $level + 2 ) );
1560 echo '* level decreased *' . PHP_EOL;
1561 }
1562 }//end if
1563 }//end if
1564
1565 $level ++;
1566 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1567 echo str_repeat( "\t", ( $level + 1 ) );
1568 echo '* level increased *' . PHP_EOL;
1569 }
1570
1571 $conditions[ $stackPtr ] = $this->tokens[ $stackPtr ]['code'];
1572 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1573 $type = $this->tokens[ $stackPtr ]['type'];
1574 echo str_repeat( "\t", ( $level + 1 ) );
1575 echo "* token $stackPtr:$type added to conditions array *" . PHP_EOL;
1576 }
1577
1578 $lastOpener = $this->tokens[ $i ]['scope_opener'];
1579 if ( $lastOpener !== null ) {
1580 $openers[ $lastOpener ] = $lastOpener;
1581 }
1582 } else if ( $lastOpener !== null && $this->tokens[ $lastOpener ]['scope_closer'] === $i ) {
1583 foreach ( array_reverse( $openers ) as $opener ) {
1584 if ( $this->tokens[ $opener ]['scope_closer'] === $i ) {
1585 $oldOpener = array_pop( $openers );
1586 if ( empty( $openers ) === false ) {
1587 $lastOpener = array_pop( $openers );
1588 $openers[ $lastOpener ] = $lastOpener;
1589 } else {
1590 $lastOpener = null;
1591 }
1592
1593 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1594 $type = $this->tokens[ $oldOpener ]['type'];
1595 echo str_repeat( "\t", ( $level + 1 ) );
1596 echo "=> Found scope closer for $oldOpener:$type" . PHP_EOL;
1597 }
1598
1599 $oldCondition = array_pop( $conditions );
1600 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1601 echo str_repeat( "\t", ( $level + 1 ) );
1602 echo '* token ' . Util\Tokens::tokenName( $oldCondition ) . ' removed from conditions array *' . PHP_EOL;
1603 }
1604
1605 // Make sure this closer actually belongs to us.
1606 // Either the condition also has to think this is the
1607 // closer, or it has to allow sharing with us.
1608 $condition = $this->tokens[ $this->tokens[ $i ]['scope_condition'] ]['code'];
1609 if ( $condition !== $oldCondition ) {
1610 if ( isset( $this->scopeOpeners[ $oldCondition ]['with'][ $condition ] ) === false ) {
1611 $badToken = $this->tokens[ $oldOpener ]['scope_condition'];
1612
1613 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1614 $type = Util\Tokens::tokenName( $oldCondition );
1615 echo str_repeat( "\t", ( $level + 1 ) );
1616 echo "* scope closer was bad, cleaning up $badToken:$type *" . PHP_EOL;
1617 }
1618
1619 for ( $x = ( $oldOpener + 1 ); $x <= $i; $x ++ ) {
1620 $oldConditions = $this->tokens[ $x ]['conditions'];
1621 $oldLevel = $this->tokens[ $x ]['level'];
1622 $this->tokens[ $x ]['level'] --;
1623 unset( $this->tokens[ $x ]['conditions'][ $badToken ] );
1624 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1625 $type = $this->tokens[ $x ]['type'];
1626 $oldConds = '';
1627 foreach ( $oldConditions as $condition ) {
1628 $oldConds .= Util\Tokens::tokenName( $condition ) . ',';
1629 }
1630
1631 $oldConds = rtrim( $oldConds, ',' );
1632
1633 $newConds = '';
1634 foreach ( $this->tokens[ $x ]['conditions'] as $condition ) {
1635 $newConds .= Util\Tokens::tokenName( $condition ) . ',';
1636 }
1637
1638 $newConds = rtrim( $newConds, ',' );
1639
1640 $newLevel = $this->tokens[ $x ]['level'];
1641 echo str_repeat( "\t", ( $level + 1 ) );
1642 echo "* cleaned $x:$type *" . PHP_EOL;
1643 echo str_repeat( "\t", ( $level + 2 ) );
1644 echo "=> level changed from $oldLevel to $newLevel" . PHP_EOL;
1645 echo str_repeat( "\t", ( $level + 2 ) );
1646 echo "=> conditions changed from $oldConds to $newConds" . PHP_EOL;
1647 }//end if
1648 }//end for
1649 }//end if
1650 }//end if
1651
1652 $level --;
1653 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1654 echo str_repeat( "\t", ( $level + 2 ) );
1655 echo '* level decreased *' . PHP_EOL;
1656 }
1657
1658 $this->tokens[ $i ]['level'] = $level;
1659 $this->tokens[ $i ]['conditions'] = $conditions;
1660 }//end if
1661 }//end foreach
1662 }//end if
1663 }//end if
1664 }//end for
1665
1666 if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
1667 echo "\t*** END LEVEL MAP ***" . PHP_EOL;
1668 }
1669
1670 }//end createLevelMap()
1671
1672
1673 }//end class
1674