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

PHPCSStandards / PHP_CodeSniffer / 14516416464

17 Apr 2025 01:09PM UTC coverage: 77.945% (+0.3%) from 77.666%
14516416464

push

github

web-flow
Merge pull request #1010 from PHPCSStandards/phpcs-4.0/feature/sq-1612-stdout-vs-stderr

(Nearly) All status, debug, and progress output is now sent to STDERR instead of STDOUT

63 of 457 new or added lines in 18 files covered. (13.79%)

1 existing line in 1 file now uncovered.

19455 of 24960 relevant lines covered (77.94%)

78.64 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
    }//end __construct()
67

68

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

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

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

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

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

102
            $this->parsedPatterns[$tokenType][] = $patternArray;
×
103
        }//end foreach
104

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

107
    }//end register()
108

109

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

137
        return $tokenTypes;
×
138

139
    }//end getPatternTokenTypes()
140

141

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

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

164
        return $tokenTypes[$token];
×
165

166
    }//end getListenerTokenPos()
167

168

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

190
        $tokens = $phpcsFile->getTokens();
×
191

192
        if (in_array($tokens[$stackPtr]['code'], $this->supplementaryTokens, true) === true) {
×
193
            $this->processSupplementary($phpcsFile, $stackPtr);
×
194
        }
195

196
        $type = $tokens[$stackPtr]['code'];
×
197

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

204
        $allErrors = [];
×
205

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

220
            foreach ($errors as $stackPtr => $error) {
×
221
                if (isset($this->errorPos[$stackPtr]) === false) {
×
222
                    $this->errorPos[$stackPtr] = true;
×
223
                    $allErrors[$stackPtr]      = $error;
×
224
                }
225
            }
226
        }
227

228
        foreach ($allErrors as $stackPtr => $error) {
×
229
            $phpcsFile->addError($error, $stackPtr, 'Found');
×
230
        }
231

232
    }//end process()
233

234

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

257
        $ignoreTokens = [T_WHITESPACE => T_WHITESPACE];
×
258
        if ($this->ignoreComments === true) {
×
259
            $ignoreTokens += Tokens::$commentTokens;
×
260
        }
261

262
        $origStackPtr = $stackPtr;
×
263
        $hasError     = false;
×
264

265
        if ($patternInfo['listen_pos'] > 0) {
×
266
            $stackPtr--;
×
267

268
            for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
×
269
                if ($pattern[$i]['type'] === 'token') {
×
270
                    if ($pattern[$i]['token'] === T_WHITESPACE) {
×
271
                        if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
272
                            $found = $tokens[$stackPtr]['content'].$found;
×
273
                        }
274

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

294
                        if ($prev === false
×
295
                            || $tokens[$prev]['code'] !== $pattern[$i]['token']
×
296
                        ) {
297
                            return false;
×
298
                        }
299

300
                        // If we skipped past some whitespace tokens, then add them
301
                        // to the found string.
302
                        $tokenContent = $phpcsFile->getTokensAsString(
×
303
                            ($prev + 1),
×
304
                            ($stackPtr - $prev - 1)
×
305
                        );
306

307
                        $found = $tokens[$prev]['content'].$tokenContent.$found;
×
308

309
                        if (isset($pattern[($i - 1)]) === true
×
310
                            && $pattern[($i - 1)]['type'] === 'skip'
×
311
                        ) {
312
                            $stackPtr = $prev;
×
313
                        } else {
314
                            $stackPtr = ($prev - 1);
×
315
                        }
316
                    }//end if
317
                } else if ($pattern[$i]['type'] === 'skip') {
×
318
                    // Skip to next piece of relevant code.
319
                    if ($pattern[$i]['to'] === 'parenthesis_closer') {
×
320
                        $to = 'parenthesis_opener';
×
321
                    } else {
322
                        $to = 'scope_opener';
×
323
                    }
324

325
                    // Find the previous opener.
326
                    $next = $phpcsFile->findPrevious(
×
327
                        $ignoreTokens,
×
328
                        $stackPtr,
×
329
                        null,
×
330
                        true
×
331
                    );
332

333
                    if ($next === false || isset($tokens[$next][$to]) === false) {
×
334
                        // If there was not opener, then we must be
335
                        // using the wrong pattern.
336
                        return false;
×
337
                    }
338

339
                    if ($to === 'parenthesis_opener') {
×
340
                        $found = '{'.$found;
×
341
                    } else {
342
                        $found = '('.$found;
×
343
                    }
344

345
                    $found = '...'.$found;
×
346

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

362
                        if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
×
363
                            $startComment++;
×
364
                        }
365

366
                        $tokenContent = $phpcsFile->getTokensAsString(
×
367
                            $startComment,
×
368
                            ($stackPtr - $startComment + 1)
×
369
                        );
370

371
                        $found    = $tokenContent.$found;
×
372
                        $stackPtr = ($startComment - 1);
×
373
                    }
374

375
                    if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
×
376
                        if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
