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

PHPCSStandards / PHP_CodeSniffer / 17663829912

12 Sep 2025 03:38AM UTC coverage: 78.786%. Remained the same
17663829912

Pull #1245

github

web-flow
Merge ccdabfebd into 607b279a1
Pull Request #1245: CS: normalize code style rules [7]

677 of 1022 new or added lines in 17 files covered. (66.24%)

7 existing lines in 1 file now uncovered.

19732 of 25045 relevant lines covered (78.79%)

96.47 hits per line

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

0.0
/src/Sniffs/AbstractPatternSniff.php
1
<?php
2
/**
3
 * Processes pattern strings and checks that the code conforms to the pattern.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Sniffs;
11

12
use PHP_CodeSniffer\Exceptions\RuntimeException;
13
use PHP_CodeSniffer\Files\File;
14
use PHP_CodeSniffer\Tokenizers\PHP;
15
use PHP_CodeSniffer\Util\Tokens;
16
use PHP_CodeSniffer\Util\Writers\StatusWriter;
17

18
abstract class AbstractPatternSniff implements Sniff
19
{
20

21
    /**
22
     * If true, comments will be ignored if they are found in the code.
23
     *
24
     * @var boolean
25
     */
26
    public $ignoreComments = false;
27

28
    /**
29
     * The current file being checked.
30
     *
31
     * @var string
32
     */
33
    protected $currFile = '';
34

35
    /**
36
     * The parsed patterns array.
37
     *
38
     * @var array
39
     */
40
    private $parsedPatterns = [];
41

42
    /**
43
     * Tokens that this sniff wishes to process outside of the patterns.
44
     *
45
     * @var int[]
46
     * @see registerSupplementary()
47
     * @see processSupplementary()
48
     */
49
    private $supplementaryTokens = [];
50

51
    /**
52
     * Positions in the stack where errors have occurred.
53
     *
54
     * @var array<int, bool>
55
     */
56
    private $errorPos = [];
57

58

59
    /**
60
     * Constructs a AbstractPatternSniff.
61
     */
62
    public function __construct()
×
63
    {
64
        $this->supplementaryTokens = $this->registerSupplementary();
×
65
    }
66

67

68
    /**
69
     * Registers the tokens to listen to.
70
     *
71
     * Classes extending <i>AbstractPatternTest</i> should implement the
72
     * <i>getPatterns()</i> method to register the patterns they wish to test.
73
     *
74
     * @return array<int|string>
75
     * @see    process()
76
     */
77
    final public function register()
×
78
    {
79
        $listenTypes = [];
×
80
        $patterns    = $this->getPatterns();
×
81

82
        foreach ($patterns as $pattern) {
×
83
            $parsedPattern = $this->parse($pattern);
×
84

85
            // Find a token position in the pattern that we can use
86
            // for a listener token.
87
            $pos           = $this->getListenerTokenPos($parsedPattern);
×
88
            $tokenType     = $parsedPattern[$pos]['token'];
×
89
            $listenTypes[] = $tokenType;
×
90

91
            $patternArray = [
92
                'listen_pos'   => $pos,
×
93
                'pattern'      => $parsedPattern,
×
94
                'pattern_code' => $pattern,
×
95
            ];
96

97
            if (isset($this->parsedPatterns[$tokenType]) === false) {
×
98
                $this->parsedPatterns[$tokenType] = [];
×
99
            }
100

101
            $this->parsedPatterns[$tokenType][] = $patternArray;
×
102
        }
103

104
        return array_unique(array_merge($listenTypes, $this->supplementaryTokens));
×
105
    }
106

107

108
    /**
109
     * Returns the token types that the specified pattern is checking for.
110
     *
111
     * Returned array is in the format:
112
     * <code>
113
     *   array(
114
     *      T_WHITESPACE => 0, // 0 is the position where the T_WHITESPACE token
115
     *                         // should occur in the pattern.
116
     *   );
117
     * </code>
118
     *
119
     * @param array $pattern The parsed pattern to find the acquire the token
120
     *                       types from.
121
     *
122
     * @return array<int, int>
123
     */
