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

PHPCSStandards / PHP_CodeSniffer / 17626940019

10 Sep 2025 09:11PM UTC coverage: 78.509%. Remained the same
17626940019

push

github

web-flow
Merge pull request #1232 from PHPCSStandards/feature/various-minor-cs-fixes

CS: various minor fixes

6 of 6 new or added lines in 5 files covered. (100.0%)

1 existing line in 1 file now uncovered.

25313 of 32242 relevant lines covered (78.51%)

74.69 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

17
abstract class AbstractPatternSniff implements Sniff
18
{
19

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

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

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

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

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

57

58
    /**
59
     * Constructs a AbstractPatternSniff.
60
     *
61
     * @param boolean $ignoreComments If true, comments will be ignored.
62
     */
63
    public function __construct($ignoreComments=null)
×
64
    {
65
        // This is here for backwards compatibility.
66
        if ($ignoreComments !== null) {
×
67
            $this->ignoreComments = $ignoreComments;
×
68
        }
69

70
        $this->supplementaryTokens = $this->registerSupplementary();
×
71

72
    }//end __construct()
73

74

75
    /**
76
     * Registers the tokens to listen to.
77
     *
78
     * Classes extending <i>AbstractPatternTest</i> should implement the
79
     * <i>getPatterns()</i> method to register the patterns they wish to test.
80
     *
81
     * @return array<int|string>
82
     * @see    process()
83
     */
84
    final public function register()
×
85
    {
86
        $listenTypes = [];
×
87
        $patterns    = $this->getPatterns();
×
88

89
        foreach ($patterns as $pattern) {
×
90
            $parsedPattern = $this->parse($pattern);
×
91

92
            // Find a token position in the pattern that we can use
93
            // for a listener token.
94
            $pos           = $this->getListenerTokenPos($parsedPattern);
×
95
            $tokenType     = $parsedPattern[$pos]['token'];
×
96
            $listenTypes[] = $tokenType;
×
97

98
            $patternArray = [
99
                'listen_pos'   => $pos,
×
100
                'pattern'      => $parsedPattern,
×
101
                'pattern_code' => $pattern,
×
102
            ];
103

104
            if (isset($this->parsedPatterns[$tokenType]) === false) {
×
105
                $this->parsedPatterns[$tokenType] = [];
×
106
            }
107

108
            $this->parsedPatterns[$tokenType][] = $patternArray;
×
109
        }//end foreach
110

111
        return array_unique(array_merge($listenTypes, $this->supplementaryTokens));
×
112

113
    }//end register()
114

115

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

143
        return $tokenTypes;
×
144

145
    }//end getPatternTokenTypes()
146

147

148
    /**
149
     * Returns the position in the pattern that this test should register as
150
     * a listener for the pattern.
151
     *
152
     * @param array $pattern The pattern to acquire the listener for.
153
     *
154
     * @return int The position in the pattern that this test should register
155
     *             as the listener.
156
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If we could not determine a token to listen for.
157
     */
158
    private function getListenerTokenPos($pattern)
×
159
    {
160
        $tokenTypes = $this->getPatternTokenTypes($pattern);
×
161
        $tokenCodes = array_keys($tokenTypes);
×
162
        $token      = Tokens::getHighestWeightedToken($tokenCodes);
×
163

164
        // If we could not get a token.
165
        if ($token === false) {
×
166
            $error = 'Could not determine a token to listen for';
×
167
            throw new RuntimeException($error);
×
168
        }
169

170
        return $tokenTypes[$token];
×
171

172
    }//end getListenerTokenPos()
173

174

175
    /**
176
     * Processes the test.
177
     *
178
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
179
     *                                               token occurred.
180
     * @param int                         $stackPtr  The position in the tokens stack
181
     *                                               where the listening token type
182
     *                                               was found.
183
     *
184
     * @return void
185
     * @see    register()
186
     */
187
    final public function process(File $phpcsFile, $stackPtr)
