• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 17174734458

23 Aug 2025 11:06AM UTC coverage: 76.88% (-2.1%) from 78.934%
17174734458

push

github

jrfnl
TEMP/TESTING PHPUnit 6331

19187 of 24957 relevant lines covered (76.88%)

60.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.4
/src/Tokenizers/Tokenizer.php
1
<?php
2
/**
3
 * The base tokenizer class.
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/PHPCSStandards/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\Common;
14
use PHP_CodeSniffer\Util\IgnoreList;
15
use PHP_CodeSniffer\Util\Tokens;
16
use PHP_CodeSniffer\Util\Writers\StatusWriter;
17

18
abstract class Tokenizer
19
{
20

21
    /**
22
     * The config data for the run.
23
     *
24
     * @var \PHP_CodeSniffer\Config
25
     */
26
    protected $config = null;
27

28
    /**
29
     * The EOL char used in the content.
30
     *
31
     * @var string
32
     */
33
    protected $eolChar = '';
34

35
    /**
36
     * A token-based representation of the content.
37
     *
38
     * @var array
39
     */
40
    protected $tokens = [];
41

42
    /**
43
     * The number of tokens in the tokens array.
44
     *
45
     * @var integer
46
     */
47
    protected $numTokens = 0;
48

49
    /**
50
     * A list of tokens that are allowed to open a scope.
51
     *
52
     * @var array
53
     */
54
    public $scopeOpeners = [];
55

56
    /**
57
     * A list of tokens that end the scope.
58
     *
59
     * @var array<int|string, int|string>
60
     */
61
    public $endScopeTokens = [];
62

63
    /**
64
     * Known lengths of tokens.
65
     *
66
     * @var array<string|int, int>
67
     */
68
    public $knownLengths = [];
69

70
    /**
71
     * A list of lines being ignored due to error suppression comments.
72
     *
73
     * @var array
74
     */
75
    public $ignoredLines = [];
76

77

78
    /**
79
     * Initialise and run the tokenizer.
80
     *
81
     * @param string                       $content The content to tokenize.
82
     * @param \PHP_CodeSniffer\Config|null $config  The config data for the run.
83
     * @param string                       $eolChar The EOL char used in the content.
84
     *
85
     * @return void
86
     * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the file appears to be minified.
87
     */
88
    public function __construct($content, $config, $eolChar='\n')
×
89
    {
90
        $this->eolChar = $eolChar;
×
91

92
        $this->config = $config;
×
93
        $this->tokens = $this->tokenize($content);
×
94

95
        if ($config === null) {
×
96
            return;
×
97
        }
98

99
        $this->createPositionMap();
×
100
        $this->createTokenMap();
×
101
        $this->createParenthesisNestingMap();
×
102
        $this->createScopeMap();
×
103
        $this->createLevelMap();
×
104

105
        // Allow the tokenizer to do additional processing if required.
106
        $this->processAdditional();
×
107

108
    }//end __construct()
109

110

111
    /**
112
     * Checks the content to see if it looks minified.
113
     *
114
     * @param string $content The content to tokenize.
115
     * @param string $eolChar The EOL char used in the content.
116
     *
117
     * @return boolean
118
     */
119
    protected function isMinifiedContent($content, $eolChar='\n')
×
120
    {
121
        // Minified files often have a very large number of characters per line
122
        // and cause issues when tokenizing.
123
        $numChars = strlen($content);
×
124
        $numLines = (substr_count($content, $eolChar) + 1);
×
125
        $average  = ($numChars / $numLines);
×
126
        if ($average > 100) {
×
127
            return true;
×
128
        }
129

130
        return false;
×
131

132
    }//end isMinifiedContent()
133

134

135
    /**
136
     * Gets the array of tokens.
137
     *
138
     * @return array
139
     */
140
    public function getTokens()
×
141
    {
142
        return $this->tokens;
×
143

144
    }//end getTokens()
145

146

147
    /**
148
     * Creates an array of tokens when given some content.
149
     *
150
     * @param string $string The string to tokenize.
151
     *
152
     * @return array
153
     */
154
    abstract protected function tokenize($string);
155

156

157
    /**
158
     * Performs additional processing after main tokenizing.
159
     *
160
     * @return void
161
     */
162
    abstract protected function processAdditional();
163

164

165
    /**
166
     * Sets token position information.
167
     *
168
     * Can also convert tabs into spaces. Each tab can represent between
169
     * 1 and $width spaces, so this cannot be a straight string replace.
170
     *
171
     * @return void
172
     */
173
    private function createPositionMap()