124
    private function getPatternTokenTypes(array $pattern)
×
125
    {
126
        $tokenTypes = [];
×
127
        foreach ($pattern as $pos => $patternInfo) {
×
128
            if ($patternInfo['type'] === 'token') {
×
129
                if (isset($tokenTypes[$patternInfo['token']]) === false) {
×
130
                    $tokenTypes[$patternInfo['token']] = $pos;
×
131
                }
132
            }
133
        }
134

135
        return $tokenTypes;
×
136
    }
137

138

139
    /**
140
     * Returns the position in the pattern that this test should register as
141
     * a listener for the pattern.
142
     *
143
     * @param array $pattern The pattern to acquire the listener for.
144
     *
145
     * @return int The position in the pattern that this test should register
146
     *             as the listener.
147
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If we could not determine a token to listen for.
148
     */
149
    private function getListenerTokenPos(array $pattern)
×
150
    {
151
        $tokenTypes = $this->getPatternTokenTypes($pattern);
×
152
        $tokenCodes = array_keys($tokenTypes);
×
153
        $token      = Tokens::getHighestWeightedToken($tokenCodes);
×
154

155
        // If we could not get a token.
156
        if ($token === false) {
×
157
            $error = 'Could not determine a token to listen for';
×
158
            throw new RuntimeException($error);
×
159
        }
160

161
        return $tokenTypes[$token];
×
162
    }
163

164

165
    /**
166
     * Processes the test.
167
     *
168
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
169
     *                                               token occurred.
170
     * @param int                         $stackPtr  The position in the tokens stack
171
     *                                               where the listening token type
172
     *                                               was found.
173
     *
174
     * @return void
175
     * @see    register()
176
     */
177
    final public function process(File $phpcsFile, int $stackPtr)
×
178
    {
179
        $file = $phpcsFile->getFilename();
×
180
        if ($this->currFile !== $file) {
×
181
            // We have changed files, so clean up.
182
            $this->errorPos = [];
×
183
            $this->currFile = $file;
×
184
        }
185

186
        $tokens = $phpcsFile->getTokens();
×
187

188
        if (in_array($tokens[$stackPtr]['code'], $this->supplementaryTokens, true) === true) {
×
189
            $this->processSupplementary($phpcsFile, $stackPtr);
×
190
        }
191

192
        $type = $tokens[$stackPtr]['code'];
×
193

194
        // If the type is not set, then it must have been a token registered
195
        // with registerSupplementary().
196
        if (isset($this->parsedPatterns[$type]) === false) {
×
197
            return;
×
198
        }
199

200
        $allErrors = [];
×
201

202
        // Loop over each pattern that is listening to the current token type
203
        // that we are processing.
204
        foreach ($this->parsedPatterns[$type] as $patternInfo) {
×
205
            // If processPattern returns false, then the pattern that we are
206
            // checking the code with must not be designed to check that code.
207
            $errors = $this->processPattern($patternInfo, $phpcsFile, $stackPtr);
×
208
            if ($errors === false) {
×
209
                // The pattern didn't match.
210
                continue;
×
211
            } elseif (empty($errors) === true) {
×
212
                // The pattern matched, but there were no errors.
213
                break;
×
214
            }
215

216
            foreach ($errors as $stackPtr => $error) {
×
217
                if (isset($this->errorPos[$stackPtr]) === false) {
×
218
                    $this->errorPos[$stackPtr] = true;
×
219
                    $allErrors[$stackPtr]      = $error;
×
220
                }
221
            }
222
        }
223

224
        foreach ($allErrors as $stackPtr => $error) {
×
225
            $phpcsFile->addError($error, $stackPtr, 'Found');
×
226
        }
227
    }
228

229

230
    /**
231
     * Processes the pattern and verifies the code at $stackPtr.
232
     *
233
     * @param array                       $patternInfo Information about the pattern used
234
     *                                                 for checking, which includes are
235
     *                                                 parsed token representation of the
236
     *                                                 pattern.
237
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The PHP_CodeSniffer file where the
238
     *                                                 token occurred.
239
     * @param int                         $stackPtr    The position in the tokens stack where
240
     *                                                 the listening token type was found.
241
     *
242
     * @return array|false
243
     */