×
188
    {
189
        $file = $phpcsFile->getFilename();
×
190
        if ($this->currFile !== $file) {
×
191
            // We have changed files, so clean up.
192
            $this->errorPos = [];
×
193
            $this->currFile = $file;
×
194
        }
195

196
        $tokens = $phpcsFile->getTokens();
×
197

198
        if (in_array($tokens[$stackPtr]['code'], $this->supplementaryTokens, true) === true) {
×
199
            $this->processSupplementary($phpcsFile, $stackPtr);
×
200
        }
201

202
        $type = $tokens[$stackPtr]['code'];
×
203

204
        // If the type is not set, then it must have been a token registered
205
        // with registerSupplementary().
206
        if (isset($this->parsedPatterns[$type]) === false) {
×
207
            return;
×
208
        }
209

210
        $allErrors = [];
×
211

212
        // Loop over each pattern that is listening to the current token type
213
        // that we are processing.
214
        foreach ($this->parsedPatterns[$type] as $patternInfo) {
×
215
            // If processPattern returns false, then the pattern that we are
216
            // checking the code with must not be designed to check that code.
217
            $errors = $this->processPattern($patternInfo, $phpcsFile, $stackPtr);
×
218
            if ($errors === false) {
×
219
                // The pattern didn't match.
220
                continue;
×
221
            } else if (empty($errors) === true) {
×
222
                // The pattern matched, but there were no errors.
223
                break;
×
224
            }
225

226
            foreach ($errors as $stackPtr => $error) {
×
227
                if (isset($this->errorPos[$stackPtr]) === false) {
×
228
                    $this->errorPos[$stackPtr] = true;
×
229
                    $allErrors[$stackPtr]      = $error;
×
230
                }
231
            }
232
        }
233

234
        foreach ($allErrors as $stackPtr => $error) {
×
235
            $phpcsFile->addError($error, $stackPtr, 'Found');
×
236
        }
237

238
    }//end process()
239

240

241
    /**
242
     * Processes the pattern and verifies the code at $stackPtr.
243
     *
244
     * @param array                       $patternInfo Information about the pattern used
245
     *                                                 for checking, which includes are
246
     *                                                 parsed token representation of the
247
     *                                                 pattern.
248
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The PHP_CodeSniffer file where the
249
     *                                                 token occurred.
250
     * @param int                         $stackPtr    The position in the tokens stack where
251
     *                                                 the listening token type was found.
252
     *
253
     * @return array|false
254
     */
255
    protected function processPattern($patternInfo, File $phpcsFile, $stackPtr)