260✔
174
    {
175
        $currColumn = 1;
260✔
176
        $lineNumber = 1;
260✔
177
        $eolLen     = strlen($this->eolChar);
260✔
178
        $ignoring   = null;
260✔
179
        $ignoreAll  = IgnoreList::getInstanceIgnoringAll();
260✔
180
        $inTests    = defined('PHP_CODESNIFFER_IN_TESTS');
260✔
181

182
        $checkEncoding = false;
260✔
183
        if (function_exists('iconv_strlen') === true) {
260✔
184
            $checkEncoding = true;
260✔
185
        }
186

187
        $checkAnnotations = $this->config->annotations;
260✔
188
        $encoding         = $this->config->encoding;
260✔
189
        $tabWidth         = $this->config->tabWidth;
260✔
190

191
        $tokensWithTabs = [
260✔
192
            T_WHITESPACE               => true,
260✔
193
            T_COMMENT                  => true,
260✔
194
            T_DOC_COMMENT              => true,
260✔
195
            T_DOC_COMMENT_WHITESPACE   => true,
260✔
196
            T_DOC_COMMENT_STRING       => true,
260✔
197
            T_CONSTANT_ENCAPSED_STRING => true,
260✔
198
            T_DOUBLE_QUOTED_STRING     => true,
260✔
199
            T_START_HEREDOC            => true,
260✔
200
            T_START_NOWDOC             => true,
260✔
201
            T_HEREDOC                  => true,
260✔
202
            T_NOWDOC                   => true,
260✔
203
            T_END_HEREDOC              => true,
260✔
204
            T_END_NOWDOC               => true,
260✔
205
            T_INLINE_HTML              => true,
260✔
206
            T_YIELD_FROM               => true,
260✔
207
        ];
260✔
208

209
        $this->numTokens = count($this->tokens);
260✔
210
        for ($i = 0; $i < $this->numTokens; $i++) {
260✔
211
            $this->tokens[$i]['line']   = $lineNumber;
260✔
212
            $this->tokens[$i]['column'] = $currColumn;
260✔
213

214
            if (isset($this->knownLengths[$this->tokens[$i]['code']]) === true) {
260✔
215
                // There are no tabs in the tokens we know the length of.
216
                $length      = $this->knownLengths[$this->tokens[$i]['code']];
250✔
217
                $currColumn += $length;
250✔
218
            } else if ($tabWidth === 0
260✔
219
                || isset($tokensWithTabs[$this->tokens[$i]['code']]) === false
32✔
220
                || strpos($this->tokens[$i]['content'], "\t") === false
260✔
221
            ) {
222
                // There are no tabs in this content, or we aren't replacing them.
223
                if ($checkEncoding === true) {
260✔
224
                    // Not using the default encoding, so take a bit more care.
225
                    $oldLevel = error_reporting();
260✔
226
                    error_reporting(0);
260✔
227
                    $length = iconv_strlen($this->tokens[$i]['content'], $encoding);
260✔
228
                    error_reporting($oldLevel);
260✔
229

230
                    if ($length === false) {
260✔
231
                        // String contained invalid characters, so revert to default.
232
                        $length = strlen($this->tokens[$i]['content']);
×
233
                    }
234
                } else {
235
                    $length = strlen($this->tokens[$i]['content']);
×
236
                }
237

238
                $currColumn += $length;
260✔
239
            } else {
240
                $this->replaceTabsInToken($this->tokens[$i]);
32✔
241
                $length      = $this->tokens[$i]['length'];
32✔
242
                $currColumn += $length;
32✔
243
            }//end if
244

245
            $this->tokens[$i]['length'] = $length;
260✔
246

247
            if (isset($this->knownLengths[$this->tokens[$i]['code']]) === false
260✔
248
                && strpos($this->tokens[$i]['content'], $this->eolChar) !== false
260✔
249
            ) {
250
                $lineNumber++;
260✔
251
                $currColumn = 1;
260✔
252

253
                // Newline chars are not counted in the token length.
254
                $this->tokens[$i]['length'] -= $eolLen;
260✔
255
            }
256

257
            if ($this->tokens[$i]['code'] === T_COMMENT
260✔
258
                || $this->tokens[$i]['code'] === T_DOC_COMMENT_STRING
260✔
259
                || $this->tokens[$i]['code'] === T_DOC_COMMENT_TAG
260✔
260
                || ($inTests === true && $this->tokens[$i]['code'] === T_INLINE_HTML)
260✔
261
            ) {
262
                $commentText      = ltrim($this->tokens[$i]['content'], " \t/*#");
252✔
263
                $commentText      = rtrim($commentText, " */\t\r\n");
252✔
264
                $commentTextLower = strtolower($commentText);
252✔
265
                if (substr($commentTextLower, 0, 6) === 'phpcs:'
252✔
266
                    || substr($commentTextLower, 0, 7) === '@phpcs:'
252✔
267
                ) {
268
                    // If the @phpcs: syntax is being used, strip the @ to make
269
                    // comparisons easier.
270
                    if ($commentText[0] === '@') {
186✔
271
                        $commentText      = substr($commentText, 1);
44✔
272
                        $commentTextLower = strtolower($commentText);
44✔
273
                    }
274

275
                    // If there is a comment on the end, strip it off.
276
                    $commentStart = strpos($commentTextLower, ' --');
186✔
277
                    if ($commentStart !== false) {
186✔
278
                        $commentText      = substr($commentText, 0, $commentStart);
8✔
279
                        $commentTextLower = strtolower($commentText);
8✔
280
                    }
281

282
                    // If this comment is the only thing on the line, it tells us
283
                    // to ignore the following line. If the line contains other content
284
                    // then we are just ignoring this one single line.
285
                    $lineHasOtherContent = false;
186✔
286
                    $lineHasOtherTokens  = false;
186✔
287
                    if ($i > 0) {
186✔
288
                        for ($prev = ($i - 1); $prev > 0; $prev--) {
186✔
289
                            if ($this->tokens[$prev]['line'] !== $this->tokens[$i]['line']) {
186✔
290
                                // Changed lines.
291
                                break;
164✔
292
                            }
293

294
                            if ($this->tokens[$prev]['code'] === T_WHITESPACE
86✔
295
                                || $this->tokens[$prev]['code'] === T_DOC_COMMENT_WHITESPACE
42✔
296
                                || ($this->tokens[$prev]['code'] === T_INLINE_HTML
86✔
297
                                && trim($this->tokens[$prev]['content']) === '')
86✔
298
                            ) {
299
                                continue;
86✔
300
                            }
301

302
                            $lineHasOtherTokens = true;
42✔
303

304
                            if ($this->tokens[$prev]['code'] === T_OPEN_TAG
42✔
305
                                || $this->tokens[$prev]['code'] === T_DOC_COMMENT_STAR
42✔
306
                            ) {
307
                                continue;
12✔
308
                            }
309

310
                            $lineHasOtherContent = true;
30✔
311
                            break;
30✔
312
                        }//end for
313

314
                        $changedLines = false;
186✔
315
                        for ($next = $i; $next < $this->numTokens; $next++) {
186✔
316
                            if ($changedLines === true) {
186✔
317
                                // Changed lines.
318
                                break;
170✔
319
                            }
320

321
                            if (isset($this->knownLengths[$this->tokens[$next]['code']]) === false
186✔
322
                                && strpos($this->tokens[$next]['content'], $this->eolChar) !== false
186✔
323
                            ) {
324
                                // Last token on the current line.
325
                                $changedLines = true;
170✔
326
                            }
327

328
                            if ($next === $i) {
186✔
329
                                continue;
186✔
330
                            }
331

332
                            if ($this->tokens[$next]['code'] === T_WHITESPACE
34✔
333
                                || $this->tokens[$next]['code'] === T_DOC_COMMENT_WHITESPACE
26✔
334
                                || ($this->tokens[$next]['code'] === T_INLINE_HTML
34✔
335
                                && trim($this->tokens[$next]['content']) === '')
34✔
336
                            ) {
337
                                continue;
22✔
338
                            }
339

340
                            $lineHasOtherTokens = true;
14✔
341

342
                            if ($this->tokens[$next]['code'] === T_CLOSE_TAG) {
14✔
343
                                continue;
×
344
                            }
345

346
                            $lineHasOtherContent = true;
14✔
347
                            break;
14✔
348
                        }//end for
349
                    }//end if
350

351
                    if (substr($commentTextLower, 0, 9) === 'phpcs:set') {
186✔
352
                        // Ignore standards for complete lines that change sniff settings.
353
                        if ($lineHasOtherTokens === false) {
×
354
                            $this->ignoredLines[$this->tokens[$i]['line']] = $ignoreAll;
×
355
                        }
356

357
                        // Need to maintain case here, to get the correct sniff code.
358
                        $parts = explode(' ', substr($commentText, 10));
×
359
                        if (count($parts) >= 2) {
×
360
                            $sniffParts = explode('.', $parts[0]);
×
361
                            if (count($sniffParts) >= 3) {
×
362
                                $this->tokens[$i]['sniffCode']          = array_shift($parts);
×
363
                                $this->tokens[$i]['sniffProperty']      = array_shift($parts);
×
364
                                $this->tokens[$i]['sniffPropertyValue'] = rtrim(implode(' ', $parts), " */\r\n");
×
365
                            }
366
                        }
367

368
                        $this->tokens[$i]['code'] = T_PHPCS_SET;
×
369
                        $this->tokens[$i]['type'] = 'T_PHPCS_SET';
×
370
                    } else if (substr($commentTextLower, 0, 16) === 'phpcs:ignorefile') {
186✔
371
                        // The whole file will be ignored, but at least set the correct token.
372
                        $this->tokens[$i]['code'] = T_PHPCS_IGNORE_FILE;
18✔
373
                        $this->tokens[$i]['type'] = 'T_PHPCS_IGNORE_FILE';
18✔
374
                    } else if (substr($commentTextLower, 0, 13) === 'phpcs:disable') {
168✔
375
                        if ($lineHasOtherContent === false) {
130✔
376
                            // Completely ignore the comment line.
377
                            $this->ignoredLines[$this->tokens[$i]['line']] = $ignoreAll;
120✔
378
                        }
379

380
                        $disabledSniffs = [];
130✔
381

382
                        $additionalText = substr($commentText, 14);
130✔
383
                        if (empty($additionalText) === true) {
130✔
384
                            $ignoring = $ignoreAll;
66✔
385
                        } else {
386
                            $ignoring = IgnoreList::getNewInstanceFrom($ignoring);
64✔
387

388
                            $parts = explode(',', $additionalText);
64✔
389
                            foreach ($parts as $sniffCode) {
64✔
390
                                $sniffCode = trim($sniffCode);
64✔
391
                                $disabledSniffs[$sniffCode] = true;
64✔
392
                                $ignoring->set($sniffCode, true);
64✔
393
                            }
394
                        }
395

396
                        $this->tokens[$i]['code']       = T_PHPCS_DISABLE;
130✔
397
                        $this->tokens[$i]['type']       = 'T_PHPCS_DISABLE';
130✔
398
                        $this->tokens[$i]['sniffCodes'] = $disabledSniffs;
130✔
399
                    } else if (substr($commentTextLower, 0, 12) === 'phpcs:enable') {
140✔
400
                        if ($ignoring !== null) {
102✔
401
                            $enabledSniffs = [];
100✔
402

403
                            $additionalText = substr($commentText, 13);
100✔
404
                            if (empty($additionalText) === true) {
100✔
405
                                $ignoring = null;
66✔
406
                            } else {
407
                                $ignoring = IgnoreList::getNewInstanceFrom($ignoring);
36✔
408
                                $parts    = explode(',', $additionalText);
36✔
409
                                foreach ($parts as $sniffCode) {
36✔
410
                                    $sniffCode = trim($sniffCode);
36✔
411
                                    $enabledSniffs[$sniffCode] = true;
36✔
412
                                    $ignoring->set($sniffCode, false);
36✔
413
                                }
414

415
                                if ($ignoring->ignoresNothing() === true) {
36✔
416
                                    $ignoring = null;
16✔
417
                                }
418
                            }
419

420
                            if ($lineHasOtherContent === false) {
100✔
421
                                // Completely ignore the comment line.
422
                                $this->ignoredLines[$this->tokens[$i]['line']] = $ignoreAll;
90✔
423
                            } else {
424
                                // The comment is on the same line as the code it is ignoring,
425
                                // so respect the new ignore rules.
426
                                $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
10✔
427
                            }
428

429
                            $this->tokens[$i]['sniffCodes'] = $enabledSniffs;
100✔
430
                        }//end if
431

432
                        $this->tokens[$i]['code'] = T_PHPCS_ENABLE;
102✔
433
                        $this->tokens[$i]['type'] = 'T_PHPCS_ENABLE';
102✔
434
                    } else if (substr($commentTextLower, 0, 12) === 'phpcs:ignore') {
52✔
435
                        $ignoreRules = [];
52✔
436

437
                        $additionalText = substr($commentText, 13);
52✔
438
                        if (empty($additionalText) === true) {
52✔
439
                            $ignoreRules  = ['.all' => true];
30✔
440
                            $lineIgnoring = $ignoreAll;
30✔
441
                        } else {
442
                            $parts        = explode(',', $additionalText);
22✔
443
                            $lineIgnoring = IgnoreList::getNewInstanceFrom($ignoring);
22✔
444

445
                            foreach ($parts as $sniffCode) {
22✔
446
                                $ignoreRules[trim($sniffCode)] = true;
22✔
447
                                $lineIgnoring->set($sniffCode, true);
22✔
448
                            }
449
                        }
450

451
                        $this->tokens[$i]['code']       = T_PHPCS_IGNORE;
52✔
452
                        $this->tokens[$i]['type']       = 'T_PHPCS_IGNORE';
52✔
453
                        $this->tokens[$i]['sniffCodes'] = $ignoreRules;
52✔
454

455
                        if ($lineHasOtherContent === false) {
52✔
456
                            // Completely ignore the comment line, and set the following
457
                            // line to include the ignore rules we've set.
458
                            $this->ignoredLines[$this->tokens[$i]['line']]       = $ignoreAll;
34✔
459
                            $this->ignoredLines[($this->tokens[$i]['line'] + 1)] = $lineIgnoring;
34✔
460
                        } else {
461
                            // The comment is on the same line as the code it is ignoring,
462
                            // so respect the ignore rules it set.
463
                            $this->ignoredLines[$this->tokens[$i]['line']] = $lineIgnoring;
18✔
464
                        }
465
                    }//end if
466
                }//end if
467
            }//end if
468

469
            if ($ignoring !== null && isset($this->ignoredLines[$this->tokens[$i]['line']]) === false) {
260✔
470
                $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
130✔
471
            }
472
        }//end for
473

474
        // If annotations are being ignored, we clear out all the ignore rules
475
        // but leave the annotations tokenized as normal.
476
        if ($checkAnnotations === false) {
260✔
477
            $this->ignoredLines = [];
×
478
        }
479

480
    }//end createPositionMap()