244
    protected function processPattern(array $patternInfo, File $phpcsFile, int $stackPtr)
×
245
    {
246
        $tokens      = $phpcsFile->getTokens();
×
247
        $pattern     = $patternInfo['pattern'];
×
248
        $patternCode = $patternInfo['pattern_code'];
×
249
        $errors      = [];
×
250
        $found       = '';
×
251

252
        $ignoreTokens = [T_WHITESPACE => T_WHITESPACE];
×
253
        if ($this->ignoreComments === true) {
×
254
            $ignoreTokens += Tokens::COMMENT_TOKENS;
×
255
        }
256

257
        $origStackPtr = $stackPtr;
×
258
        $hasError     = false;
×
259

260
        if ($patternInfo['listen_pos'] > 0) {
×
261
            $stackPtr--;
×
262

263
            for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
×
264
                if ($pattern[$i]['type'] === 'token') {
×
265
                    if ($pattern[$i]['token'] === T_WHITESPACE) {
×
266
                        if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
267
                            $found = $tokens[$stackPtr]['content'] . $found;
×
268
                        }
269

270
                        // Only check the size of the whitespace if this is not
271
                        // the first token. We don't care about the size of
272
                        // leading whitespace, just that there is some.
273
                        if ($i !== 0) {
×
274
                            if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
×
275
                                $hasError = true;
×
276
                            }
277
                        }
278
                    } else {
279
                        // Check to see if this important token is the same as the
280
                        // previous important token in the pattern. If it is not,
281
                        // then the pattern cannot be for this piece of code.
282
                        $prev = $phpcsFile->findPrevious(
×
283
                            $ignoreTokens,
×
284
                            $stackPtr,
×
285
                            null,
×
286
                            true
×
287
                        );
288

289
                        if ($prev === false
×
290
                            || $tokens[$prev]['code'] !== $pattern[$i]['token']
×
291
                        ) {
292
                            return false;
×
293
                        }
294

295
                        // If we skipped past some whitespace tokens, then add them
296
                        // to the found string.
297
                        $tokenContent = $phpcsFile->getTokensAsString(
×
298
                            ($prev + 1),
×
299
                            ($stackPtr - $prev - 1)
×
300
                        );
301

302
                        $found = $tokens[$prev]['content'] . $tokenContent . $found;
×
303

304
                        if (isset($pattern[($i - 1)]) === true
×
305
                            && $pattern[($i - 1)]['type'] === 'skip'
×
306
                        ) {
307
                            $stackPtr = $prev;
×
308
                        } else {
309
                            $stackPtr = ($prev - 1);
×
310
                        }
311
                    }
312
                } elseif ($pattern[$i]['type'] === 'skip') {
×
313
                    // Skip to next piece of relevant code.
314
                    if ($pattern[$i]['to'] === 'parenthesis_closer') {
×
315
                        $to = 'parenthesis_opener';
×
316
                    } else {
317
                        $to = 'scope_opener';
×
318
                    }
319

320
                    // Find the previous opener.
321
                    $next = $phpcsFile->findPrevious(
×
322
                        $ignoreTokens,
×
323
                        $stackPtr,
×
324
                        null,
×
325
                        true
×
326
                    );
327

328
                    if ($next === false || isset($tokens[$next][$to]) === false) {
×
329
                        // If there was not opener, then we must be
330
                        // using the wrong pattern.
331
                        return false;
×
332
                    }
333

334
                    if ($to === 'parenthesis_opener') {
×
335
                        $found = '{' . $found;
×
336
                    } else {
337
                        $found = '(' . $found;
×
338
                    }
339

340
                    $found = '...' . $found;
×
341

342
                    // Skip to the opening token.
343
                    $stackPtr = ($tokens[$next][$to] - 1);
×
344
                } elseif ($pattern[$i]['type'] === 'string') {
×
345
                    $found = 'abc';
×
346
                } elseif ($pattern[$i]['type'] === 'newline') {
×
347
                    if ($this->ignoreComments === true
×
348
                        && isset(Tokens::COMMENT_TOKENS[$tokens[$stackPtr]['code']]) === true
×
349
                    ) {
350
                        $startComment = $phpcsFile->findPrevious(
×
351
                            Tokens::COMMENT_TOKENS,
×
352
                            ($stackPtr - 1),
×
353
                            null,
×
354
                            true
×
355
                        );
356

357
                        if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
×
358
                            $startComment++;
×
359
                        }
360

361
                        $tokenContent = $phpcsFile->getTokensAsString(
×
362
                            $startComment,
×
363
                            ($stackPtr - $startComment + 1)
×
364
                        );
365

366
                        $found    = $tokenContent . $found;
×
367
                        $stackPtr = ($startComment - 1);
×
368
                    }