×
256
    {
257
        $tokens      = $phpcsFile->getTokens();
×
258
        $pattern     = $patternInfo['pattern'];
×
259
        $patternCode = $patternInfo['pattern_code'];
×
260
        $errors      = [];
×
261
        $found       = '';
×
262

263
        $ignoreTokens = [T_WHITESPACE => T_WHITESPACE];
×
264
        if ($this->ignoreComments === true) {
×
265
            $ignoreTokens += Tokens::$commentTokens;
×
266
        }
267

268
        $origStackPtr = $stackPtr;
×
269
        $hasError     = false;
×
270

271
        if ($patternInfo['listen_pos'] > 0) {
×
272
            $stackPtr--;
×
273

274
            for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
×
275
                if ($pattern[$i]['type'] === 'token') {
×
276
                    if ($pattern[$i]['token'] === T_WHITESPACE) {
×
277
                        if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
278
                            $found = $tokens[$stackPtr]['content'].$found;
×
279
                        }
280

281
                        // Only check the size of the whitespace if this is not
282
                        // the first token. We don't care about the size of
283
                        // leading whitespace, just that there is some.
284
                        if ($i !== 0) {
×
285
                            if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
×
286
                                $hasError = true;
×
287
                            }
288
                        }
289
                    } else {
290
                        // Check to see if this important token is the same as the
291
                        // previous important token in the pattern. If it is not,
292
                        // then the pattern cannot be for this piece of code.
293
                        $prev = $phpcsFile->findPrevious(
×
294
                            $ignoreTokens,
×
295
                            $stackPtr,
×
296
                            null,
×
297
                            true
298
                        );
299

300
                        if ($prev === false
301
                            || $tokens[$prev]['code'] !== $pattern[$i]['token']
×
302
                        ) {
303
                            return false;
×
304
                        }
305

306
                        // If we skipped past some whitespace tokens, then add them
307
                        // to the found string.
308
                        $tokenContent = $phpcsFile->getTokensAsString(
×
309
                            ($prev + 1),
×
310
                            ($stackPtr - $prev - 1)
×
311
                        );
312

313
                        $found = $tokens[$prev]['content'].$tokenContent.$found;
×
314

315
                        if (isset($pattern[($i - 1)]) === true
×
316
                            && $pattern[($i - 1)]['type'] === 'skip'
×
317
                        ) {
318
                            $stackPtr = $prev;
×
319
                        } else {
320
                            $stackPtr = ($prev - 1);
×
321
                        }
322
                    }//end if
323
                } else if ($pattern[$i]['type'] === 'skip') {
×
324
                    // Skip to next piece of relevant code.
325
                    if ($pattern[$i]['to'] === 'parenthesis_closer') {
×
326
                        $to = 'parenthesis_opener';
×
327
                    } else {
328
                        $to = 'scope_opener';
×
329
                    }
330

331
                    // Find the previous opener.
332
                    $next = $phpcsFile->findPrevious(
×
333
                        $ignoreTokens,
×
334
                        $stackPtr,
×
335
                        null,
×
336
                        true
337
                    );
338

339
                    if ($next === false || isset($tokens[$next][$to]) === false) {
×
340
                        // If there was not opener, then we must be
341
                        // using the wrong pattern.
342
                        return false;
×
343
                    }
344

345
                    if ($to === 'parenthesis_opener') {
×
346
                        $found = '{'.$found;
×
347
                    } else {
348
                        $found = '('.$found;
×
349
                    }
350

351
                    $found = '...'.$found;
×
352

353
                    // Skip to the opening token.
354
                    $stackPtr = ($tokens[$next][$to] - 1);
×
355
                } else if ($pattern[$i]['type'] === 'string') {
×
356
                    $found = 'abc';
×
357
                } else if ($pattern[$i]['type'] === 'newline') {
×
358
                    if ($this->ignoreComments === true
×
359
                        && isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true
×
360
                    ) {
361
                        $startComment = $phpcsFile->findPrevious(
×
362
                            Tokens::$commentTokens,
×
363
                            ($stackPtr - 1),
×
364
                            null,
×
365
                            true
366
                        );
367

368
                        if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
×
369
                            $startComment++;
×
370
                        }
371

372
                        $tokenContent = $phpcsFile->getTokensAsString(
×
373
                            $startComment,
×
374
                            ($stackPtr - $startComment + 1)
×
375
                        );
376

377
                        $found    = $tokenContent.$found;
×
378
                        $stackPtr = ($startComment - 1);
×
379
                    }
380

381
                    if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
382
                        if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
×
383
                            $found = $tokens[$stackPtr]['content'].$found;
×
384

385
                            // This may just be an indent that comes after a newline
386
                            // so check the token before to make sure. If it is a newline, we
387
                            // can ignore the error here.
388
                            if (($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar)
×
389
                                && ($this->ignoreComments === true
×
390
                                && isset(Tokens::$commentTokens[$tokens[($stackPtr - 1)]['code']]) === false)
×
391
                            ) {
392
                                $hasError = true;
×
393
                            } else {
394
                                $stackPtr--;
×
395
                            }
396
                        } else {
397
                            $found = 'EOL'.$found;
×
398
                        }
399
                    } else {
400
                        $found    = $tokens[$stackPtr]['content'].$found;
×
401
                        $hasError = true;
×
402
                    }//end if
403

404
                    if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
×
405
                        // Make sure they only have 1 newline.
406
                        $prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
×
407
                        if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
×
408
                            $hasError = true;
×
409
                        }
410
                    }
411
                }//end if
412
            }//end for
413
        }//end if
414

415
        $stackPtr          = $origStackPtr;
×
416
        $lastAddedStackPtr = null;
×
417
        $patternLen        = count($pattern);
×
418

419
        if (($stackPtr + $patternLen - $patternInfo['listen_pos']) > $phpcsFile->numTokens) {
×
420
            // Pattern can never match as there are not enough tokens left in the file.
421
            return false;
×
422
        }
423

424
        for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
×
425
            if (isset($tokens[$stackPtr]) === false) {
×
426
                break;
×
427
            }