481

482

483
    /**
484
     * Replaces tabs in original token content with spaces.
485
     *
486
     * Each tab can represent between 1 and $config->tabWidth spaces,
487
     * so this cannot be a straight string replace. The original content
488
     * is placed into an orig_content index and the new token length is also
489
     * set in the length index.
490
     *
491
     * @param array  $token    The token to replace tabs inside.
492
     * @param string $prefix   The character to use to represent the start of a tab.
493
     * @param string $padding  The character to use to represent the end of a tab.
494
     * @param int    $tabWidth The number of spaces each tab represents.
495
     *
496
     * @return void
497
     */
498
    public function replaceTabsInToken(&$token, $prefix=' ', $padding=' ', $tabWidth=null)
118✔
499
    {
500
        $checkEncoding = false;
118✔
501
        if (function_exists('iconv_strlen') === true) {
118✔
502
            $checkEncoding = true;
118✔
503
        }
504

505
        $currColumn = $token['column'];
118✔
506
        if ($tabWidth === null) {
118✔
507
            $tabWidth = $this->config->tabWidth;
118✔
508
            if ($tabWidth === 0) {
118✔
509
                $tabWidth = 1;
2✔
510
            }
511
        }
512

513
        if (rtrim($token['content'], "\t") === '') {
118✔
514
            // String only contains tabs, so we can shortcut the process.
515
            $numTabs = strlen($token['content']);
114✔
516

517
            $firstTabSize = ($tabWidth - (($currColumn - 1) % $tabWidth));
114✔
518
            $length       = ($firstTabSize + ($tabWidth * ($numTabs - 1)));
114✔
519
            $newContent   = $prefix.str_repeat($padding, ($length - 1));
114✔
520
        } else {
521
            // We need to determine the length of each tab.
522
            $tabs = explode("\t", $token['content']);
116✔
523

524
            $numTabs    = (count($tabs) - 1);
116✔
525
            $tabNum     = 0;
116✔
526
            $newContent = '';
116✔
527
            $length     = 0;
116✔
528

529
            foreach ($tabs as $content) {
116✔
530
                if ($content !== '') {
116✔
531
                    $newContent .= $content;
116✔
532
                    if ($checkEncoding === true) {
116✔
533
                        // Not using ASCII encoding, so take a bit more care.
534
                        $oldLevel = error_reporting();
116✔
535
                        error_reporting(0);
116✔
536
                        $contentLength = iconv_strlen($content, $this->config->encoding);
116✔
537
                        error_reporting($oldLevel);
116✔
538
                        if ($contentLength === false) {
116✔
539
                            // String contained invalid characters, so revert to default.
540
                            $contentLength = strlen($content);
2✔
541
                        }
542
                    } else {
543
                        $contentLength = strlen($content);
×
544
                    }
545

546
                    $currColumn += $contentLength;
116✔
547
                    $length     += $contentLength;
116✔
548
                }
549

550
                // The last piece of content does not have a tab after it.
551
                if ($tabNum === $numTabs) {
116✔
552
                    break;
116✔
553
                }
554

555
                // Process the tab that comes after the content.
556
                $tabNum++;
116✔
557

558
                // Move the pointer to the next tab stop.
559
                $pad         = ($tabWidth - ($currColumn + $tabWidth - 1) % $tabWidth);
116✔
560
                $currColumn += $pad;
116✔
561
                $length     += $pad;
116✔
562
                $newContent .= $prefix.str_repeat($padding, ($pad - 1));
116✔
563
            }//end foreach
564
        }//end if
565

566
        $token['orig_content'] = $token['content'];
118✔
567
        $token['content']      = $newContent;
118✔
568
        $token['length']       = $length;
118✔
569

570
    }//end replaceTabsInToken()
571

572

573
    /**
574
     * Creates a map of brackets positions.
575
     *
576
     * @return void
577
     */
578
    private function createTokenMap()
146✔
579
    {
580
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
146✔
581
            StatusWriter::write('*** START TOKEN MAP ***', 1);
×
582
        }
583

584
        $squareOpeners   = [];
146✔
585
        $curlyOpeners    = [];
146✔
586
        $this->numTokens = count($this->tokens);
146✔
587

588
        $openers = [];
146✔
589

590
        for ($i = 0; $i < $this->numTokens; $i++) {
146✔
591
            /*
592
                Parenthesis mapping.
593
            */
594

595
            if (isset(Tokens::PARENTHESIS_OPENERS[$this->tokens[$i]['code']]) === true) {
146✔
596
                // Find the next non-empty token.
597
                $find = Tokens::EMPTY_TOKENS;
146✔
598
                if ($this->tokens[$i]['code'] === T_FUNCTION) {
146✔
599
                    $find[T_STRING]      = T_STRING;
144✔
600
                    $find[T_BITWISE_AND] = T_BITWISE_AND;
144✔
601
                }
602

603
                for ($j = ($i + 1); isset($this->tokens[$j], $find[$this->tokens[$j]['code']]) === true; $j++);
146✔
604
                if ($j < $this->numTokens && $this->tokens[$j]['code'] === T_OPEN_PARENTHESIS) {
146✔
605
                    $openers[] = $j;
144✔
606
                    $this->tokens[$i]['parenthesis_opener'] = $j;
144✔
607
                    $this->tokens[$i]['parenthesis_closer'] = null;
144✔
608
                    $this->tokens[$i]['parenthesis_owner']  = $i;
144✔
609

610
                    $this->tokens[$j]['parenthesis_opener'] = $j;
144✔
611
                    $this->tokens[$j]['parenthesis_owner']  = $i;
144✔
612

613
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
144✔
614
                        StatusWriter::write("=> Found parenthesis owner at $i", (count($openers) + 1));
×
615
                        StatusWriter::write("=> Found parenthesis opener at $j for $i", count($openers));
×
616
                    }
617

618
                    $i = $j;
144✔
619
                }
620
            } else if ($this->tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
146✔
621
                $openers[] = $i;
144✔
622
                $this->tokens[$i]['parenthesis_opener'] = $i;
144✔
623

624
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
144✔
625
                    StatusWriter::write("=> Found unowned parenthesis opener at $i", count($openers));
×
626
                }
627
            } else if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
146✔
628
                // Did we set an owner for this set of parenthesis?
629
                $numOpeners = count($openers);