369

370
                    if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
371
                        if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
×
372
                            $found = $tokens[$stackPtr]['content'] . $found;
×
373

374
                            // This may just be an indent that comes after a newline
375
                            // so check the token before to make sure. If it is a newline, we
376
                            // can ignore the error here.
377
                            if (($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar)
×
378
                                && ($this->ignoreComments === true
×
379
                                && isset(Tokens::COMMENT_TOKENS[$tokens[($stackPtr - 1)]['code']]) === false)
×
380
                            ) {
381
                                $hasError = true;
×
382
                            } else {
383
                                $stackPtr--;
×
384
                            }
385
                        } else {
386
                            $found = 'EOL' . $found;
×
387
                        }
388
                    } else {
389
                        $found    = $tokens[$stackPtr]['content'] . $found;
×
390
                        $hasError = true;
×
391
                    }
392

393
                    if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
×
394
                        // Make sure they only have 1 newline.
395
                        $prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
×
396
                        if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
×
397
                            $hasError = true;
×
398
                        }
399
                    }
400
                }
401
            }
402
        }
403

404
        $stackPtr          = $origStackPtr;
×
405
        $lastAddedStackPtr = null;
×
406
        $patternLen        = count($pattern);
×
407

408
        if (($stackPtr + $patternLen - $patternInfo['listen_pos']) > $phpcsFile->numTokens) {
×
409
            // Pattern can never match as there are not enough tokens left in the file.
410
            return false;
×
411
        }
412

413
        for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
×
414
            if (isset($tokens[$stackPtr]) === false) {
×
415
                break;
×
416
            }
417