×
377
                            $found = $tokens[$stackPtr]['content'].$found;
×
378

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

398
                    if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
×
399
                        // Make sure they only have 1 newline.
400
                        $prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
×
401
                        if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
×
402
                            $hasError = true;
×
403
                        }
404
                    }
405
                }//end if
406
            }//end for
407
        }//end if
408

409
        $stackPtr          = $origStackPtr;
×
410
        $lastAddedStackPtr = null;
×
411
        $patternLen        = count($pattern);
×
412

413
        if (($stackPtr + $patternLen - $patternInfo['listen_pos']) > $phpcsFile->numTokens) {
×
414
            // Pattern can never match as there are not enough tokens left in the file.
415
            return false;
×
416
        }
417

418
        for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
×
419
            if (isset($tokens[$stackPtr]) === false) {
×
420
                break;
×
421
            }
422

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

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

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

457
                            $tokenContent = $phpcsFile->getTokensAsString(
×
458
                                $stackPtr,
×
459
                                ($next - $stackPtr)
×
460
                            );
461

462
                            $lastAddedStackPtr = $stackPtr;
×
463
                            $stackPtr          = $next;
×
464
                        }//end if
465

466
                        if ($stackPtr !== $lastAddedStackPtr) {
×
467
                            $found .= $tokenContent;
×
468
                        }
469
                    } else {
470
                        if ($stackPtr !== $lastAddedStackPtr) {
×
471
                            $found            .= $tokens[$stackPtr]['content'];
×
472
                            $lastAddedStackPtr = $stackPtr;
×
473
                        }
474
                    }//end if
475

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

501
                    if ($next === false
×
502
                        || $tokens[$next]['code'] !== $pattern[$i]['token']
×
503
                    ) {
504
                        // The next important token did not match the pattern.
505
                        return false;
×
506
                    }
507

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

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

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

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

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

562
                    if ($next !== $lastAddedStackPtr) {
×
563
                        $found            .= $tokens[$next]['content'];
×
564
                        $lastAddedStackPtr = $next;
×
565
                    }
566

567
                    if (isset($pattern[($i + 1)]) === true
×
568
                        && $pattern[($i + 1)]['type'] === 'skip'
×
569
                    ) {
570
                        $stackPtr = $next;
×
571
                    } else {
572
                        $stackPtr = ($next + 1);
×
573
                    }
574
                }//end if
575
            } else if ($pattern[$i]['type'] === 'skip') {
×
576
                if ($pattern[$i]['to'] === 'unknown') {
×
577
                    $next = $phpcsFile->findNext(
×
578
                        $pattern[($i + 1)]['token'],
×
579
                        $stackPtr
×
580
                    );
581

582
                    if ($next === false) {
×
583
                        // Couldn't find the next token, so we must
584
                        // be using the wrong pattern.
585
                        return false;
×
586
                    }
587

588
                    $found   .= '...';
×
589
                    $stackPtr = $next;
×
590
                } else {
591
                    // Find the previous opener.
592
                    $next = $phpcsFile->findPrevious(
×
593
                        Tokens::$blockOpeners,
×
594
                        $stackPtr
×
595
                    );
596

597
                    if ($next === false
×
598
                        || isset($tokens[$next][$pattern[$i]['to']]) === false
×
599
                    ) {
600
                        // If there was not opener, then we must
601
                        // be using the wrong pattern.
602
                        return false;
×
603
                    }
604

605
                    $found .= '...';
×
606
                    if ($pattern[$i]['to'] === 'parenthesis_closer') {
×
607
                        $found .= ')';
×
608
                    } else {
609
                        $found .= '}';
×
610
                    }
611

612
                    // Skip to the closing token.
613
                    $stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
×
614
                }//end if
615
            } else if ($pattern[$i]['type'] === 'string') {
×
616
                if ($tokens[$stackPtr]['code'] !== T_STRING) {
×
617
                    $hasError = true;
×
618
                }
619

620
                if ($stackPtr !== $lastAddedStackPtr) {
×
621
                    $found            .= 'abc';
×
622
                    $lastAddedStackPtr = $stackPtr;
×
623
                }
624

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

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

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

660
                        if ($next < $newline) {
×
661
                            // We skipped a non-ignored token.
662
                            $hasError = true;
×
663
                        } else {
664
                            $next = ($newline + 1);
×
665
                        }
666
                    }
667
                }//end if
668

669
                if ($stackPtr !== $lastAddedStackPtr) {
×
670
                    $found .= $phpcsFile->getTokensAsString(
×
671
                        $stackPtr,
×
672
                        ($next - $stackPtr)
×
673
                    );
674

675
                    $lastAddedStackPtr = ($next - 1);
×
676
                }
677

678
                $stackPtr = $next;
×
679
            }//end if
680
        }//end for
681

682
        if ($hasError === true) {
×
683
            $error = $this->prepareError($found, $patternCode);
×
684
            $errors[$origStackPtr] = $error;
×
685
        }
686

687
        return $errors;
×
688