144✔
630
                if ($numOpeners !== 0) {
144✔
631
                    $opener = array_pop($openers);
144✔
632
                    if (isset($this->tokens[$opener]['parenthesis_owner']) === true) {
144✔
633
                        $owner = $this->tokens[$opener]['parenthesis_owner'];
144✔
634

635
                        $this->tokens[$owner]['parenthesis_closer'] = $i;
144✔
636
                        $this->tokens[$i]['parenthesis_owner']      = $owner;
144✔
637

638
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
144✔
639
                            StatusWriter::write("=> Found parenthesis closer at $i for $owner", (count($openers) + 1));
×
640
                        }
641
                    } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
144✔
642
                        StatusWriter::write("=> Found unowned parenthesis closer at $i for $opener", (count($openers) + 1));
×
643
                    }
644

645
                    $this->tokens[$i]['parenthesis_opener']      = $opener;
144✔
646
                    $this->tokens[$i]['parenthesis_closer']      = $i;
144✔
647
                    $this->tokens[$opener]['parenthesis_closer'] = $i;
144✔
648
                }//end if
649
            } else if ($this->tokens[$i]['code'] === T_ATTRIBUTE) {
146✔
650
                $openers[] = $i;
×
651
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
652
                    StatusWriter::write("=> Found attribute opener at $i", count($openers));
×
653
                }
654

655
                $this->tokens[$i]['attribute_opener'] = $i;
×
656
                $this->tokens[$i]['attribute_closer'] = null;
×
657
            } else if ($this->tokens[$i]['code'] === T_ATTRIBUTE_END) {
146✔
658
                $numOpeners = count($openers);
×
659
                if ($numOpeners !== 0) {
×
660
                    $opener = array_pop($openers);
×
661
                    if (isset($this->tokens[$opener]['attribute_opener']) === true) {
×
662
                        $this->tokens[$opener]['attribute_closer'] = $i;
×
663

664
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
665
                            StatusWriter::write("=> Found attribute closer at $i for $opener", (count($openers) + 1));
×
666
                        }
667

668
                        for ($x = ($opener + 1); $x <= $i; ++$x) {
×
669
                            if (isset($this->tokens[$x]['attribute_closer']) === true) {
×
670
                                continue;
×
671
                            }
672

673
                            $this->tokens[$x]['attribute_opener'] = $opener;
×
674
                            $this->tokens[$x]['attribute_closer'] = $i;
×
675
                        }
676
                    } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
677
                        StatusWriter::write("=> Found unowned attribute closer at $i for $opener", (count($openers) + 1));
×
678
                    }
679
                }//end if
680
            }//end if
681

682
            /*
683
                Bracket mapping.
684
            */
685

686
            switch ($this->tokens[$i]['code']) {
146✔
687
            case T_OPEN_SQUARE_BRACKET:
146✔
688
                $squareOpeners[] = $i;
38✔
689

690
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
38✔
691
                    StatusWriter::write("=> Found square bracket opener at $i", (count($squareOpeners) + count($curlyOpeners)));
×
692
                }
693
                break;
38✔
694
            case T_OPEN_CURLY_BRACKET:
146✔
695
                if (isset($this->tokens[$i]['scope_closer']) === false) {
144✔
696
                    $curlyOpeners[] = $i;
144✔
697

698
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
144✔
699
                        StatusWriter::write("=> Found curly bracket opener at $i", (count($squareOpeners) + count($curlyOpeners)));
×
700
                    }
701
                }
702
                break;
144✔
703
            case T_CLOSE_SQUARE_BRACKET:
146✔
704
                if (empty($squareOpeners) === false) {
38✔
705
                    $opener = array_pop($squareOpeners);
38✔
706
                    $this->tokens[$i]['bracket_opener']      = $opener;
38✔
707
                    $this->tokens[$i]['bracket_closer']      = $i;
38✔
708
                    $this->tokens[$opener]['bracket_opener'] = $opener;
38✔
709
                    $this->tokens[$opener]['bracket_closer'] = $i;
38✔
710

711
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
38✔
712
                        StatusWriter::write("=> Found square bracket closer at $i for $opener", (count($squareOpeners) + count($curlyOpeners) + 1));
×
713
                    }
714
                }
715
                break;
38✔
716
            case T_CLOSE_CURLY_BRACKET:
146✔
717
                if (empty($curlyOpeners) === false
144✔
718
                    && isset($this->tokens[$i]['scope_opener']) === false
144✔
719
                ) {
720
                    $opener = array_pop($curlyOpeners);
144✔
721
                    $this->tokens[$i]['bracket_opener']      = $opener;
144✔
722
                    $this->tokens[$i]['bracket_closer']      = $i;
144✔
723
                    $this->tokens[$opener]['bracket_opener'] = $opener;
144✔
724
                    $this->tokens[$opener]['bracket_closer'] = $i;
144✔
725

726
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
144✔
727
                        StatusWriter::write("=> Found curly bracket closer at $i for $opener", (count($squareOpeners) + count($curlyOpeners) + 1));
×
728
                    }
729
                }
730
                break;
144✔
731
            default:
732
                continue 2;
146✔
733
            }//end switch
734
        }//end for
735

736
        // Cleanup for any openers that we didn't find closers for.
737
        // This typically means there was a syntax error breaking things.
738
        foreach ($openers as $opener) {
146✔
739
            unset($this->tokens[$opener]['parenthesis_opener']);
×
740
            unset($this->tokens[$opener]['parenthesis_owner']);
×
741
        }
742

743
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
146✔
744
            StatusWriter::write('*** END TOKEN MAP ***', 1);
×
745
        }
746

747
    }//end createTokenMap()
748

749

750
    /**
751
     * Creates a map for the parenthesis tokens that surround other tokens.
752
     *
753
     * @return void
754
     */
755
    private function createParenthesisNestingMap()
136✔
756
    {
757
        $map = [];
136✔
758
        for ($i = 0; $i < $this->numTokens; $i++) {
136✔
759
            if (isset($this->tokens[$i]['parenthesis_opener']) === true
136✔
760
                && $i === $this->tokens[$i]['parenthesis_opener']
136✔
761
            ) {
762
                if (empty($map) === false) {
136✔
763
                    $this->tokens[$i]['nested_parenthesis'] = $map;
136✔
764
                }
765

766
                if (isset($this->tokens[$i]['parenthesis_closer']) === true) {
136✔
767
                    $map[$this->tokens[$i]['parenthesis_opener']]
136✔
768
                        = $this->tokens[$i]['parenthesis_closer'];
136✔
769
                }
770
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
136✔
771
                && $i === $this->tokens[$i]['parenthesis_closer']
136✔
772
            ) {
773
                array_pop($map);
136✔
774
                if (empty($map) === false) {
136✔
775
                    $this->tokens[$i]['nested_parenthesis'] = $map;
136✔
776
                }
777
            } else {
778
                if (empty($map) === false) {
136✔
779
                    $this->tokens[$i]['nested_parenthesis'] = $map;
136✔
780
                }
781
            }//end if
782
        }//end for
783

784
    }//end createParenthesisNestingMap()
785

786

787
    /**
788
     * Creates a scope map of tokens that open scopes.
789
     *
790
     * @return void
791
     * @see    recurseScopeMap()
792
     */
793
    private function createScopeMap()
×
794
    {
795
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
796
            StatusWriter::write('*** START SCOPE MAP ***', 1);
×
797
        }
798

799
        for ($i = 0; $i < $this->numTokens; $i++) {
×
800
            // Check to see if the current token starts a new scope.
801
            if (isset($this->scopeOpeners[$this->tokens[$i]['code']]) === true) {
×
802
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
803
                    $type    = $this->tokens[$i]['type'];
×
804
                    $content = Common::prepareForOutput($this->tokens[$i]['content']);
×
805
                    StatusWriter::write("Start scope map at $i:$type => $content", 1);
×
806
                }
807

808
                if (isset($this->tokens[$i]['scope_condition']) === true) {
×
809
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
810
                        StatusWriter::write('* already processed, skipping *', 1);
×
811
                    }
812

813
                    continue;
×
814
                }
815

816
                $i = $this->recurseScopeMap($i);
×
817
            }//end if
818
        }//end for
819

820
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
821
            StatusWriter::write('*** END SCOPE MAP ***', 1);
×
822
        }
823

824
    }//end createScopeMap()
825

826

827
    /**
828
     * Recurses though the scope openers to build a scope map.
829
     *
830
     * @param int $stackPtr The position in the stack of the token that
831
     *                      opened the scope (eg. an IF token or FOR token).
832
     * @param int $depth    How many scope levels down we are.
833
     * @param int $ignore   How many curly braces we are ignoring.
834
     *
835
     * @return int The position in the stack that closed the scope.
836
     * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the nesting level gets too deep.
837
     */
838
    private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0)
148✔
839
    {
840
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
148✔
841
            StatusWriter::write("=> Begin scope map recursion at token $stackPtr with depth $depth", $depth);
×
842
        }