418
            if ($pattern[$i]['type'] === 'token') {
×
419
                if ($pattern[$i]['token'] === T_WHITESPACE) {
×
420
                    if ($this->ignoreComments === true) {
×
421
                        // If we are ignoring comments, check to see if this current
422
                        // token is a comment. If so skip it.
423
                        if (isset(Tokens::COMMENT_TOKENS[$tokens[$stackPtr]['code']]) === true) {
×
424
                            continue;
×
425
                        }
426

427
                        // If the next token is a comment, the we need to skip the
428
                        // current token as we should allow a space before a
429
                        // comment for readability.
430
                        if (isset($tokens[($stackPtr + 1)]) === true
×
431
                            && isset(Tokens::COMMENT_TOKENS[$tokens[($stackPtr + 1)]['code']]) === true
×
432
                        ) {
433
                            continue;
×
434
                        }
435
                    }
436

437
                    $tokenContent = '';
×
438
                    if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
439
                        if (isset($pattern[($i + 1)]) === false) {
×
440
                            // This is the last token in the pattern, so just compare
441
                            // the next token of content.
442
                            $tokenContent = $tokens[$stackPtr]['content'];
×
443
                        } else {
444
                            // Get all the whitespace to the next token.
445
                            $next = $phpcsFile->findNext(
×
446
                                Tokens::EMPTY_TOKENS,
×
447
                                $stackPtr,
×
448
                                null,
×
449
                                true
×
450
                            );
451

452
                            $tokenContent = $phpcsFile->getTokensAsString(
×
453
                                $stackPtr,
×
454
                                ($next - $stackPtr)
×
455
                            );
456

457
                            $lastAddedStackPtr = $stackPtr;
×
458
                            $stackPtr          = $next;
×
459
                        }
460

461
                        if ($stackPtr !== $lastAddedStackPtr) {
×
462
                            $found .= $tokenContent;
×
463
                        }
464
                    } else {
465
                        if ($stackPtr !== $lastAddedStackPtr) {
×
466
                            $found            .= $tokens[$stackPtr]['content'];
×
467
                            $lastAddedStackPtr = $stackPtr;
×
468
                        }
469
                    }
470

471
                    if (isset($pattern[($i + 1)]) === true
×
472
                        && $pattern[($i + 1)]['type'] === 'skip'
×
473
                    ) {
474
                        // The next token is a skip token, so we just need to make
475
                        // sure the whitespace we found has *at least* the
476
                        // whitespace required.
477
                        if (strpos($tokenContent, $pattern[$i]['value']) !== 0) {
×
478
                            $hasError = true;
×
479
                        }
480
                    } else {
481
                        if ($tokenContent !== $pattern[$i]['value']) {
×
482
                            $hasError = true;
×
483
                        }
484
                    }
485
                } else {
486
                    // Check to see if this important token is the same as the
487
                    // next important token in the pattern. If it is not, then
488
                    // the pattern cannot be for this piece of code.
489
                    $next = $phpcsFile->findNext(
×
490
                        $ignoreTokens,
×
491
                        $stackPtr,
×
492
                        null,
×
493
                        true
×
494
                    );
495

496
                    if ($next === false
×
497
                        || $tokens[$next]['code'] !== $pattern[$i]['token']
×
498
                    ) {
499
                        // The next important token did not match the pattern.
500
                        return false;
×
501
                    }
502

503
                    if ($lastAddedStackPtr !== null) {
×
504
                        if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
×
505
                            || $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
×
506
                            && isset($tokens[$next]['scope_condition']) === true
×
507
                            && $tokens[$next]['scope_condition'] > $lastAddedStackPtr
×
508
                        ) {
509
                            // This is a brace, but the owner of it is after the current
510
                            // token, which means it does not belong to any token in
511
                            // our pattern. This means the pattern is not for us.
512
                            return false;
×
513
                        }
514

515
                        if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
×
516
                            || $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
×
517
                            && isset($tokens[$next]['parenthesis_owner']) === true
×
518
                            && $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
×
519
                        ) {
520
                            // This is a bracket, but the owner of it is after the current
521
                            // token, which means it does not belong to any token in
522
                            // our pattern. This means the pattern is not for us.
523
                            return false;
×
524
                        }
525
                    }
526

527
                    // If we skipped past some whitespace tokens, then add them
528
                    // to the found string.
529
                    if (($next - $stackPtr) > 0) {
×
530
                        $hasComment = false;
×
531
                        for ($j = $stackPtr; $j < $next; $j++) {
×
532
                            $found .= $tokens[$j]['content'];
×
533
                            if (isset(Tokens::COMMENT_TOKENS[$tokens[$j]['code']]) === true) {
×
534
                                $hasComment = true;
×
535
                            }
536
                        }
537

538
                        // If we are not ignoring comments, this additional
539
                        // whitespace or comment is not allowed. If we are
540
                        // ignoring comments, there needs to be at least one
541
                        // comment for this to be allowed.
542
                        if ($this->ignoreComments === false
×
543
                            || ($this->ignoreComments === true
×
544
                            && $hasComment === false)
×
545
                        ) {
546
                            $hasError = true;
×
547
                        }
548

549
                        // Even when ignoring comments, we are not allowed to include
550
                        // newlines without the pattern specifying them, so
551
                        // everything should be on the same line.
552
                        if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
×
553
                            $hasError = true;
×
554
                        }
555
                    }
556

557
                    if ($next !== $lastAddedStackPtr) {
×
558
                        $found            .= $tokens[$next]['content'];
×
559
                        $lastAddedStackPtr = $next;
×
560
                    }
561

562
                    if (isset($pattern[($i + 1)]) === true
×
563
                        && $pattern[($i + 1)]['type'] === 'skip'
×
564
                    ) {
565
                        $stackPtr = $next;
×
566
                    } else {
567
                        $stackPtr = ($next + 1);
×
568
                    }