428

429
            if ($pattern[$i]['type'] === 'token') {
×
430
                if ($pattern[$i]['token'] === T_WHITESPACE) {
×
431
                    if ($this->ignoreComments === true) {
×
432
                        // If we are ignoring comments, check to see if this current
433
                        // token is a comment. If so skip it.
434
                        if (isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true) {
×
435
                            continue;
×
436
                        }
437

438
                        // If the next token is a comment, the we need to skip the
439
                        // current token as we should allow a space before a
440
                        // comment for readability.
441
                        if (isset($tokens[($stackPtr + 1)]) === true
×
442
                            && isset(Tokens::$commentTokens[$tokens[($stackPtr + 1)]['code']]) === true
×
443
                        ) {
444
                            continue;
×
445
                        }
446
                    }
447

448
                    $tokenContent = '';
×
449
                    if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
450
                        if (isset($pattern[($i + 1)]) === false) {
×
451
                            // This is the last token in the pattern, so just compare
452
                            // the next token of content.
453
                            $tokenContent = $tokens[$stackPtr]['content'];
×
454
                        } else {
455
                            // Get all the whitespace to the next token.
456
                            $next = $phpcsFile->findNext(
×
457
                                Tokens::$emptyTokens,
×
458
                                $stackPtr,
×
459
                                null,
×
460
                                true
461
                            );
462

463
                            $tokenContent = $phpcsFile->getTokensAsString(
×
464
                                $stackPtr,
×
465
                                ($next - $stackPtr)
×
466
                            );
467

468
                            $lastAddedStackPtr = $stackPtr;
×
469
                            $stackPtr          = $next;
×
470
                        }//end if
471

472
                        if ($stackPtr !== $lastAddedStackPtr) {
×
473
                            $found .= $tokenContent;
×
474
                        }
475
                    } else {
476
                        if ($stackPtr !== $lastAddedStackPtr) {
×
477
                            $found            .= $tokens[$stackPtr]['content'];
×
478
                            $lastAddedStackPtr = $stackPtr;
×
479
                        }
480
                    }//end if
481

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

507
                    if ($next === false
508
                        || $tokens[$next]['code'] !== $pattern[$i]['token']
×
509
                    ) {
510
                        // The next important token did not match the pattern.
511
                        return false;
×
512
                    }
513

514
                    if ($lastAddedStackPtr !== null) {
×
515
                        if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
×
516
                            || $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
×
517
                            && isset($tokens[$next]['scope_condition']) === true
×
518
                            && $tokens[$next]['scope_condition'] > $lastAddedStackPtr
×
519
                        ) {
520
                            // This is a brace, 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
                        if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
×
527
                            || $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
×
528
                            && isset($tokens[$next]['parenthesis_owner']) === true
×
529
                            && $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
×
530
                        ) {
531
                            // This is a bracket, but the owner of it is after the current
532
                            // token, which means it does not belong to any token in
533
                            // our pattern. This means the pattern is not for us.
534
                            return false;
×
535
                        }
536
                    }//end if
537

538
                    // If we skipped past some whitespace tokens, then add them
539
                    // to the found string.
540
                    if (($next - $stackPtr) > 0) {
×
541
                        $hasComment = false;
×
542
                        for ($j = $stackPtr; $j < $next; $j++) {
×
543
                            $found .= $tokens[$j]['content'];
×
544
                            if (isset(Tokens::$commentTokens[$tokens[$j]['code']]) === true) {
×
545
                                $hasComment = true;
×
546
                            }
547
                        }
548

549
                        // If we are not ignoring comments, this additional
550
                        // whitespace or comment is not allowed. If we are
551
                        // ignoring comments, there needs to be at least one
552
                        // comment for this to be allowed.
553
                        if ($this->ignoreComments === false
×
554
                            || ($this->ignoreComments === true
×
555
                            && $hasComment === false)
×
556
                        ) {
557
                            $hasError = true;
×
558
                        }
559

560
                        // Even when ignoring comments, we are not allowed to include
561
                        // newlines without the pattern specifying them, so
562
                        // everything should be on the same line.
563
                        if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
×
564
                            $hasError = true;
×
565
                        }
566
                    }//end if
567