843

844
        $opener    = null;
148✔
845
        $currType  = $this->tokens[$stackPtr]['code'];
148✔
846
        $startLine = $this->tokens[$stackPtr]['line'];
148✔
847

848
        // We will need this to restore the value if we end up
849
        // returning a token ID that causes our calling function to go back
850
        // over already ignored braces.
851
        $originalIgnore = $ignore;
148✔
852

853
        // If the start token for this scope opener is the same as
854
        // the scope token, we have already found our opener.
855
        if (isset($this->scopeOpeners[$currType]['start'][$currType]) === true) {
148✔
856
            $opener = $stackPtr;
×
857
        }
858

859
        for ($i = ($stackPtr + 1); $i < $this->numTokens; $i++) {
148✔
860
            $tokenType = $this->tokens[$i]['code'];
148✔
861

862
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
148✔
863
                $type    = $this->tokens[$i]['type'];
×
864
                $line    = $this->tokens[$i]['line'];
×
865
                $content = Common::prepareForOutput($this->tokens[$i]['content']);
×
866

867
                $statusMessage = "Process token $i on line $line [";
×
868
                if ($opener !== null) {
×
869
                    $statusMessage .= "opener:$opener;";
×
870
                }
871

872
                if ($ignore > 0) {
×
873
                    $statusMessage .= "ignore=$ignore;";
×
874
                }
875

876
                $statusMessage .= "]: $type => $content";
×
877
                StatusWriter::write($statusMessage, $depth);
×
878
            }//end if
879

880
            // Very special case for IF statements in PHP that can be defined without
881
            // scope tokens. E.g., if (1) 1; 1 ? (1 ? 1 : 1) : 1;
882
            // If an IF statement below this one has an opener but no
883
            // keyword, the opener will be incorrectly assigned to this IF statement.
884
            // The same case also applies to USE statements, which don't have to have
885
            // openers, so a following USE statement can cause an incorrect brace match.
886
            if (($currType === T_IF || $currType === T_ELSE || $currType === T_USE)
148✔
887
                && $opener === null
148✔
888
                && ($this->tokens[$i]['code'] === T_SEMICOLON
148✔
889
                || $this->tokens[$i]['code'] === T_CLOSE_TAG)
148✔
890
            ) {
891
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
48✔
892
                    $type = $this->tokens[$stackPtr]['type'];
×
893
                    if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
894
                        $closerType = 'semicolon';
×
895
                    } else {
896
                        $closerType = 'close tag';
×
897
                    }
898

899
                    StatusWriter::write("=> Found $closerType before scope opener for $stackPtr:$type, bailing", $depth);
×
900
                }
901

902
                return $i;
48✔
903
            }
904

905
            // Special case for PHP control structures that have no braces.
906
            // If we find a curly brace closer before we find the opener,
907
            // we're not going to find an opener. That closer probably belongs to
908
            // a control structure higher up.
909
            if ($opener === null
148✔
910
                && $ignore === 0
148✔
911
                && $tokenType === T_CLOSE_CURLY_BRACKET
148✔
912
                && isset($this->scopeOpeners[$currType]['end'][$tokenType]) === true
148✔
913
            ) {
914
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
915
                    $type = $this->tokens[$stackPtr]['type'];
×
916
                    StatusWriter::write("=> Found curly brace closer before scope opener for $stackPtr:$type, bailing", $depth);
×
917
                }
918

919
                return ($i - 1);
×
920
            }
921

922
            if ($opener !== null
148✔
923
                && (isset($this->tokens[$i]['scope_opener']) === false
148✔
924
                || $this->scopeOpeners[$this->tokens[$stackPtr]['code']]['shared'] === true)
148✔
925
                && isset($this->scopeOpeners[$currType]['end'][$tokenType]) === true
148✔
926
            ) {
927
                if ($ignore > 0 && $tokenType === T_CLOSE_CURLY_BRACKET) {
148✔
928
                    // The last opening bracket must have been for a string
929
                    // offset or alike, so let's ignore it.
930
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
122✔
931
                        StatusWriter::write('* finished ignoring curly brace *', $depth);
×
932
                    }
933

934
                    $ignore--;
122✔
935
                    continue;
122✔
936
                } else if ($this->tokens[$opener]['code'] === T_OPEN_CURLY_BRACKET
148✔
937
                    && $tokenType !== T_CLOSE_CURLY_BRACKET
148✔
938
                ) {
939
                    // The opener is a curly bracket so the closer must be a curly bracket as well.
940
                    // We ignore this closer to handle cases such as T_ELSE or T_ELSEIF being considered
941
                    // a closer of T_IF when it should not.
942
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
943
                        $type = $this->tokens[$stackPtr]['type'];
×
944
                        StatusWriter::write("=> Ignoring non-curly scope closer for $stackPtr:$type", $depth);
×
945
                    }
946
                } else {
947
                    $scopeCloser = $i;
148✔
948
                    $todo        = [
148✔
949
                        $stackPtr,
148✔
950
                        $opener,
148✔
951
                    ];
148✔
952

953
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
148✔
954
                        $type       = $this->tokens[$stackPtr]['type'];
×
955
                        $closerType = $this->tokens[$scopeCloser]['type'];
×
956
                        StatusWriter::write("=> Found scope closer ($scopeCloser:$closerType) for $stackPtr:$type", $depth);
×
957
                    }
958

959
                    $validCloser = true;
148✔
960
                    if (($this->tokens[$stackPtr]['code'] === T_IF || $this->tokens[$stackPtr]['code'] === T_ELSEIF)
148✔
961
                        && ($tokenType === T_ELSE || $tokenType === T_ELSEIF)
148✔
962
                    ) {
963
                        // To be a closer, this token must have an opener.
964
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
2✔
965
                            StatusWriter::write('* closer needs to be tested *', $depth);
×
966
                        }
967

968
                        $i = self::recurseScopeMap($i, ($depth + 1), $ignore);
2✔
969

970
                        if (isset($this->tokens[$scopeCloser]['scope_opener']) === false) {
2✔
971
                            $validCloser = false;
×
972
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
973
                                StatusWriter::write('* closer is not valid (no opener found) *', $depth);
×
974
                            }
975
                        } else if ($this->tokens[$this->tokens[$scopeCloser]['scope_opener']]['code'] !== $this->tokens[$opener]['code']) {
2✔
976
                            $validCloser = false;
×
977
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
978
                                $type       = $this->tokens[$this->tokens[$scopeCloser]['scope_opener']]['type'];
×
979
                                $openerType = $this->tokens[$opener]['type'];
×
980
                                StatusWriter::write("* closer is not valid (mismatched opener type; $type != $openerType) *", $depth);
×
981
                            }
982
                        } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
2✔
983
                            StatusWriter::write('* closer was valid *', $depth);
×
984
                        }
985
                    } else {
986
                        // The closer was not processed, so we need to
987
                        // complete that token as well.
988
                        $todo[] = $scopeCloser;
148✔
989
                    }//end if
990

991
                    if ($validCloser === true) {
148✔
992
                        foreach ($todo as $token) {
148✔
993
                            $this->tokens[$token]['scope_condition'] = $stackPtr;
148✔
994
                            $this->tokens[$token]['scope_opener']    = $opener;
148✔
995
                            $this->tokens[$token]['scope_closer']    = $scopeCloser;
148✔
996
                        }
997

998
                        if ($this->scopeOpeners[$this->tokens[$stackPtr]['code']]['shared'] === true) {
148✔
999
                            // As we are going back to where we started originally, restore
1000
                            // the ignore value back to its original value.
1001
                            $ignore = $originalIgnore;
134✔
1002
                            return $opener;
134✔
1003
                        } else if ($scopeCloser === $i
148✔
1004
                            && isset($this->scopeOpeners[$tokenType]) === true
148✔
1005
                        ) {
1006
                            // Unset scope_condition here or else the token will appear to have
1007
                            // already been processed, and it will be skipped. Normally we want that,
1008
                            // but in this case, the token is both a closer and an opener, so
1009
                            // it needs to act like an opener. This is also why we return the
1010
                            // token before this one; so the closer has a chance to be processed
1011
                            // a second time, but as an opener.
1012
                            unset($this->tokens[$scopeCloser]['scope_condition']);
×
1013
                            return ($i - 1);
×
1014
                        } else {
1015
                            return $i;
148✔
1016
                        }
1017
                    } else {
1018
                        continue;
×
1019
                    }//end if
1020
                }//end if
1021
            }//end if
1022

1023
            // Is this an opening condition ?