569
                }
570
            } elseif ($pattern[$i]['type'] === 'skip') {
×
571
                if ($pattern[$i]['to'] === 'unknown') {
×
572
                    $next = $phpcsFile->findNext(
×
573
                        $pattern[($i + 1)]['token'],
×
574
                        $stackPtr
×
575
                    );
576

577
                    if ($next === false) {
×
578
                        // Couldn't find the next token, so we must
579
                        // be using the wrong pattern.
580
                        return false;
×
581
                    }
582

583
                    $found   .= '...';
×
584
                    $stackPtr = $next;
×
585
                } else {
586
                    // Find the previous opener.
587
                    $next = $phpcsFile->findPrevious(
×
588
                        Tokens::BLOCK_OPENERS,
×
589
                        $stackPtr
×
590
                    );
591

592
                    if ($next === false
×
593
                        || isset($tokens[$next][$pattern[$i]['to']]) === false
×
594
                    ) {
595
                        // If there was not opener, then we must
596
                        // be using the wrong pattern.
597
                        return false;
×
598
                    }
599

600
                    $found .= '...';
×
601
                    if ($pattern[$i]['to'] === 'parenthesis_closer') {
×
602
                        $found .= ')';
×
603
                    } else {
604
                        $found .= '}';
×
605
                    }
606

607
                    // Skip to the closing token.
608
                    $stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
×
609
                }
610
            } elseif ($pattern[$i]['type'] === 'string') {
×
611
                if ($tokens[$stackPtr]['code'] !== T_STRING) {
×
612
                    $hasError = true;
×
613
                }
614

615
                if ($stackPtr !== $lastAddedStackPtr) {
×
616
                    $found            .= 'abc';
×
617
                    $lastAddedStackPtr = $stackPtr;
×
618
                }
619

620
                $stackPtr++;
×
621
            } elseif ($pattern[$i]['type'] === 'newline') {
×
622
                // Find the next token that contains a newline character.
623
                $newline = 0;
×
624
                for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
×
625
                    if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
×
626
                        $newline = $j;
×
627
                        break;
×
628
                    }
629
                }
630

631
                if ($newline === 0) {
×
632
                    // We didn't find a newline character in the rest of the file.
633
                    $next     = ($phpcsFile->numTokens - 1);
×
634
                    $hasError = true;
×
635
                } else {
636
                    if ($this->ignoreComments === false) {
×
637
                        // The newline character cannot be part of a comment.
638
                        if (isset(Tokens::COMMENT_TOKENS[$tokens[$newline]['code']]) === true) {
×
639
                            $hasError = true;
×
640
                        }
641
                    }
642

643
                    if ($newline === $stackPtr) {
×
644
                        $next = ($stackPtr + 1);
×
645
                    } else {
646
                        // Check that there were no significant tokens that we
647
                        // skipped over to find our newline character.
648
                        $next = $phpcsFile->findNext(
×
649
                            $ignoreTokens,
×
650
                            $stackPtr,
×
651
                            null,
×
652
                            true
×
653
                        );
654

655
                        if ($next < $newline) {
×
656
                            // We skipped a non-ignored token.
657
                            $hasError = true;
×
658
                        } else {
659
                            $next = ($newline + 1);
×
660
                        }
661
                    }
662
                }
663

664
                if ($stackPtr !== $lastAddedStackPtr) {
×
665
                    $found .= $phpcsFile->getTokensAsString(
×
666
                        $stackPtr,
×
667
                        ($next - $stackPtr)
×
668
                    );
669

670
                    $lastAddedStackPtr = ($next - 1);
×
671
                }
672

673
                $stackPtr = $next;
×
674
            }
675
        }
676

677
        if ($hasError === true) {
×
678
            $error = $this->prepareError($found, $patternCode);
×
679
            $errors[$origStackPtr] = $error;
×
680
        }
681

682
        return $errors;
×
683
    }
684

685

686
    /**
687
     * Prepares an error for the specified patternCode.
688
     *
689
     * @param string $found       The actual found string in the code.
690
     * @param string $patternCode The expected pattern code.
691
     *
692
     * @return string The error message.
693
     */
694
    protected function prepareError(string $found, string $patternCode)
×
695
    {
696
        $found    = str_replace("\r\n", '\n', $found);
×
697
        $found    = str_replace("\n", '\n', $found);
×
698
        $found    = str_replace("\r", '\n', $found);
×
699
        $found    = str_replace("\t", '\t', $found);
×
700
        $found    = str_replace('EOL', '\n', $found);
×
701
        $expected = str_replace('EOL', '\n', $patternCode);
×
702

703
        $error = "Expected \"$expected\"; found \"$found\"";
×
704

705
        return $error;
×
706
    }
707

708

709
    /**
710
     * Returns the patterns that should be checked.
711
     *
712
     * @return string[]
713
     */
714
    abstract protected function getPatterns();
715

716

717
    /**
718
     * Registers any supplementary tokens that this test might wish to process.
719
     *
720
     * A sniff may wish to register supplementary tests when it wishes to group
721
     * an arbitrary validation that cannot be performed using a pattern, with
722
     * other pattern tests.
723
     *
724
     * @return int[]
725
     * @see    processSupplementary()
726
     */
727
    protected function registerSupplementary()
×
728
    {
729
        return [];
×
730
    }
731

732

733
    /**
734
     * Processes any tokens registered with registerSupplementary().
735
     *
736
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where to
737
     *                                               process the skip.
738
     * @param int                         $stackPtr  The position in the tokens stack to
739
     *                                               process.
740
     *
741
     * @return void
742
     * @see    registerSupplementary()
743
     */
744
    protected function processSupplementary(File $phpcsFile, int $stackPtr)
×
745
    {
746
    }
×
747

748

749
    /**
750
     * Parses a pattern string into an array of pattern steps.
751
     *
752
     * @param string $pattern The pattern to parse.
753
     *
754
     * @return array The parsed pattern array.
755
     * @see    createSkipPattern()
756
     * @see    createTokenPattern()
757
     */
758
    private function parse(string $pattern)
×
759
    {
760
        $patterns   = [];
×
761
        $length     = strlen($pattern);
×
762
        $lastToken  = 0;
×
763
        $firstToken = 0;
×
764

765
        for ($i = 0; $i < $length; $i++) {
×
766
            $specialPattern = false;
×
767
            $isLastChar     = ($i === ($length - 1));
×
768
            $oldFirstToken  = $firstToken;
×
769

770
            if (substr($pattern, $i, 3) === '...') {
×
771
                // It's a skip pattern. The skip pattern requires the
772
                // content of the token in the "from" position and the token
773
                // to skip to.
774
                $specialPattern = $this->createSkipPattern($pattern, ($i - 1));
×
775
                $lastToken      = ($i - $firstToken);
×
776
                $firstToken     = ($i + 3);
×
777
                $i += 2;
×
778

779
                if ($specialPattern['to'] !== 'unknown') {
×
780
                    $firstToken++;
×
781
                }
782
            } elseif (substr($pattern, $i, 3) === 'abc') {
×
783
                $specialPattern = ['type' => 'string'];
×
784
                $lastToken      = ($i - $firstToken);
×
785
                $firstToken     = ($i + 3);
×
786
                $i += 2;
×
787
            } elseif (substr($pattern, $i, 3) === 'EOL') {
×
788
                $specialPattern = ['type' => 'newline'];
×
789
                $lastToken      = ($i - $firstToken);
×
790
                $firstToken     = ($i + 3);
×
791
                $i += 2;
×
792
            }
793

794
            if ($specialPattern !== false || $isLastChar === true) {
×
795
                // If we are at the end of the string, don't worry about a limit.
796
                if ($isLastChar === true) {
×
797
                    // Get the string from the end of the last skip pattern, if any,
798
                    // to the end of the pattern string.
799
                    $str = substr($pattern, $oldFirstToken);
×
800
                } else {
801
                    // Get the string from the end of the last special pattern,
802
                    // if any, to the start of this special pattern.
803
                    if ($lastToken === 0) {
×
804
                        // Note that if the last special token was zero characters ago,
805
                        // there will be nothing to process so we can skip this bit.
806
                        // This happens if you have something like: EOL... in your pattern.
807
                        $str = '';
×
808
                    } else {
809
                        $str = substr($pattern, $oldFirstToken, $lastToken);
×
810
                    }
811
                }
812

813
                if ($str !== '') {
×
814
                    $tokenPatterns = $this->createTokenPattern($str);
×
815
                    foreach ($tokenPatterns as $tokenPattern) {
×
816
                        $patterns[] = $tokenPattern;
×
817
                    }
818
                }
819

820
                // Make sure we don't skip the last token.
821
                if ($isLastChar === false && $i === ($length - 1)) {
×
822
                    $i--;
×
823
                }
824
            }
825

826
            // Add the skip pattern *after* we have processed
827
            // all the tokens from the end of the last skip pattern
828
            // to the start of this skip pattern.
829
            if ($specialPattern !== false) {
×
830
                $patterns[] = $specialPattern;
×
831
            }
832
        }
833

834
        return $patterns;
×
835
    }
836

837

838
    /**
839
     * Creates a skip pattern.
840
     *
841
     * @param string $pattern The pattern being parsed.
842
     * @param int    $from    The token position that the skip pattern starts from.
843
     *
844
     * @return array The pattern step.
845
     * @see    createTokenPattern()
846
     * @see    parse()
847
     */
848
    private function createSkipPattern(string $pattern, int $from)
×
849
    {
850
        $skip = ['type' => 'skip'];
×
851

852
        $nestedParenthesis = 0;
×
853
        $nestedBraces      = 0;
×
854
        for ($start = $from; $start >= 0; $start--) {
×
855
            switch ($pattern[$start]) {
×
NEW
856
                case '(':
×
NEW
857
                    if ($nestedParenthesis === 0) {
×
NEW
858
                        $skip['to'] = 'parenthesis_closer';
×
859
                    }
860

NEW
861
                    $nestedParenthesis--;
×
NEW
862
                    break;
×
NEW
863
                case '{':
×
NEW
864
                    if ($nestedBraces === 0) {
×
NEW
865
                        $skip['to'] = 'scope_closer';
×
866
                    }
867

NEW
868
                    $nestedBraces--;
×
NEW
869
                    break;
×
NEW
870
                case '}':
×
NEW
871
                    $nestedBraces++;
×
NEW
872
                    break;
×
NEW
873
                case ')':
×
NEW
874
                    $nestedParenthesis++;
×
NEW
875
                    break;
×
876
            }
877

878
            if (isset($skip['to']) === true) {
×
879
                break;
×
880
            }
881
        }
882

883
        if (isset($skip['to']) === false) {
×
884
            $skip['to'] = 'unknown';
×
885
        }
886

887
        return $skip;
×
888
    }
889

890

891
    /**
892
     * Creates a token pattern.
893
     *
894
     * @param string $str The tokens string that the pattern should match.
895
     *
896
     * @return array The pattern step.
897
     * @see    createSkipPattern()
898
     * @see    parse()
899
     */
900
    private function createTokenPattern(string $str)
×
901
    {
902
        // Pause the StatusWriter to silence Tokenizer debug info about the patterns being parsed (which only confuses things).
903
        StatusWriter::pause();
×
904

905
        // Don't add a space after the closing php tag as it will add a new
906
        // whitespace token.
907
        $tokenizer = new PHP('<?php' . "\n" . $str . '?>', null);
×
908
        StatusWriter::resume();
×
909

910
        // Remove the <?php tag from the front and the end php tag from the back.
911
        $tokens = $tokenizer->getTokens();
×
912
        $tokens = array_slice($tokens, 2, (count($tokens) - 3));
×
913

914
        $patterns = [];
×
915
        foreach ($tokens as $patternInfo) {
×
916
            $patterns[] = [
×
917
                'type'  => 'token',
×
918
                'token' => $patternInfo['code'],
×
919
                'value' => $patternInfo['content'],
×
920
            ];
921
        }
922

923
        return $patterns;
×
924
    }
925
}
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

© 2025 Coveralls, Inc