568
                    if ($next !== $lastAddedStackPtr) {
×
569
                        $found            .= $tokens[$next]['content'];
×
570
                        $lastAddedStackPtr = $next;
×
571
                    }
572

573
                    if (isset($pattern[($i + 1)]) === true
×
574
                        && $pattern[($i + 1)]['type'] === 'skip'
×
575
                    ) {
576
                        $stackPtr = $next;
×
577
                    } else {
578
                        $stackPtr = ($next + 1);
×
579
                    }
580
                }//end if
581
            } else if ($pattern[$i]['type'] === 'skip') {
×
582
                if ($pattern[$i]['to'] === 'unknown') {
×
583
                    $next = $phpcsFile->findNext(
×
584
                        $pattern[($i + 1)]['token'],
×
585
                        $stackPtr
586
                    );
587

588
                    if ($next === false) {
×
589
                        // Couldn't find the next token, so we must
590
                        // be using the wrong pattern.
591
                        return false;
×
592
                    }
593

594
                    $found   .= '...';
×
595
                    $stackPtr = $next;
×
596
                } else {
597
                    // Find the previous opener.
598
                    $next = $phpcsFile->findPrevious(
×
599
                        Tokens::$blockOpeners,
×
600
                        $stackPtr
601
                    );
602

603
                    if ($next === false
604
                        || isset($tokens[$next][$pattern[$i]['to']]) === false
×
605
                    ) {
606
                        // If there was not opener, then we must
607
                        // be using the wrong pattern.
608
                        return false;
×
609
                    }
610

611
                    $found .= '...';
×
612
                    if ($pattern[$i]['to'] === 'parenthesis_closer') {
×
613
                        $found .= ')';
×
614
                    } else {
615
                        $found .= '}';
×
616
                    }
617

618
                    // Skip to the closing token.
619
                    $stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
×
620
                }//end if
621
            } else if ($pattern[$i]['type'] === 'string') {
×
622
                if ($tokens[$stackPtr]['code'] !== T_STRING) {
×
623
                    $hasError = true;
×
624
                }
625

626
                if ($stackPtr !== $lastAddedStackPtr) {
×
627
                    $found            .= 'abc';
×
628
                    $lastAddedStackPtr = $stackPtr;
×
629
                }
630

631
                $stackPtr++;
×
632
            } else if ($pattern[$i]['type'] === 'newline') {
×
633
                // Find the next token that contains a newline character.
634
                $newline = 0;
×
635
                for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
×
636
                    if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
×
637
                        $newline = $j;
×
638
                        break;
×
639
                    }
640
                }
641

642
                if ($newline === 0) {
×
643
                    // We didn't find a newline character in the rest of the file.
644
                    $next     = ($phpcsFile->numTokens - 1);
×
645
                    $hasError = true;
×
646
                } else {
647
                    if ($this->ignoreComments === false) {
×
648
                        // The newline character cannot be part of a comment.
649
                        if (isset(Tokens::$commentTokens[$tokens[$newline]['code']]) === true) {
×
650
                            $hasError = true;
×
651
                        }
652
                    }
653

654
                    if ($newline === $stackPtr) {
×
655
                        $next = ($stackPtr + 1);
×
656
                    } else {
657
                        // Check that there were no significant tokens that we
658
                        // skipped over to find our newline character.
659
                        $next = $phpcsFile->findNext(
×
660
                            $ignoreTokens,
×
661
                            $stackPtr,
×
662
                            null,
×
663
                            true
664
                        );
665

666
                        if ($next < $newline) {
×
667
                            // We skipped a non-ignored token.
668
                            $hasError = true;
×
669
                        } else {
670
                            $next = ($newline + 1);
×
671
                        }
672
                    }
673
                }//end if
674

675
                if ($stackPtr !== $lastAddedStackPtr) {
×
676
                    $found .= $phpcsFile->getTokensAsString(
×
677
                        $stackPtr,
×
678
                        ($next - $stackPtr)
×
679
                    );
680

681
                    $lastAddedStackPtr = ($next - 1);
×
682
                }
683

684
                $stackPtr = $next;
×
685
            }//end if
686
        }//end for
687