1024
            if (isset($this->scopeOpeners[$tokenType]) === true) {
148✔
1025
                if ($opener === null) {
136✔
1026
                    if ($tokenType === T_USE) {
12✔
1027
                        // PHP use keywords are special because they can be
1028
                        // used as blocks but also inline in function definitions.
1029
                        // So if we find them nested inside another opener, just skip them.
1030
                        continue;
×
1031
                    }
1032

1033
                    if ($tokenType === T_FUNCTION
12✔
1034
                        && $this->tokens[$stackPtr]['code'] !== T_FUNCTION
12✔
1035
                    ) {
1036
                        // Probably a closure, so process it manually.
1037
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
1038
                            $type = $this->tokens[$stackPtr]['type'];
×
1039
                            StatusWriter::write("=> Found function before scope opener for $stackPtr:$type, processing manually", $depth);
×
1040
                        }
1041

1042
                        if (isset($this->tokens[$i]['scope_closer']) === true) {
12✔
1043
                            // We've already processed this closure.
1044
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1045
                                StatusWriter::write('* already processed, skipping *', $depth);
×
1046
                            }
1047

1048
                            $i = $this->tokens[$i]['scope_closer'];
×
1049
                            continue;
×
1050
                        }
1051

1052
                        $i = self::recurseScopeMap($i, ($depth + 1), $ignore);
12✔
1053
                        continue;
12✔
1054
                    }//end if
1055

1056
                    if ($tokenType === T_CLASS) {
×
1057
                        // Probably an anonymous class inside another anonymous class,
1058
                        // so process it manually.
1059
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1060
                            $type = $this->tokens[$stackPtr]['type'];
×
1061
                            StatusWriter::write("=> Found class before scope opener for $stackPtr:$type, processing manually", $depth);
×
1062
                        }
1063

1064
                        if (isset($this->tokens[$i]['scope_closer']) === true) {
×
1065
                            // We've already processed this anon class.
1066
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1067
                                StatusWriter::write('* already processed, skipping *', $depth);
×
1068
                            }
1069

1070
                            $i = $this->tokens[$i]['scope_closer'];
×
1071
                            continue;
×
1072
                        }
1073

1074
                        $i = self::recurseScopeMap($i, ($depth + 1), $ignore);
×
1075
                        continue;
×
1076
                    }//end if
1077

1078
                    // Found another opening condition but still haven't
1079
                    // found our opener, so we are never going to find one.
1080
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1081
                        $type = $this->tokens[$stackPtr]['type'];
×
1082
                        StatusWriter::write("=> Found new opening condition before scope opener for $stackPtr:$type, ", $depth, 0);
×
1083
                    }
1084

1085
                    if (($this->tokens[$stackPtr]['code'] === T_IF
×
1086
                        || $this->tokens[$stackPtr]['code'] === T_ELSEIF
×
1087
                        || $this->tokens[$stackPtr]['code'] === T_ELSE)
×
1088
                        && ($this->tokens[$i]['code'] === T_ELSE
×
1089
                        || $this->tokens[$i]['code'] === T_ELSEIF)
×
1090
                    ) {
1091
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1092
                            StatusWriter::write('continuing');
×
1093
                        }
1094

1095
                        return ($i - 1);
×
1096
                    } else {
1097
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1098
                            StatusWriter::write('backtracking');
×
1099
                        }
1100

1101
                        return $stackPtr;
×
1102
                    }
1103
                }//end if
1104

1105
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
136✔
1106
                    StatusWriter::write('* token is an opening condition *', $depth);
×
1107
                }
1108

1109
                $isShared = ($this->scopeOpeners[$tokenType]['shared'] === true);
136✔
1110

1111
                if (isset($this->tokens[$i]['scope_condition']) === true) {
136✔
1112
                    // We've been here before.
1113
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
122✔
1114
                        StatusWriter::write('* already processed, skipping *', $depth);
×
1115
                    }
1116

1117
                    if ($isShared === false
122✔
1118
                        && isset($this->tokens[$i]['scope_closer']) === true
122✔
1119
                    ) {
1120
                        $i = $this->tokens[$i]['scope_closer'];
122✔
1121
                    }
1122

1123
                    continue;
122✔
1124
                } else if ($currType === $tokenType
136✔
1125
                    && $isShared === false
136✔
1126
                    && $opener === null
136✔
1127
                ) {
1128
                    // We haven't yet found our opener, but we have found another
1129
                    // scope opener which is the same type as us, and we don't
1130
                    // share openers, so we will never find one.
1131
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1132
                        StatusWriter::write('* it was another token\'s opener, bailing *', $depth);
×
1133
                    }
1134

1135
                    return $stackPtr;
×
1136
                } else {
1137
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
136✔
1138
                        StatusWriter::write('* searching for opener *', $depth);
×
1139
                    }
1140

1141
                    if (isset($this->scopeOpeners[$tokenType]['end'][T_CLOSE_CURLY_BRACKET]) === true) {
136✔
1142
                        $oldIgnore = $ignore;
124✔
1143
                        $ignore    = 0;
124✔
1144
                    }
1145

1146
                    // PHP has a max nesting level for functions. Stop before we hit that limit
1147
                    // because too many loops means we've run into trouble anyway.
1148
                    if ($depth > 50) {
136✔
1149
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1150
                            StatusWriter::write('* reached maximum nesting level; aborting *', $depth);
×
1151
                        }
1152

1153
                        throw new TokenizerException('Maximum nesting level reached; file could not be processed');
×
1154
                    }
1155

1156
                    $oldDepth = $depth;
136✔
1157
                    if ($isShared === true
136✔
1158
                        && isset($this->scopeOpeners[$tokenType]['with'][$currType]) === true
136✔
1159
                    ) {
1160
                        // Don't allow the depth to increment because this is
1161
                        // possibly not a true nesting if we are sharing our closer.
1162
                        // This can happen, for example, when a SWITCH has a large
1163
                        // number of CASE statements with the same shared BREAK.
1164
                        $depth--;
136✔
1165
                    }
1166

1167
                    $i     = self::recurseScopeMap($i, ($depth + 1), $ignore);
136✔
1168
                    $depth = $oldDepth;
136✔
1169

1170
                    if (isset($this->scopeOpeners[$tokenType]['end'][T_CLOSE_CURLY_BRACKET]) === true) {
136✔
1171
                        $ignore = $oldIgnore;
124✔
1172
                    }
1173
                }//end if
1174
            }//end if
1175

1176
            if (isset($this->scopeOpeners[$currType]['start'][$tokenType]) === true
148✔
1177
                && $opener === null
148✔
1178
            ) {
1179
                if ($tokenType === T_OPEN_CURLY_BRACKET) {
148✔
1180
                    if (isset($this->tokens[$stackPtr]['parenthesis_closer']) === true
146✔
1181
                        && $i < $this->tokens[$stackPtr]['parenthesis_closer']
146✔
1182
                    ) {
1183
                        // We found a curly brace inside the condition of the
1184
                        // current scope opener, so it must be a string offset.
1185
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
1186
                            StatusWriter::write('* ignoring curly brace inside condition *', $depth);
×
1187
                        }
1188

1189
                        $ignore++;
12✔
1190
                    } else {
1191
                        // Make sure this is actually an opener and not a
1192
                        // string offset (e.g., $var{0}).
1193
                        for ($x = ($i - 1); $x > 0; $x--) {
146✔
1194
                            if (isset(Tokens::EMPTY_TOKENS[$this->tokens[$x]['code']]) === true) {
146✔
1195
                                continue;
146✔
1196
                            } else {
1197
                                // If the first non-whitespace/comment token looks like this
1198
                                // brace is a string offset, or this brace is mid-way through
1199
                                // a new statement, it isn't a scope opener.
1200
                                $disallowed  = Tokens::ASSIGNMENT_TOKENS;
146✔
1201
                                $disallowed += [
146✔
1202
                                    T_DOLLAR                   => true,
146✔
1203
                                    T_VARIABLE                 => true,
146✔
1204
                                    T_OBJECT_OPERATOR          => true,
146✔
1205
                                    T_NULLSAFE_OBJECT_OPERATOR => true,
146✔
1206
                                    T_COMMA                    => true,
146✔
1207
                                    T_OPEN_PARENTHESIS         => true,
146✔
1208
                                ];
146✔
1209

1210
                                if (isset($disallowed[$this->tokens[$x]['code']]) === true) {
146✔
1211
                                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1212
                                        StatusWriter::write('* ignoring curly brace *', $depth);
×
1213
                                    }
1214

1215
                                    $ignore++;
×
1216
                                }
1217

1218
                                break;
146✔
1219
                            }//end if
1220
                        }//end for
1221
                    }//end if
1222
                }//end if
1223