689
    }//end processPattern()
690

691

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

709
        $error = "Expected \"$expected\"; found \"$found\"";
×
710

711
        return $error;
×
712

713
    }//end prepareError()
714

715

716
    /**
717
     * Returns the patterns that should be checked.
718
     *
719
     * @return string[]
720
     */
721
    abstract protected function getPatterns();
722

723

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

738
    }//end registerSupplementary()
739

740

741
     /**
742
      * Processes any tokens registered with registerSupplementary().
743
      *
744
      * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where to
745
      *                                               process the skip.
746
      * @param int                         $stackPtr  The position in the tokens stack to
747
      *                                               process.
748
      *
749
      * @return void
750
      * @see    registerSupplementary()
751
      */
752
    protected function processSupplementary(File $phpcsFile, $stackPtr)
×
753
    {
754

755
    }//end processSupplementary()
×
756

757

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

774
        for ($i = 0; $i < $length; $i++) {
×
775
            $specialPattern = false;
×
776
            $isLastChar     = ($i === ($length - 1));
×
777
            $oldFirstToken  = $firstToken;
×
778

779
            if (substr($pattern, $i, 3) === '...') {
×
780
                // It's a skip pattern. The skip pattern requires the
781
                // content of the token in the "from" position and the token
782
                // to skip to.
783
                $specialPattern = $this->createSkipPattern($pattern, ($i - 1));
×
784
                $lastToken      = ($i - $firstToken);
×
785
                $firstToken     = ($i + 3);
×
786
                $i += 2;
×
787

788
                if ($specialPattern['to'] !== 'unknown') {
×
789
                    $firstToken++;
×
790
                }
791
            } else if (substr($pattern, $i, 3) === 'abc') {
×
792
                $specialPattern = ['type' => 'string'];
×
793
                $lastToken      = ($i - $firstToken);
×
794
                $firstToken     = ($i + 3);
×
795
                $i += 2;
×
796
            } else if (substr($pattern, $i, 3) === 'EOL') {
×
797
                $specialPattern = ['type' => 'newline'];
×
798
                $lastToken      = ($i - $firstToken);
×
799
                $firstToken     = ($i + 3);
×
800
                $i += 2;
×
801
            }//end if
802

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

822
                if ($str !== '') {
×
823
                    $tokenPatterns = $this->createTokenPattern($str);
×
824
                    foreach ($tokenPatterns as $tokenPattern) {
×
825
                        $patterns[] = $tokenPattern;
×
826
                    }
827
                }
828

829
                // Make sure we don't skip the last token.
830
                if ($isLastChar === false && $i === ($length - 1)) {
×
831
                    $i--;
×
832
                }
833
            }//end if
834

835
            // Add the skip pattern *after* we have processed
836
            // all the tokens from the end of the last skip pattern
837
            // to the start of this skip pattern.
838
            if ($specialPattern !== false) {
×
839
                $patterns[] = $specialPattern;
×
840
            }
841
        }//end for
842

843
        return $patterns;
×
844

845
    }//end parse()
846

847

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

862
        $nestedParenthesis = 0;
×
863
        $nestedBraces      = 0;
×
864
        for ($start = $from; $start >= 0; $start--) {
×
865
            switch ($pattern[$start]) {
×
866
            case '(':
×
867
                if ($nestedParenthesis === 0) {
×
868
                    $skip['to'] = 'parenthesis_closer';
×
869
                }
870

871
                $nestedParenthesis--;
×
872
                break;
×
873
            case '{':
×
874
                if ($nestedBraces === 0) {
×
875
                    $skip['to'] = 'scope_closer';
×
876
                }
877

878
                $nestedBraces--;
×
879
                break;
×
880
            case '}':
×
881
                $nestedBraces++;
×
882
                break;
×
883
            case ')':
×
884
                $nestedParenthesis++;
×
885
                break;
×
886
            }//end switch
887

888
            if (isset($skip['to']) === true) {
×
889
                break;
×
890
            }
891
        }//end for
892

893
        if (isset($skip['to']) === false) {
×
894
            $skip['to'] = 'unknown';
×
895
        }
896

897
        return $skip;
×
898

899
    }//end createSkipPattern()
900

901

902
    /**
903
     * Creates a token pattern.
904
     *
905
     * @param string $str The tokens string that the pattern should match.
906
     *
907
     * @return array The pattern step.
908
     * @see    createSkipPattern()
909
     * @see    parse()
910
     */
911
    private function createTokenPattern($str)
×
912
    {
913
        // Pause the StatusWriter to silence Tokenizer debug info about the patterns being parsed (which only confuses things).
NEW
914
        StatusWriter::pause();
×
915

916
        // Don't add a space after the closing php tag as it will add a new
917
        // whitespace token.
918
        $tokenizer = new PHP('<?php '.$str.'?>', null);
×
NEW
919
        StatusWriter::resume();
×
920

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

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

934
        return $patterns;
×
935

936
    }//end createTokenPattern()
937

938

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