688
        if ($hasError === true) {
×
689
            $error = $this->prepareError($found, $patternCode);
×
690
            $errors[$origStackPtr] = $error;
×
691
        }
692

693
        return $errors;
×
694

695
    }//end processPattern()
696

697

698
    /**
699
     * Prepares an error for the specified patternCode.
700
     *
701
     * @param string $found       The actual found string in the code.
702
     * @param string $patternCode The expected pattern code.
703
     *
704
     * @return string The error message.
705
     */
706
    protected function prepareError($found, $patternCode)
×
707
    {
708
        $found    = str_replace("\r\n", '\n', $found);
×
709
        $found    = str_replace("\n", '\n', $found);
×
710
        $found    = str_replace("\r", '\n', $found);
×
711
        $found    = str_replace("\t", '\t', $found);
×
712
        $found    = str_replace('EOL', '\n', $found);
×
713
        $expected = str_replace('EOL', '\n', $patternCode);
×
714

715
        $error = "Expected \"$expected\"; found \"$found\"";
×
716

717
        return $error;
×
718

719
    }//end prepareError()
720

721

722
    /**
723
     * Returns the patterns that should be checked.
724
     *
725
     * @return string[]
726
     */
727
    abstract protected function getPatterns();
728

729

730
    /**
731
     * Registers any supplementary tokens that this test might wish to process.
732
     *
733
     * A sniff may wish to register supplementary tests when it wishes to group
734
     * an arbitrary validation that cannot be performed using a pattern, with
735
     * other pattern tests.
736
     *
737
     * @return int[]
738
     * @see    processSupplementary()
739
     */
740
    protected function registerSupplementary()
×
741
    {
742
        return [];
×
743

744
    }//end registerSupplementary()
745

746

747
    /**
748
     * Processes any tokens registered with registerSupplementary().
749
     *
750
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where to
751
     *                                               process the skip.
752
     * @param int                         $stackPtr  The position in the tokens stack to
753
     *                                               process.
754
     *
755
     * @return void
756
     * @see    registerSupplementary()
757
     */
UNCOV
758
    protected function processSupplementary(File $phpcsFile, $stackPtr)
×
759
    {
760

761
    }//end processSupplementary()
×
762

763

764
    /**
765
     * Parses a pattern string into an array of pattern steps.
766
     *
767
     * @param string $pattern The pattern to parse.
768
     *
769
     * @return array The parsed pattern array.
770
     * @see    createSkipPattern()
771
     * @see    createTokenPattern()
772
     */
773
    private function parse($pattern)
×
774
    {
775
        $patterns   = [];
×
776
        $length     = strlen($pattern);
×
777
        $lastToken  = 0;
×
778
        $firstToken = 0;
×
779

780
        for ($i = 0; $i < $length; $i++) {
×
781
            $specialPattern = false;
×
782
            $isLastChar     = ($i === ($length - 1));
×
783
            $oldFirstToken  = $firstToken;
×
784

785
            if (substr($pattern, $i, 3) === '...') {
×
786
                // It's a skip pattern. The skip pattern requires the
787
                // content of the token in the "from" position and the token
788
                // to skip to.
789
                $specialPattern = $this->createSkipPattern($pattern, ($i - 1));
×
790
                $lastToken      = ($i - $firstToken);
×
791
                $firstToken     = ($i + 3);
×
792
                $i += 2;
×
793

794
                if ($specialPattern['to'] !== 'unknown') {
×
795
                    $firstToken++;
×
796
                }
797
            } else if (substr($pattern, $i, 3) === 'abc') {
×
798
                $specialPattern = ['type' => 'string'];
×
799
                $lastToken      = ($i - $firstToken);
×
800
                $firstToken     = ($i + 3);
×
801
                $i += 2;
×
802
            } else if (substr($pattern, $i, 3) === 'EOL') {
×
803
                $specialPattern = ['type' => 'newline'];
×
804
                $lastToken      = ($i - $firstToken);
×
805
                $firstToken     = ($i + 3);
×
806
                $i += 2;
×
807
            }//end if
808

809
            if ($specialPattern !== false || $isLastChar === true) {
×
810
                // If we are at the end of the string, don't worry about a limit.
811
                if ($isLastChar === true) {
×
812
                    // Get the string from the end of the last skip pattern, if any,
813
                    // to the end of the pattern string.
814
                    $str = substr($pattern, $oldFirstToken);
×
815
                } else {
816
                    // Get the string from the end of the last special pattern,
817
                    // if any, to the start of this special pattern.
818
                    if ($lastToken === 0) {
×
819
                        // Note that if the last special token was zero characters ago,
820
                        // there will be nothing to process so we can skip this bit.
821
                        // This happens if you have something like: EOL... in your pattern.
822
                        $str = '';
×
823
                    } else {
824
                        $str = substr($pattern, $oldFirstToken, $lastToken);
×
825
                    }
826
                }
827

828
                if ($str !== '') {
×
829
                    $tokenPatterns = $this->createTokenPattern($str);
×
830
                    foreach ($tokenPatterns as $tokenPattern) {
×
831
                        $patterns[] = $tokenPattern;
×
832
                    }
833
                }
834

835
                // Make sure we don't skip the last token.
836
                if ($isLastChar === false && $i === ($length - 1)) {
×
837
                    $i--;
×
838
                }
839
            }//end if
840

841
            // Add the skip pattern *after* we have processed
842
            // all the tokens from the end of the last skip pattern
843
            // to the start of this skip pattern.
844
            if ($specialPattern !== false) {
×
845
                $patterns[] = $specialPattern;
×
846
            }
847
        }//end for
848

849
        return $patterns;
×
850

851
    }//end parse()
852

853

854
    /**
855
     * Creates a skip pattern.
856
     *
857
     * @param string $pattern The pattern being parsed.
858
     * @param int    $from    The token position that the skip pattern starts from.
859
     *
860
     * @return array The pattern step.
861
     * @see    createTokenPattern()
862
     * @see    parse()
863
     */
864
    private function createSkipPattern($pattern, $from)
×
865
    {
866
        $skip = ['type' => 'skip'];
×
867

868
        $nestedParenthesis = 0;
×
869
        $nestedBraces      = 0;
×
870
        for ($start = $from; $start >= 0; $start--) {
×
871
            switch ($pattern[$start]) {
×
872
            case '(':
×
873
                if ($nestedParenthesis === 0) {
×
874
                    $skip['to'] = 'parenthesis_closer';
×
875
                }
876

877
                $nestedParenthesis--;
×
878
                break;
×
879
            case '{':
×
880
                if ($nestedBraces === 0) {
×
881
                    $skip['to'] = 'scope_closer';
×
882
                }
883

884
                $nestedBraces--;
×
885
                break;
×
886
            case '}':
×
887
                $nestedBraces++;
×
888
                break;
×
889
            case ')':
×
890
                $nestedParenthesis++;
×
891
                break;
×
892
            }//end switch
893

894
            if (isset($skip['to']) === true) {
×
895
                break;
×
896
            }
897
        }//end for
898

899
        if (isset($skip['to']) === false) {
×
900
            $skip['to'] = 'unknown';
×
901
        }
902

903
        return $skip;
×
904

905
    }//end createSkipPattern()
906

907

908
    /**
909
     * Creates a token pattern.
910
     *
911
     * @param string $str The tokens string that the pattern should match.
912
     *
913
     * @return array The pattern step.
914
     * @see    createSkipPattern()
915
     * @see    parse()
916
     */
917
    private function createTokenPattern($str)
×
918
    {
919
        // Don't add a space after the closing php tag as it will add a new
920
        // whitespace token.
921
        $tokenizer = new PHP('<?php '.$str.'?>', null);
×
922

923
        // Remove the <?php tag from the front and the end php tag from the back.
924
        $tokens = $tokenizer->getTokens();
×
925
        $tokens = array_slice($tokens, 1, (count($tokens) - 2));
×
926

927
        $patterns = [];
×
928
        foreach ($tokens as $patternInfo) {
×
929
            $patterns[] = [
×
930
                'type'  => 'token',
×
931
                'token' => $patternInfo['code'],
×
932
                'value' => $patternInfo['content'],
×
933
            ];
934
        }
935

936
        return $patterns;
×
937

938
    }//end createTokenPattern()
939

940

941
}//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

© 2025 Coveralls, Inc