1224
                if ($ignore === 0 || $tokenType !== T_OPEN_CURLY_BRACKET) {
148✔
1225
                    $openerNested = isset($this->tokens[$i]['nested_parenthesis']);
148✔
1226
                    $ownerNested  = isset($this->tokens[$stackPtr]['nested_parenthesis']);
148✔
1227

1228
                    if (($openerNested === true && $ownerNested === false)
148✔
1229
                        || ($openerNested === false && $ownerNested === true)
148✔
1230
                        || ($openerNested === true
148✔
1231
                        && $this->tokens[$i]['nested_parenthesis'] !== $this->tokens[$stackPtr]['nested_parenthesis'])
148✔
1232
                    ) {
1233
                        // We found the a token that looks like the opener, but it's nested differently.
1234
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
1235
                            $type = $this->tokens[$i]['type'];
×
1236
                            StatusWriter::write("* ignoring possible opener $i:$type as nested parenthesis don't match *", $depth);
×
1237
                        }
1238
                    } else {
1239
                        // We found the opening scope token for $currType.
1240
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
148✔
1241
                            $type = $this->tokens[$stackPtr]['type'];
×
1242
                            StatusWriter::write("=> Found scope opener for $stackPtr:$type", $depth);
×
1243
                        }
1244

1245
                        $opener = $i;
148✔
1246
                    }
1247
                }//end if
1248
            } else if ($tokenType === T_SEMICOLON
148✔
1249
                && $opener === null
148✔
1250
                && (isset($this->tokens[$stackPtr]['parenthesis_closer']) === false
148✔
1251
                || $i > $this->tokens[$stackPtr]['parenthesis_closer'])
148✔
1252
            ) {
1253
                // Found the end of a statement but still haven't
1254
                // found our opener, so we are never going to find one.
1255
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
122✔
1256
                    $type = $this->tokens[$stackPtr]['type'];
×
1257
                    StatusWriter::write("=> Found end of statement before scope opener for $stackPtr:$type, continuing", $depth);
×
1258
                }
1259

1260
                return ($i - 1);
122✔
1261
            } else if ($tokenType === T_OPEN_PARENTHESIS) {
148✔
1262
                if (isset($this->tokens[$i]['parenthesis_owner']) === true) {
148✔
1263
                    $owner = $this->tokens[$i]['parenthesis_owner'];
148✔
1264
                    if (isset(Tokens::SCOPE_OPENERS[$this->tokens[$owner]['code']]) === true
148✔
1265
                        && isset($this->tokens[$i]['parenthesis_closer']) === true
148✔
1266
                    ) {
1267
                        // If we get into here, then we opened a parenthesis for
1268
                        // a scope (eg. an if or else if) so we need to update the
1269
                        // start of the line so that when we check to see
1270
                        // if the closing parenthesis is more than n lines away from
1271
                        // the statement, we check from the closing parenthesis.
1272
                        $startLine = $this->tokens[$this->tokens[$i]['parenthesis_closer']]['line'];
148✔
1273
                    }
1274
                }
1275
            } else if ($tokenType === T_OPEN_CURLY_BRACKET && $opener !== null) {
148✔
1276
                // We opened something that we don't have a scope opener for.
1277
                // Examples of this are curly brackets for string offsets etc.
1278
                // We want to ignore this so that we don't have an invalid scope
1279
                // map.
1280
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
122✔
1281
                    StatusWriter::write('* ignoring curly brace *', $depth);
×
1282
                }
1283

1284
                $ignore++;
122✔
1285
            } else if ($tokenType === T_CLOSE_CURLY_BRACKET && $ignore > 0) {
148✔
1286
                // We found the end token for the opener we were ignoring.
1287
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
1288
                    StatusWriter::write('* finished ignoring curly brace *', $depth);
×
1289
                }
1290

1291
                $ignore--;
12✔
1292
            } else if ($opener === null
148✔
1293
                && isset($this->scopeOpeners[$currType]) === true
148✔
1294
            ) {
1295
                // If we still haven't found the opener after 30 lines,
1296
                // we're not going to find it, unless we know it requires
1297
                // an opener (in which case we better keep looking) or the last
1298
                // token was empty (in which case we'll just confirm there is
1299
                // more code in this file and not just a big comment).
1300
                if ($this->tokens[$i]['line'] >= ($startLine + 30)
148✔
1301
                    && isset(Tokens::EMPTY_TOKENS[$this->tokens[($i - 1)]['code']]) === false
148✔
1302
                ) {
1303
                    if ($this->scopeOpeners[$currType]['strict'] === true) {
×
1304
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1305
                            $type  = $this->tokens[$stackPtr]['type'];
×
1306
                            $lines = ($this->tokens[$i]['line'] - $startLine);
×
1307
                            StatusWriter::write("=> Still looking for $stackPtr:$type scope opener after $lines lines", $depth);
×
1308
                        }
1309
                    } else {
1310
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1311
                            $type = $this->tokens[$stackPtr]['type'];
×
1312
                            StatusWriter::write("=> Couldn't find scope opener for $stackPtr:$type, bailing", $depth);
×
1313
                        }
1314

1315
                        return $stackPtr;
×
1316
                    }
1317
                }
1318
            } else if ($opener !== null
136✔
1319
                && $tokenType !== T_BREAK
136✔
1320
                && isset($this->endScopeTokens[$tokenType]) === true
136✔
1321
            ) {
1322
                if (isset($this->tokens[$i]['scope_condition']) === false) {
124✔
1323
                    if ($ignore > 0) {
124✔
1324
                        // We found the end token for the opener we were ignoring.
1325
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1326
                            StatusWriter::write('* finished ignoring curly brace *', $depth);
×
1327
                        }
1328

1329
                        $ignore--;
×
1330
                    } else {
1331
                        // We found a token that closes the scope but it doesn't
1332
                        // have a condition, so it belongs to another token and
1333
                        // our token doesn't have a closer, so pretend this is
1334
                        // the closer.
1335
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
124✔
1336
                            $type = $this->tokens[$stackPtr]['type'];
×
1337
                            StatusWriter::write("=> Found (unexpected) scope closer for $stackPtr:$type", $depth);
×
1338
                        }
1339

1340
                        foreach ([$stackPtr, $opener] as $token) {
124✔
1341
                            $this->tokens[$token]['scope_condition'] = $stackPtr;
124✔
1342
                            $this->tokens[$token]['scope_opener']    = $opener;
124✔
1343
                            $this->tokens[$token]['scope_closer']    = $i;
124✔
1344
                        }
1345

1346
                        return ($i - 1);
124✔
1347
                    }//end if
1348
                }//end if
1349
            }//end if
1350
        }//end for
1351

1352
        return $stackPtr;
×
1353

1354
    }//end recurseScopeMap()
1355

1356

1357
    /**
1358
     * Constructs the level map.
1359
     *
1360
     * The level map adds a 'level' index to each token which indicates the
1361
     * depth that a token within a set of scope blocks. It also adds a
1362
     * 'conditions' index which is an array of the scope conditions that opened
1363
     * each of the scopes - position 0 being the first scope opener.
1364
     *
1365
     * @return void
1366
     */
1367
    private function createLevelMap()
×
1368
    {
1369
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1370
            StatusWriter::write('*** START LEVEL MAP ***', 1);
×
1371
        }
1372

1373
        $this->numTokens = count($this->tokens);
×
1374
        $level           = 0;
×
1375
        $conditions      = [];
×
1376
        $lastOpener      = null;
×
1377
        $openers         = [];
×
1378

1379
        for ($i = 0; $i < $this->numTokens; $i++) {
×
1380
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1381
                $type = $this->tokens[$i]['type'];
×
1382
                $line = $this->tokens[$i]['line'];
×
1383
                $len  = $this->tokens[$i]['length'];
×
1384
                $col  = $this->tokens[$i]['column'];
×
1385

1386
                $content = Common::prepareForOutput($this->tokens[$i]['content']);
×
1387

1388
                $statusMessage = "Process token $i on line $line [col:$col;len:$len;lvl:$level;";
×
1389
                if (empty($conditions) !== true) {
×
1390
                    $conditionString = 'conds;';
×
1391
                    foreach ($conditions as $condition) {
×
1392
                        $conditionString .= Tokens::tokenName($condition).',';
×
1393
                    }
1394

1395
                    $statusMessage .= rtrim($conditionString, ',').';';
×
1396
                }
1397

1398
                $statusMessage .= "]: $type => $content";
×
1399
                StatusWriter::write($statusMessage, ($level + 1));
×
1400
            }//end if
1401

1402
            $this->tokens[$i]['level']      = $level;
×
1403
            $this->tokens[$i]['conditions'] = $conditions;
×
1404

1405
            if (isset($this->tokens[$i]['scope_condition']) === true) {
×
1406
                // Check to see if this token opened the scope.
1407
                if ($this->tokens[$i]['scope_opener'] === $i) {
×
1408
                    $stackPtr = $this->tokens[$i]['scope_condition'];
×
1409
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1410
                        $type = $this->tokens[$stackPtr]['type'];
×
1411
                        StatusWriter::write("=> Found scope opener for $stackPtr:$type", ($level + 1));
×
1412
                    }
1413

1414
                    $stackPtr = $this->tokens[$i]['scope_condition'];
×
1415

1416
                    // If we find a scope opener that has a shared closer,
1417
                    // then we need to go back over the condition map that we
1418
                    // just created and fix ourselves as we just added some
1419
                    // conditions where there was none. This happens for T_CASE
1420
                    // statements that are using the same break statement.
1421
                    if ($lastOpener !== null && $this->tokens[$lastOpener]['scope_closer'] === $this->tokens[$i]['scope_closer']) {
×
1422
                        // This opener shares its closer with the previous opener,
1423
                        // but we still need to check if the two openers share their
1424
                        // closer with each other directly (like CASE and DEFAULT)
1425
                        // or if they are just sharing because one doesn't have a
1426
                        // closer (like CASE with no BREAK using a SWITCHes closer).
1427
                        $thisType = $this->tokens[$this->tokens[$i]['scope_condition']]['code'];
×
1428
                        $opener   = $this->tokens[$lastOpener]['scope_condition'];
×
1429

1430
                        $isShared = isset($this->scopeOpeners[$thisType]['with'][$this->tokens[$opener]['code']]);
×
1431

1432
                        reset($this->scopeOpeners[$thisType]['end']);
×
1433
                        reset($this->scopeOpeners[$this->tokens[$opener]['code']]['end']);
×
1434
                        $sameEnd = (current($this->scopeOpeners[$thisType]['end']) === current($this->scopeOpeners[$this->tokens[$opener]['code']]['end']));
×
1435

1436
                        if ($isShared === true && $sameEnd === true) {
×
1437
                            $badToken = $opener;
×
1438
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1439
                                $type = $this->tokens[$badToken]['type'];
×
1440
                                StatusWriter::write("* shared closer, cleaning up $badToken:$type *", ($level + 1));
×
1441
                            }
1442

1443
                            for ($x = $this->tokens[$i]['scope_condition']; $x <= $i; $x++) {
×
1444
                                $oldConditions = $this->tokens[$x]['conditions'];
×
1445
                                $oldLevel      = $this->tokens[$x]['level'];
×
1446
                                $this->tokens[$x]['level']--;
×
1447
                                unset($this->tokens[$x]['conditions'][$badToken]);
×
1448
                                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1449
                                    $type     = $this->tokens[$x]['type'];
×
1450
                                    $oldConds = '';
×
1451
                                    foreach ($oldConditions as $condition) {
×
1452
                                        $oldConds .= Tokens::tokenName($condition).',';
×
1453
                                    }
1454

1455
                                    $oldConds = rtrim($oldConds, ',');
×
1456

1457
                                    $newConds = '';
×
1458
                                    foreach ($this->tokens[$x]['conditions'] as $condition) {
×
1459
                                        $newConds .= Tokens::tokenName($condition).',';
×
1460
                                    }
1461

1462
                                    $newConds = rtrim($newConds, ',');
×
1463

1464
                                    $newLevel = $this->tokens[$x]['level'];
×
1465
                                    StatusWriter::write("* cleaned $x:$type *", ($level + 1));
×
1466
                                    StatusWriter::write("=> level changed from $oldLevel to $newLevel", ($level + 2));
×
1467
                                    StatusWriter::write("=> conditions changed from $oldConds to $newConds", ($level + 2));
×
1468
                                }//end if
1469
                            }//end for
1470

1471
                            unset($conditions[$badToken]);
×
1472
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1473
                                $type = $this->tokens[$badToken]['type'];
×
1474
                                StatusWriter::write("* token $badToken:$type removed from conditions array *", ($level + 1));
×
1475
                            }
1476

1477
                            unset($openers[$lastOpener]);
×
1478

1479
                            $level--;
×
1480
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1481
                                StatusWriter::write('* level decreased *', ($level + 2));
×
1482
                            }
1483
                        }//end if
1484
                    }//end if
1485

1486
                    $level++;
×
1487
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1488
                        StatusWriter::write('* level increased *', ($level + 1));
×
1489
                    }
1490

1491
                    $conditions[$stackPtr] = $this->tokens[$stackPtr]['code'];
×
1492
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1493
                        $type = $this->tokens[$stackPtr]['type'];
×
1494
                        StatusWriter::write("* token $stackPtr:$type added to conditions array *", ($level + 1));
×
1495
                    }
1496

1497
                    $lastOpener = $this->tokens[$i]['scope_opener'];
×
1498
                    if ($lastOpener !== null) {
×
1499
                        $openers[$lastOpener] = $lastOpener;
×
1500
                    }
1501
                } else if ($lastOpener !== null && $this->tokens[$lastOpener]['scope_closer'] === $i) {
×
1502
                    foreach (array_reverse($openers) as $opener) {
×
1503
                        if ($this->tokens[$opener]['scope_closer'] === $i) {
×
1504
                            $oldOpener = array_pop($openers);
×
1505
                            if (empty($openers) === false) {
×
1506
                                $lastOpener           = array_pop($openers);
×
1507
                                $openers[$lastOpener] = $lastOpener;
×
1508
                            } else {
1509
                                $lastOpener = null;
×
1510
                            }
1511

1512
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1513
                                $type = $this->tokens[$oldOpener]['type'];
×
1514
                                StatusWriter::write("=> Found scope closer for $oldOpener:$type", ($level + 1));
×
1515
                            }
1516

1517
                            $oldCondition = array_pop($conditions);
×
1518
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1519
                                StatusWriter::write('* token '.Tokens::tokenName($oldCondition).' removed from conditions array *', ($level + 1));
×
1520
                            }
1521

1522
                            // Make sure this closer actually belongs to us.
1523
                            // Either the condition also has to think this is the
1524
                            // closer, or it has to allow sharing with us.
1525
                            $condition = $this->tokens[$this->tokens[$i]['scope_condition']]['code'];
×
1526
                            if ($condition !== $oldCondition) {
×
1527
                                if (isset($this->scopeOpeners[$oldCondition]['with'][$condition]) === false) {
×
1528
                                    $badToken = $this->tokens[$oldOpener]['scope_condition'];
×
1529

1530
                                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1531
                                        $type = Tokens::tokenName($oldCondition);
×
1532
                                        StatusWriter::write("* scope closer was bad, cleaning up $badToken:$type *", ($level + 1));
×
1533
                                    }
1534

1535
                                    for ($x = ($oldOpener + 1); $x <= $i; $x++) {
×
1536
                                        $oldConditions = $this->tokens[$x]['conditions'];
×
1537
                                        $oldLevel      = $this->tokens[$x]['level'];
×
1538
                                        $this->tokens[$x]['level']--;
×
1539
                                        unset($this->tokens[$x]['conditions'][$badToken]);
×
1540
                                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1541
                                            $type     = $this->tokens[$x]['type'];
×
1542
                                            $oldConds = '';
×
1543
                                            foreach ($oldConditions as $condition) {
×
1544
                                                $oldConds .= Tokens::tokenName($condition).',';
×
1545
                                            }
1546

1547
                                            $oldConds = rtrim($oldConds, ',');
×
1548

1549
                                            $newConds = '';
×
1550
                                            foreach ($this->tokens[$x]['conditions'] as $condition) {
×
1551
                                                $newConds .= Tokens::tokenName($condition).',';
×
1552
                                            }
1553

1554
                                            $newConds = rtrim($newConds, ',');
×
1555

1556
                                            $newLevel = $this->tokens[$x]['level'];
×
1557
                                            StatusWriter::write("* cleaned $x:$type *", ($level + 1));
×
1558
                                            StatusWriter::write("=> level changed from $oldLevel to $newLevel", ($level + 2));
×
1559
                                            StatusWriter::write("=> conditions changed from $oldConds to $newConds", ($level + 2));
×
1560
                                        }//end if
1561
                                    }//end for
1562
                                }//end if
1563
                            }//end if
1564

1565
                            $level--;
×
1566
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1567
                                StatusWriter::write('* level decreased *', ($level + 2));
×
1568
                            }
1569

1570
                            $this->tokens[$i]['level']      = $level;
×
1571
                            $this->tokens[$i]['conditions'] = $conditions;
×
1572
                        }//end if
1573
                    }//end foreach
1574
                }//end if
1575
            }//end if
1576
        }//end for
1577

1578
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
1579
            StatusWriter::write('*** END LEVEL MAP ***', 1);
×
1580
        }
1581

1582
    }//end createLevelMap()
1583

1584

1585
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc