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

PHPCSStandards / PHP_CodeSniffer / 14469966128

15 Apr 2025 12:55PM UTC coverage: 77.607% (-0.01%) from 77.621%
14469966128

push

github

web-flow
Merge pull request #996 from PHPCSStandards/phpcs-4.0/feature/test-code-should-be-in-tests

Ruleset: remove test specific code

19356 of 24941 relevant lines covered (77.61%)

78.6 hits per line

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

85.36
/src/Ruleset.php
1
<?php
2
/**
3
 * Stores the rules used to check and fix files.
4
 *
5
 * A ruleset object directly maps to a ruleset XML file.
6
 *
7
 * @author    Greg Sherwood <gsherwood@squiz.net>
8
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
9
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
10
 */
11

12
namespace PHP_CodeSniffer;
13

14
use InvalidArgumentException;
15
use PHP_CodeSniffer\Exceptions\RuntimeException;
16
use PHP_CodeSniffer\Sniffs\DeprecatedSniff;
17
use PHP_CodeSniffer\Util\Common;
18
use PHP_CodeSniffer\Util\MessageCollector;
19
use PHP_CodeSniffer\Util\Standards;
20
use RecursiveDirectoryIterator;
21
use RecursiveIteratorIterator;
22
use ReflectionClass;
23
use stdClass;
24

25
class Ruleset
26
{
27

28
    /**
29
     * The name of the coding standard being used.
30
     *
31
     * If a top-level standard includes other standards, or sniffs
32
     * from other standards, only the name of the top-level standard
33
     * will be stored in here.
34
     *
35
     * If multiple top-level standards are being loaded into
36
     * a single ruleset object, this will store a comma separated list
37
     * of the top-level standard names.
38
     *
39
     * @var string
40
     */
41
    public $name = '';
42

43
    /**
44
     * A list of file paths for the ruleset files being used.
45
     *
46
     * @var string[]
47
     */
48
    public $paths = [];
49

50
    /**
51
     * A list of regular expressions used to ignore specific sniffs for files and folders.
52
     *
53
     * Is also used to set global exclude patterns.
54
     * The key is the regular expression and the value is the type
55
     * of ignore pattern (absolute or relative).
56
     *
57
     * @var array<string, array>
58
     */
59
    public $ignorePatterns = [];
60

61
    /**
62
     * A list of regular expressions used to include specific sniffs for files and folders.
63
     *
64
     * The key is the sniff code and the value is an array with
65
     * the key being a regular expression and the value is the type
66
     * of ignore pattern (absolute or relative).
67
     *
68
     * @var array<string, array<string, string>>
69
     */
70
    public $includePatterns = [];
71

72
    /**
73
     * An array of sniff objects that are being used to check files.
74
     *
75
     * The key is the fully qualified name of the sniff class
76
     * and the value is the sniff object.
77
     *
78
     * @var array<string, \PHP_CodeSniffer\Sniffs\Sniff>
79
     */
80
    public $sniffs = [];
81

82
    /**
83
     * A mapping of sniff codes to fully qualified class names.
84
     *
85
     * The key is the sniff code and the value
86
     * is the fully qualified name of the sniff class.
87
     *
88
     * @var array<string, string>
89
     */
90
    public $sniffCodes = [];
91

92
    /**
93
     * An array of token types and the sniffs that are listening for them.
94
     *
95
     * The key is the token name being listened for and the value
96
     * is the sniff object.
97
     *
98
     * @var array<int, array<string, array<string, mixed>>>
99
     */
100
    public $tokenListeners = [];
101

102
    /**
103
     * An array of rules from the ruleset.xml file.
104
     *
105
     * It may be empty, indicating that the ruleset does not override
106
     * any of the default sniff settings.
107
     *
108
     * @var array<string, mixed>
109
     */
110
    public $ruleset = [];
111

112
    /**
113
     * The directories that the processed rulesets are in.
114
     *
115
     * @var string[]
116
     */
117
    protected $rulesetDirs = [];
118

119
    /**
120
     * The config data for the run.
121
     *
122
     * @var \PHP_CodeSniffer\Config
123
     */
124
    private $config = null;
125

126
    /**
127
     * An array of the names of sniffs which have been marked as deprecated.
128
     *
129
     * The key is the sniff code and the value
130
     * is the fully qualified name of the sniff class.
131
     *
132
     * @var array<string, string>
133
     */
134
    private $deprecatedSniffs = [];
135

136
    /**
137
     * Message collector object.
138
     *
139
     * User-facing messages should be collected via this object for display once the ruleset processing has finished.
140
     *
141
     * The following type of errors should *NOT* be collected, but should still throw their own `RuntimeException`:
142
     * - Errors which could cause other (uncollectable) errors further into the ruleset processing, like a missing autoload file.
143
     * - Errors which are directly aimed at and only intended for sniff developers or integrators
144
     *   (in contrast to ruleset maintainers or end-users).
145
     *
146
     * @var \PHP_CodeSniffer\Util\MessageCollector
147
     */
148
    private $msgCache;
149

150

151
    /**
152
     * Initialise the ruleset that the run will use.
153
     *
154
     * @param \PHP_CodeSniffer\Config $config The config data for the run.
155
     *
156
     * @return void
157
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If blocking errors were encountered when processing the ruleset.
158
     */
159
    public function __construct(Config $config)
44✔
160
    {
161
        $this->config   = $config;
44✔
162
        $restrictions   = $config->sniffs;
44✔
163
        $exclusions     = $config->exclude;
44✔
164
        $sniffs         = [];
44✔
165
        $this->msgCache = new MessageCollector();
44✔
166

167
        $standardPaths = [];
44✔
168
        foreach ($config->standards as $standard) {
44✔
169
            $installed = Standards::getInstalledStandardPath($standard);
44✔
170
            if ($installed === null) {
44✔
171
                $standard = Common::realpath($standard);
14✔
172
                if (is_dir($standard) === true
14✔
173
                    && is_file(Common::realpath($standard.DIRECTORY_SEPARATOR.'ruleset.xml')) === true
14✔
174
                ) {
175
                    $standard = Common::realpath($standard.DIRECTORY_SEPARATOR.'ruleset.xml');
10✔
176
                }
177
            } else {
178
                $standard = $installed;
30✔
179
            }
180

181
            $standardPaths[] = $standard;
44✔
182
        }
183

184
        foreach ($standardPaths as $standard) {
44✔
185
            $ruleset = @simplexml_load_string(file_get_contents($standard));
44✔
186
            if ($ruleset !== false) {
44✔
187
                $standardName = (string) $ruleset['name'];
44✔
188
                if ($this->name !== '') {
44✔
189
                    $this->name .= ', ';
3✔
190
                }
191

192
                $this->name .= $standardName;
44✔
193

194
                // Allow autoloading of custom files inside this standard.
195
                if (isset($ruleset['namespace']) === true) {
44✔
196
                    $namespace = (string) $ruleset['namespace'];
6✔
197
                } else {
198
                    $namespace = basename(dirname($standard));
38✔
199
                }
200

201
                Autoload::addSearchPath(dirname($standard), $namespace);
44✔
202
            }
203

204
            if (PHP_CODESNIFFER_VERBOSITY === 1) {
44✔
205
                echo "Registering sniffs in the $standardName standard... ";
×
206
                if (count($config->standards) > 1 || PHP_CODESNIFFER_VERBOSITY > 2) {
×
207
                    echo PHP_EOL;
×
208
                }
209
            }
210

211
            $sniffs = array_merge($sniffs, $this->processRuleset($standard));
44✔
212
        }//end foreach
213

214
        // Ignore sniff restrictions if caching is on.
215
        if ($config->cache === true) {
44✔
216
            $restrictions = [];
6✔
217
            $exclusions   = [];
6✔
218
        }
219

220
        $sniffRestrictions = [];
44✔
221
        foreach ($restrictions as $sniffCode) {
44✔
222
            $parts     = explode('.', strtolower($sniffCode));
9✔
223
            $sniffName = $parts[0].'\sniffs\\'.$parts[1].'\\'.$parts[2].'sniff';
9✔
224
            $sniffRestrictions[$sniffName] = true;
9✔
225
        }
226

227
        $sniffExclusions = [];
44✔
228
        foreach ($exclusions as $sniffCode) {
44✔
229
            $parts     = explode('.', strtolower($sniffCode));
3✔
230
            $sniffName = $parts[0].'\sniffs\\'.$parts[1].'\\'.$parts[2].'sniff';
3✔
231
            $sniffExclusions[$sniffName] = true;
3✔
232
        }
233

234
        $this->registerSniffs($sniffs, $sniffRestrictions, $sniffExclusions);
44✔
235
        $this->populateTokenListeners();
44✔
236

237
        $numSniffs = count($this->sniffs);
44✔
238
        if (PHP_CODESNIFFER_VERBOSITY === 1) {
44✔
239
            echo "DONE ($numSniffs sniffs registered)".PHP_EOL;
×
240
        }
241

242
        if ($numSniffs === 0) {
44✔
243
            $this->msgCache->add('No sniffs were registered.', MessageCollector::ERROR);
3✔
244
        }
245

246
        $this->displayCachedMessages();
44✔
247

248
    }//end __construct()
14✔
249

250

251
    /**
252
     * Prints a report showing the sniffs contained in a standard.
253
     *
254
     * @return void
255
     */
256
    public function explain()
15✔
257
    {
258
        $sniffs = array_keys($this->sniffCodes);
15✔
259
        sort($sniffs, (SORT_NATURAL | SORT_FLAG_CASE));
15✔
260

261
        $sniffCount = count($sniffs);
15✔
262

263
        // Add a dummy entry to the end so we loop one last time
264
        // and echo out the collected info about the last standard.
265
        $sniffs[] = '';
15✔
266

267
        $summaryLine = PHP_EOL."The $this->name standard contains 1 sniff".PHP_EOL;
15✔
268
        if ($sniffCount !== 1) {
15✔
269
            $summaryLine = str_replace('1 sniff', "$sniffCount sniffs", $summaryLine);
12✔
270
        }
271

272
        echo $summaryLine;
15✔
273

274
        $lastStandard     = null;
15✔
275
        $lastCount        = 0;
15✔
276
        $sniffsInStandard = [];
15✔
277

278
        foreach ($sniffs as $i => $sniff) {
15✔
279
            if ($i === $sniffCount) {
15✔
280
                $currentStandard = null;
15✔
281
            } else {
282
                $currentStandard = substr($sniff, 0, strpos($sniff, '.'));
15✔
283
                if ($lastStandard === null) {
15✔
284
                    $lastStandard = $currentStandard;
15✔
285
                }
286
            }
287

288
            // Reached the first item in the next standard.
289
            // Echo out the info collected from the previous standard.
290
            if ($currentStandard !== $lastStandard) {
15✔
291
                $subTitle = $lastStandard.' ('.$lastCount.' sniff';
15✔
292
                if ($lastCount > 1) {
15✔
293
                    $subTitle .= 's';
12✔
294
                }
295

296
                $subTitle .= ')';
15✔
297

298
                echo PHP_EOL.$subTitle.PHP_EOL;
15✔
299
                echo str_repeat('-', strlen($subTitle)).PHP_EOL;
15✔
300
                echo '  '.implode(PHP_EOL.'  ', $sniffsInStandard).PHP_EOL;
15✔
301

302
                $lastStandard     = $currentStandard;
15✔
303
                $lastCount        = 0;
15✔
304
                $sniffsInStandard = [];
15✔
305

306
                if ($currentStandard === null) {
15✔
307
                    break;
15✔
308
                }
309
            }//end if
310

311
            if (isset($this->deprecatedSniffs[$sniff]) === true) {
15✔
312
                $sniff .= ' *';
3✔
313
            }
314

315
            $sniffsInStandard[] = $sniff;
15✔
316
            ++$lastCount;
15✔
317
        }//end foreach
318

319
        if (count($this->deprecatedSniffs) > 0) {
15✔
320
            echo PHP_EOL.'* Sniffs marked with an asterix are deprecated.'.PHP_EOL;
3✔
321
        }
322

323
    }//end explain()
5✔
324

325

326
    /**
327
     * Checks whether any deprecated sniffs were registered via the ruleset.
328
     *
329
     * @return bool
330
     */
331
    public function hasSniffDeprecations()
57✔
332
    {
333
        return (count($this->deprecatedSniffs) > 0);
57✔
334

335
    }//end hasSniffDeprecations()
336

337

338
    /**
339
     * Prints an information block about deprecated sniffs being used.
340
     *
341
     * @return void
342
     *
343
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When the interface implementation is faulty.
344
     */
345
    public function showSniffDeprecations()
51✔
346
    {
347
        if ($this->hasSniffDeprecations() === false) {
51✔
348
            return;
9✔
349
        }
350

351
        // Don't show deprecation notices in quiet mode, in explain mode
352
        // or when the documentation is being shown.
353
        // Documentation and explain will mark a sniff as deprecated natively
354
        // and also call the Ruleset multiple times which would lead to duplicate
355
        // display of the deprecation messages.
356
        if ($this->config->quiet === true
42✔
357
            || $this->config->explain === true
39✔
358
            || $this->config->generator !== null
42✔
359
        ) {
360
            return;
9✔
361
        }
362

363
        $reportWidth = $this->config->reportWidth;
33✔
364
        // Message takes report width minus the leading dash + two spaces, minus a one space gutter at the end.
365
        $maxMessageWidth = ($reportWidth - 4);
33✔
366
        $maxActualWidth  = 0;
33✔
367

368
        ksort($this->deprecatedSniffs, (SORT_NATURAL | SORT_FLAG_CASE));
33✔
369

370
        $messages        = [];
33✔
371
        $messageTemplate = 'This sniff has been deprecated since %s and will be removed in %s. %s';
33✔
372
        $errorTemplate   = 'ERROR: The %s::%s() method must return a %sstring, received %s';
33✔
373

374
        foreach ($this->deprecatedSniffs as $sniffCode => $className) {
33✔
375
            if (isset($this->sniffs[$className]) === false) {
33✔
376
                // Should only be possible in test situations, but some extra defensive coding is never a bad thing.
377
                continue;
×
378
            }
379

380
            // Verify the interface was implemented correctly.
381
            // Unfortunately can't be safeguarded via type declarations yet.
382
            $deprecatedSince = $this->sniffs[$className]->getDeprecationVersion();
33✔
383
            if (is_string($deprecatedSince) === false) {
33✔
384
                throw new RuntimeException(
3✔
385
                    sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', gettype($deprecatedSince))
3✔
386
                );
2✔
387
            }
388

389
            if ($deprecatedSince === '') {
30✔
390
                throw new RuntimeException(
3✔
391
                    sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', '""')
3✔
392
                );
2✔
393
            }
394

395
            $removedIn = $this->sniffs[$className]->getRemovalVersion();
27✔
396
            if (is_string($removedIn) === false) {
27✔
397
                throw new RuntimeException(
3✔
398
                    sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', gettype($removedIn))
3✔
399
                );
2✔
400
            }
401

402
            if ($removedIn === '') {
24✔
403
                throw new RuntimeException(
3✔
404
                    sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', '""')
3✔
405
                );
2✔
406
            }
407

408
            $customMessage = $this->sniffs[$className]->getDeprecationMessage();
21✔
409
            if (is_string($customMessage) === false) {
21✔
410
                throw new RuntimeException(
3✔
411
                    sprintf($errorTemplate, $className, 'getDeprecationMessage', '', gettype($customMessage))
3✔
412
                );
2✔
413
            }
414

415
            // Truncate the error code if there is not enough report width.
416
            if (strlen($sniffCode) > $maxMessageWidth) {
18✔
417
                $sniffCode = substr($sniffCode, 0, ($maxMessageWidth - 3)).'...';
3✔
418
            }
419

420
            $message        = '-  '."\033[36m".$sniffCode."\033[0m".PHP_EOL;
18✔
421
            $maxActualWidth = max($maxActualWidth, strlen($sniffCode));
18✔
422

423
            // Normalize new line characters in custom message.
424
            $customMessage = preg_replace('`\R`', PHP_EOL, $customMessage);
18✔
425

426
            $notice         = trim(sprintf($messageTemplate, $deprecatedSince, $removedIn, $customMessage));
18✔
427
            $maxActualWidth = max($maxActualWidth, min(strlen($notice), $maxMessageWidth));
18✔
428
            $wrapped        = wordwrap($notice, $maxMessageWidth, PHP_EOL);
18✔
429
            $message       .= '   '.implode(PHP_EOL.'   ', explode(PHP_EOL, $wrapped));
18✔
430

431
            $messages[] = $message;
18✔
432
        }//end foreach
433

434
        if (count($messages) === 0) {
18✔
435
            return;
×
436
        }
437

438
        $summaryLine = "WARNING: The $this->name standard uses 1 deprecated sniff";
18✔
439
        $sniffCount  = count($messages);
18✔
440
        if ($sniffCount !== 1) {
18✔
441
            $summaryLine = str_replace('1 deprecated sniff', "$sniffCount deprecated sniffs", $summaryLine);
6✔
442
        }
443

444
        $maxActualWidth = max($maxActualWidth, min(strlen($summaryLine), $maxMessageWidth));
18✔
445

446
        $summaryLine = wordwrap($summaryLine, $reportWidth, PHP_EOL);
18✔
447
        if ($this->config->colors === true) {
18✔
448
            echo "\033[33m".$summaryLine."\033[0m".PHP_EOL;
×
449
        } else {
450
            echo $summaryLine.PHP_EOL;
18✔
451
        }
452

453
        $messages = implode(PHP_EOL, $messages);
18✔
454
        if ($this->config->colors === false) {
18✔
455
            $messages = Common::stripColors($messages);
18✔
456
        }
457

458
        echo str_repeat('-', min(($maxActualWidth + 4), $reportWidth)).PHP_EOL;
18✔
459
        echo $messages;
18✔
460

461
        $closer = wordwrap('Deprecated sniffs are still run, but will stop working at some point in the future.', $reportWidth, PHP_EOL);
18✔
462
        echo PHP_EOL.PHP_EOL.$closer.PHP_EOL.PHP_EOL;
18✔
463

464
    }//end showSniffDeprecations()
6✔
465

466

467
    /**
468
     * Print any notices encountered while processing the ruleset(s).
469
     *
470
     * Note: these messages aren't shown at the time they are encountered to avoid "one error hiding behind another".
471
     * This way the (end-)user gets to see all of them in one go.
472
     *
473
     * @return void
474
     *
475
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If blocking errors were encountered.
476
     */
477
    private function displayCachedMessages()
50✔
478
    {
479
        // Don't show deprecations/notices/warnings in quiet mode, in explain mode
480
        // or when the documentation is being shown.
481
        // Documentation and explain will call the Ruleset multiple times which
482
        // would lead to duplicate display of the messages.
483
        if ($this->msgCache->containsBlockingErrors() === false
50✔
484
            && ($this->config->quiet === true
47✔
485
            || $this->config->explain === true
45✔
486
            || $this->config->generator !== null)
50✔
487
        ) {
488
            return;
18✔
489
        }
490

491
        $this->msgCache->display();
41✔
492

493
    }//end displayCachedMessages()
8✔
494

495

496
    /**
497
     * Processes a single ruleset and returns a list of the sniffs it represents.
498
     *
499
     * Rules founds within the ruleset are processed immediately, but sniff classes
500
     * are not registered by this method.
501
     *
502
     * @param string $rulesetPath The path to a ruleset XML file.
503
     * @param int    $depth       How many nested processing steps we are in. This
504
     *                            is only used for debug output.
505
     *
506
     * @return string[]
507
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException - If the ruleset path is invalid.
508
     *                                                      - If a specified autoload file could not be found.
509
     */
510
    public function processRuleset($rulesetPath, $depth=0)
50✔
511
    {
512
        $rulesetPath = Common::realpath($rulesetPath);
50✔
513
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
50✔
514
            echo str_repeat("\t", $depth);
×
515
            echo 'Processing ruleset '.Common::stripBasepath($rulesetPath, $this->config->basepath).PHP_EOL;
×
516
        }
517

518
        libxml_use_internal_errors(true);
50✔
519
        $ruleset = simplexml_load_string(file_get_contents($rulesetPath));
50✔
520
        if ($ruleset === false) {
50✔
521
            $errorMsg = "ERROR: Ruleset $rulesetPath is not valid".PHP_EOL;
15✔
522
            $errors   = libxml_get_errors();
15✔
523
            foreach ($errors as $error) {
15✔
524
                $errorMsg .= '- On line '.$error->line.', column '.$error->column.': '.$error->message;
10✔
525
            }
526

527
            libxml_clear_errors();
15✔
528
            throw new RuntimeException($errorMsg);
15✔
529
        }
530

531
        libxml_use_internal_errors(false);
35✔
532

533
        $ownSniffs      = [];
35✔
534
        $includedSniffs = [];
35✔
535
        $excludedSniffs = [];
35✔
536

537
        $this->paths[]       = $rulesetPath;
35✔
538
        $rulesetDir          = dirname($rulesetPath);
35✔
539
        $this->rulesetDirs[] = $rulesetDir;
35✔
540

541
        $sniffDir = $rulesetDir.DIRECTORY_SEPARATOR.'Sniffs';
35✔
542
        if (is_dir($sniffDir) === true) {
35✔
543
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
544
                echo str_repeat("\t", $depth);
×
545
                echo "\tAdding sniff files from ".Common::stripBasepath($sniffDir, $this->config->basepath).' directory'.PHP_EOL;
×
546
            }
547

548
            $ownSniffs = $this->expandSniffDirectory($sniffDir, $depth);
9✔
549
        }
550

551
        // Include custom autoloaders.
552
        foreach ($ruleset->{'autoload'} as $autoload) {
35✔
553
            if ($this->shouldProcessElement($autoload) === false) {
9✔
554
                continue;
6✔
555
            }
556

557
            $autoloadPath = (string) $autoload;
9✔
558

559
            // Try relative autoload paths first.
560
            $relativePath = Common::realpath(dirname($rulesetPath).DIRECTORY_SEPARATOR.$autoloadPath);
9✔
561

562
            if ($relativePath !== false && is_file($relativePath) === true) {
9✔
563
                $autoloadPath = $relativePath;
6✔
564
            } else if (is_file($autoloadPath) === false) {
9✔
565
                throw new RuntimeException('ERROR: The specified autoload file "'.$autoload.'" does not exist');
3✔
566
            }
567

568
            include_once $autoloadPath;
6✔
569

570
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
571
                echo str_repeat("\t", $depth);
×
572
                echo "\t=> included autoloader $autoloadPath".PHP_EOL;
×
573
            }
574
        }//end foreach
575

576
        // Process custom sniff config settings.
577
        foreach ($ruleset->{'config'} as $config) {
32✔
578
            if ($this->shouldProcessElement($config) === false) {
12✔
579
                continue;
6✔
580
            }
581

582
            Config::setConfigData((string) $config['name'], (string) $config['value'], true);
12✔
583
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
584
                echo str_repeat("\t", $depth);
×
585
                echo "\t=> set config value ".(string) $config['name'].': '.(string) $config['value'].PHP_EOL;
×
586
            }
587
        }
588

589
        foreach ($ruleset->rule as $rule) {
32✔
590
            if (isset($rule['ref']) === false
32✔
591
                || $this->shouldProcessElement($rule) === false
32✔
592
            ) {
593
                continue;
9✔
594
            }
595

596
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
32✔
597
                echo str_repeat("\t", $depth);
×
598
                echo "\tProcessing rule \"".$rule['ref'].'"'.PHP_EOL;
×
599
            }
600

601
            $expandedSniffs = $this->expandRulesetReference((string) $rule['ref'], $rulesetDir, $depth);
32✔
602
            $newSniffs      = array_diff($expandedSniffs, $includedSniffs);
32✔
603
            $includedSniffs = array_merge($includedSniffs, $expandedSniffs);
32✔
604

605
            $parts = explode('.', $rule['ref']);
32✔
606
            if (count($parts) === 4
32✔
607
                && $parts[0] !== ''
32✔
608
                && $parts[1] !== ''
32✔
609
                && $parts[2] !== ''
32✔
610
            ) {
611
                $sniffCode = $parts[0].'.'.$parts[1].'.'.$parts[2];
9✔
612
                if (isset($this->ruleset[$sniffCode]['severity']) === true
9✔
613
                    && $this->ruleset[$sniffCode]['severity'] === 0
9✔
614
                ) {
615
                    // This sniff code has already been turned off, but now
616
                    // it is being explicitly included again, so turn it back on.
617
                    $this->ruleset[(string) $rule['ref']]['severity'] = 5;
6✔
618
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
619
                        echo str_repeat("\t", $depth);
×
620
                        echo "\t\t* disabling sniff exclusion for specific message code *".PHP_EOL;
×
621
                        echo str_repeat("\t", $depth);
×
622
                        echo "\t\t=> severity set to 5".PHP_EOL;
4✔
623
                    }
624
                } else if (empty($newSniffs) === false) {
9✔
625
                    $newSniff = $newSniffs[0];
6✔
626
                    if (in_array($newSniff, $ownSniffs, true) === false) {
6✔
627
                        // Including a sniff that hasn't been included higher up, but
628
                        // only including a single message from it. So turn off all messages in
629
                        // the sniff, except this one.
630
                        $this->ruleset[$sniffCode]['severity']            = 0;
6✔
631
                        $this->ruleset[(string) $rule['ref']]['severity'] = 5;
6✔
632
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
633
                            echo str_repeat("\t", $depth);
×
634
                            echo "\t\tExcluding sniff \"".$sniffCode.'" except for "'.$parts[3].'"'.PHP_EOL;
×
635
                        }
636
                    }
637
                }//end if
638
            }//end if
639

640
            if (isset($rule->exclude) === true) {
32✔
641
                foreach ($rule->exclude as $exclude) {
15✔
642
                    if (isset($exclude['name']) === false) {
15✔
643
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
644
                            echo str_repeat("\t", $depth);
×
645
                            echo "\t\t* ignoring empty exclude rule *".PHP_EOL;
×
646
                            echo "\t\t\t=> ".$exclude->asXML().PHP_EOL;
×
647
                        }
648

649
                        continue;
3✔
650
                    }
651

652
                    if ($this->shouldProcessElement($exclude) === false) {
12✔
653
                        continue;
6✔
654
                    }
655

656
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
657
                        echo str_repeat("\t", $depth);
×
658
                        echo "\t\tExcluding rule \"".$exclude['name'].'"'.PHP_EOL;
×
659
                    }
660

661
                    // Check if a single code is being excluded, which is a shortcut
662
                    // for setting the severity of the message to 0.
663
                    $parts = explode('.', $exclude['name']);
12✔
664
                    if (count($parts) === 4) {
12✔
665
                        $this->ruleset[(string) $exclude['name']]['severity'] = 0;
6✔
666
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
667
                            echo str_repeat("\t", $depth);
×
668
                            echo "\t\t=> severity set to 0".PHP_EOL;
4✔
669
                        }
670
                    } else {
671
                        $excludedSniffs = array_merge(
6✔
672
                            $excludedSniffs,
6✔
673
                            $this->expandRulesetReference((string) $exclude['name'], $rulesetDir, ($depth + 1))
6✔
674
                        );
4✔
675
                    }
676
                }//end foreach
677
            }//end if
678

679
            $this->processRule($rule, $newSniffs, $depth);
32✔
680
        }//end foreach
681

682
        // Process custom command line arguments.
683
        $cliArgs = [];
32✔
684
        foreach ($ruleset->{'arg'} as $arg) {
32✔
685
            if ($this->shouldProcessElement($arg) === false) {
9✔
686
                continue;
6✔
687
            }
688

689
            if (isset($arg['name']) === true) {
9✔
690
                $argString = '--'.(string) $arg['name'];
9✔
691
                if (isset($arg['value']) === true) {
9✔
692
                    $argString .= '='.(string) $arg['value'];
9✔
693
                }
694
            } else {
695
                $argString = '-'.(string) $arg['value'];
6✔
696
            }
697

698
            $cliArgs[] = $argString;
9✔
699

700
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
701
                echo str_repeat("\t", $depth);
×
702
                echo "\t=> set command line value $argString".PHP_EOL;
×
703
            }
704
        }//end foreach
705

706
        // Set custom php ini values as CLI args.
707
        foreach ($ruleset->{'ini'} as $arg) {
32✔
708
            if ($this->shouldProcessElement($arg) === false) {
9✔
709
                continue;
6✔
710
            }
711

712
            if (isset($arg['name']) === false) {
9✔
713
                continue;
3✔
714
            }
715

716
            $name      = (string) $arg['name'];
9✔
717
            $argString = $name;
9✔
718
            if (isset($arg['value']) === true) {
9✔
719
                $value      = (string) $arg['value'];
6✔
720
                $argString .= "=$value";
6✔
721
            } else {
722
                $value = 'true';
3✔
723
            }
724

725
            $cliArgs[] = '-d';
9✔
726
            $cliArgs[] = $argString;
9✔
727

728
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
729
                echo str_repeat("\t", $depth);
×
730
                echo "\t=> set PHP ini value $name to $value".PHP_EOL;
×
731
            }
732
        }//end foreach
733

734
        if (empty($this->config->files) === true) {
32✔
735
            // Process hard-coded file paths.
736
            foreach ($ruleset->{'file'} as $file) {
32✔
737
                $file      = (string) $file;
12✔
738
                $cliArgs[] = $file;
12✔
739
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
740
                    echo str_repeat("\t", $depth);
×
741
                    echo "\t=> added \"$file\" to the file list".PHP_EOL;
×
742
                }
743
            }
744
        }
745

746
        if (empty($cliArgs) === false) {
32✔
747
            // Change the directory so all relative paths are worked
748
            // out based on the location of the ruleset instead of
749
            // the location of the user.
750
            $inPhar = Common::isPharFile($rulesetDir);
18✔
751
            if ($inPhar === false) {
18✔
752
                $currentDir = getcwd();
18✔
753
                chdir($rulesetDir);
18✔
754
            }
755

756
            $this->config->setCommandLineValues($cliArgs);
18✔
757

758
            if ($inPhar === false) {
18✔
759
                chdir($currentDir);
18✔
760
            }
761
        }
762

763
        // Process custom ignore pattern rules.
764
        foreach ($ruleset->{'exclude-pattern'} as $pattern) {
32✔
765
            if ($this->shouldProcessElement($pattern) === false) {
6✔
766
                continue;
6✔
767
            }
768

769
            if (isset($pattern['type']) === false) {
6✔
770
                $pattern['type'] = 'absolute';
6✔
771
            }
772

773
            $this->ignorePatterns[(string) $pattern] = (string) $pattern['type'];
6✔
774
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
775
                echo str_repeat("\t", $depth);
×
776
                echo "\t=> added global ".(string) $pattern['type'].' ignore pattern: '.(string) $pattern.PHP_EOL;
×
777
            }
778
        }
779

780
        $includedSniffs = array_unique(array_merge($ownSniffs, $includedSniffs));
32✔
781
        $excludedSniffs = array_unique($excludedSniffs);
32✔
782

783
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
32✔
784
            $included = count($includedSniffs);
×
785
            $excluded = count($excludedSniffs);
×
786
            echo str_repeat("\t", $depth);
×
787
            echo "=> Ruleset processing complete; included $included sniffs and excluded $excluded".PHP_EOL;
×
788
        }
789

790
        // Merge our own sniff list with our externally included
791
        // sniff list, but filter out any excluded sniffs.
792
        $files = [];
32✔
793
        foreach ($includedSniffs as $sniff) {
32✔
794
            if (in_array($sniff, $excludedSniffs, true) === true) {
32✔
795
                continue;
6✔
796
            } else {
797
                $files[] = Common::realpath($sniff);
32✔
798
            }
799
        }
800

801
        return $files;
32✔
802

803
    }//end processRuleset()
804

805

806
    /**
807
     * Expands a directory into a list of sniff files within.
808
     *
809
     * @param string $directory The path to a directory.
810
     * @param int    $depth     How many nested processing steps we are in. This
811
     *                          is only used for debug output.
812
     *
813
     * @return array
814
     */
815
    private function expandSniffDirectory($directory, $depth=0)
6✔
816
    {
817
        $sniffs = [];
6✔
818

819
        $rdi = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
6✔
820
        $di  = new RecursiveIteratorIterator($rdi, 0, RecursiveIteratorIterator::CATCH_GET_CHILD);
6✔
821

822
        $dirLen = strlen($directory);
6✔
823

824
        foreach ($di as $file) {
6✔
825
            $filename = $file->getFilename();
6✔
826

827
            // Skip hidden files.
828
            if (substr($filename, 0, 1) === '.') {
6✔
829
                continue;
6✔
830
            }
831

832
            // We are only interested in PHP and sniff files.
833
            $fileParts = explode('.', $filename);
6✔
834
            if (array_pop($fileParts) !== 'php') {
6✔
835
                continue;
3✔
836
            }
837

838
            $basename = basename($filename, '.php');
6✔
839
            if (substr($basename, -5) !== 'Sniff') {
6✔
840
                continue;
3✔
841
            }
842

843
            $path = $file->getPathname();
6✔
844

845
            // Skip files in hidden directories within the Sniffs directory of this
846
            // standard. We use the offset with strpos() to allow hidden directories
847
            // before, valid example:
848
            // /home/foo/.composer/vendor/squiz/custom_tool/MyStandard/Sniffs/...
849
            if (strpos($path, DIRECTORY_SEPARATOR.'.', $dirLen) !== false) {
6✔
850
                continue;
3✔
851
            }
852

853
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
854
                echo str_repeat("\t", $depth);
×
855
                echo "\t\t=> ".Common::stripBasepath($path, $this->config->basepath).PHP_EOL;
×
856
            }
857

858
            $sniffs[] = $path;
6✔
859
        }//end foreach
860

861
        return $sniffs;
6✔
862

863
    }//end expandSniffDirectory()
864

865

866
    /**
867
     * Expands a ruleset reference into a list of sniff files.
868
     *
869
     * @param string $ref        The reference from the ruleset XML file.
870
     * @param string $rulesetDir The directory of the ruleset XML file, used to
871
     *                           evaluate relative paths.
872
     * @param int    $depth      How many nested processing steps we are in. This
873
     *                           is only used for debug output.
874
     *
875
     * @return array
876
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the reference is invalid.
877
     */
878
    private function expandRulesetReference($ref, $rulesetDir, $depth=0)
53✔
879
    {
880
        // Naming an (external) standard "Internal" is not supported.
881
        if (strtolower($ref) === 'internal') {
53✔
882
            $message  = 'The name "Internal" is reserved for internal use. A PHP_CodeSniffer standard should not be called "Internal".'.PHP_EOL;
3✔
883
            $message .= 'Contact the maintainer of the standard to fix this.';
3✔
884
            $this->msgCache->add($message, MessageCollector::ERROR);
3✔
885

886
            return [];
3✔
887
        }
888

889
        // Ignore internal sniffs codes as they are used to only
890
        // hide and change internal messages.
891
        if (substr($ref, 0, 9) === 'Internal.') {
53✔
892
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
893
                echo str_repeat("\t", $depth);
×
894
                echo "\t\t* ignoring internal sniff code *".PHP_EOL;
×
895
            }
896

897
            return [];
3✔
898
        }
899

900
        // As sniffs can't begin with a full stop, assume references in
901
        // this format are relative paths and attempt to convert them
902
        // to absolute paths. If this fails, let the reference run through
903
        // the normal checks and have it fail as normal.
904
        if (substr($ref, 0, 1) === '.') {
53✔
905
            $realpath = Common::realpath($rulesetDir.'/'.$ref);
12✔
906
            if ($realpath !== false) {
12✔
907
                $ref = $realpath;
6✔
908
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
909
                    echo str_repeat("\t", $depth);
×
910
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
×
911
                }
912
            }
913
        }
914

915
        // As sniffs can't begin with a tilde, assume references in
916
        // this format are relative to the user's home directory.
917
        if (substr($ref, 0, 2) === '~/') {
53✔
918
            $realpath = Common::realpath($ref);
9✔
919
            if ($realpath !== false) {
9✔
920
                $ref = $realpath;
3✔
921
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
922
                    echo str_repeat("\t", $depth);
×
923
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
×
924
                }
925
            }
926
        }
927

928
        if (is_file($ref) === true) {
53✔
929
            if (substr($ref, -9) === 'Sniff.php') {
11✔
930
                // A single external sniff.
931
                $this->rulesetDirs[] = dirname(dirname(dirname($ref)));
11✔
932
                return [$ref];
11✔
933
            }
934
        } else {
935
            // See if this is a whole standard being referenced.
936
            $path = Standards::getInstalledStandardPath($ref);
48✔
937
            if ($path !== null && Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) {
48✔
938
                // If the ruleset exists inside the phar file, use it.
939
                if (file_exists($path.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
×
940
                    $path .= DIRECTORY_SEPARATOR.'ruleset.xml';
×
941
                } else {
942
                    $path = null;
×
943
                }
944
            }
945

946
            if ($path !== null) {
48✔
947
                $ref = $path;
3✔
948
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
949
                    echo str_repeat("\t", $depth);
×
950
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
2✔
951
                }
952
            } else if (is_dir($ref) === false) {
48✔
953
                // Work out the sniff path.
954
                $sepPos = strpos($ref, DIRECTORY_SEPARATOR);
42✔
955
                if ($sepPos !== false) {
42✔
956
                    $stdName = substr($ref, 0, $sepPos);
9✔
957
                    $path    = substr($ref, $sepPos);
9✔
958
                } else {
959
                    $parts   = explode('.', $ref);
33✔
960
                    $stdName = $parts[0];
33✔
961
                    if (count($parts) === 1) {
33✔
962
                        // A whole standard?
963
                        $path = '';
3✔
964
                    } else if (count($parts) === 2) {
30✔
965
                        // A directory of sniffs?
966
                        $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1];
6✔
967
                    } else {
968
                        // A single sniff?
969
                        $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1].DIRECTORY_SEPARATOR.$parts[2].'Sniff.php';
27✔
970
                    }
971
                }
972

973
                $newRef  = false;
42✔
974
                $stdPath = Standards::getInstalledStandardPath($stdName);
42✔
975
                if ($stdPath !== null && $path !== '') {
42✔
976
                    if (Common::isPharFile($stdPath) === true
18✔
977
                        && strpos($stdPath, 'ruleset.xml') === false
18✔
978
                    ) {
979
                        // Phar files can only return the directory,
980
                        // since ruleset can be omitted if building one standard.
981
                        $newRef = Common::realpath($stdPath.$path);
×
982
                    } else {
983
                        $newRef = Common::realpath(dirname($stdPath).$path);
18✔
984
                    }
985
                }
986

987
                if ($newRef === false) {
42✔
988
                    // The sniff is not locally installed, so check if it is being
989
                    // referenced as a remote sniff outside the install. We do this
990
                    // by looking through all directories where we have found ruleset
991
                    // files before, looking for ones for this particular standard,
992
                    // and seeing if it is in there.
993
                    foreach ($this->rulesetDirs as $dir) {
33✔
994
                        if (strtolower(basename($dir)) !== strtolower($stdName)) {
33✔
995
                            continue;
33✔
996
                        }
997

998
                        $newRef = Common::realpath($dir.$path);
×
999

1000
                        if ($newRef !== false) {
×
1001
                            $ref = $newRef;
×
1002
                        }
1003
                    }
1004
                } else {
1005
                    $ref = $newRef;
9✔
1006
                }
1007

1008
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
42✔
1009
                    echo str_repeat("\t", $depth);
×
1010
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
×
1011
                }
1012
            }//end if
1013
        }//end if
1014

1015
        if (is_dir($ref) === true) {
48✔
1016
            if (is_file($ref.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
9✔
1017
                // We are referencing an external coding standard.
1018
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
1019
                    echo str_repeat("\t", $depth);
×
1020
                    echo "\t\t* rule is referencing a standard using directory name; processing *".PHP_EOL;
×
1021
                }
1022

1023
                return $this->processRuleset($ref.DIRECTORY_SEPARATOR.'ruleset.xml', ($depth + 2));
3✔
1024
            } else {
1025
                // We are referencing a whole directory of sniffs.
1026
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
1027
                    echo str_repeat("\t", $depth);
×
1028
                    echo "\t\t* rule is referencing a directory of sniffs *".PHP_EOL;
×
1029
                    echo str_repeat("\t", $depth);
×
1030
                    echo "\t\tAdding sniff files from directory".PHP_EOL;
×
1031
                }
1032

1033
                return $this->expandSniffDirectory($ref, ($depth + 1));
9✔
1034
            }
1035
        } else {
1036
            if (is_file($ref) === false) {
42✔
1037
                $this->msgCache->add("Referenced sniff \"$ref\" does not exist.", MessageCollector::ERROR);
33✔
1038
                return [];
33✔
1039
            }
1040

1041
            if (substr($ref, -9) === 'Sniff.php') {
9✔
1042
                // A single sniff.
1043
                return [$ref];
9✔
1044
            } else {
1045
                // Assume an external ruleset.xml file.
1046
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
1047
                    echo str_repeat("\t", $depth);
×
1048
                    echo "\t\t* rule is referencing a standard using ruleset path; processing *".PHP_EOL;
×
1049
                }
1050

1051
                return $this->processRuleset($ref, ($depth + 2));
3✔
1052
            }
1053
        }//end if
1054

1055
    }//end expandRulesetReference()
1056

1057

1058
    /**
1059
     * Processes a rule from a ruleset XML file, overriding built-in defaults.
1060
     *
1061
     * @param \SimpleXMLElement $rule      The rule object from a ruleset XML file.
1062
     * @param string[]          $newSniffs An array of sniffs that got included by this rule.
1063
     * @param int               $depth     How many nested processing steps we are in.
1064
     *                                     This is only used for debug output.
1065
     *
1066
     * @return void
1067
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If rule settings are invalid.
1068
     */
1069
    private function processRule($rule, $newSniffs, $depth=0)
23✔
1070
    {
1071
        $ref  = (string) $rule['ref'];
23✔
1072
        $todo = [$ref];
23✔
1073

1074
        $parts      = explode('.', $ref);
23✔
1075
        $partsCount = count($parts);
23✔
1076
        if ($partsCount <= 2
23✔
1077
            || $partsCount > count(array_filter($parts))
18✔
1078
            || in_array($ref, $newSniffs) === true
23✔
1079
        ) {
1080
            // We are processing a standard, a category of sniffs or a relative path inclusion.
1081
            foreach ($newSniffs as $sniffFile) {
20✔
1082
                $parts = explode(DIRECTORY_SEPARATOR, $sniffFile);
14✔
1083
                if (count($parts) === 1 && DIRECTORY_SEPARATOR === '\\') {
14✔
1084
                    // Path using forward slashes while running on Windows.
1085
                    $parts = explode('/', $sniffFile);
×
1086
                }
1087

1088
                $sniffName     = array_pop($parts);
14✔
1089
                $sniffCategory = array_pop($parts);
14✔
1090
                array_pop($parts);
14✔
1091
                $sniffStandard = array_pop($parts);
14✔
1092
                $todo[]        = $sniffStandard.'.'.$sniffCategory.'.'.substr($sniffName, 0, -9);
14✔
1093
            }
1094
        }
1095

1096
        foreach ($todo as $code) {
23✔
1097
            // Custom severity.
1098
            if (isset($rule->severity) === true
23✔
1099
                && $this->shouldProcessElement($rule->severity) === true
23✔
1100
            ) {
1101
                if (isset($this->ruleset[$code]) === false) {
9✔
1102
                    $this->ruleset[$code] = [];
9✔
1103
                }
1104

1105
                $this->ruleset[$code]['severity'] = (int) $rule->severity;
9✔
1106
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
1107
                    echo str_repeat("\t", $depth);
×
1108
                    echo "\t\t=> severity set to ".(int) $rule->severity;
×
1109
                    if ($code !== $ref) {
×
1110
                        echo " for $code";
×
1111
                    }
1112

1113
                    echo PHP_EOL;
×
1114
                }
1115
            }
1116

1117
            // Custom message type.
1118
            if (isset($rule->type) === true
23✔
1119
                && $this->shouldProcessElement($rule->type) === true
23✔
1120
            ) {
1121
                if (isset($this->ruleset[$code]) === false) {
9✔
1122
                    $this->ruleset[$code] = [];
3✔
1123
                }
1124

1125
                $type = strtolower((string) $rule->type);
9✔
1126
                if ($type !== 'error' && $type !== 'warning') {
9✔
1127
                    $message = "Message type \"$type\" for \"$code\" is invalid; must be \"error\" or \"warning\".";
3✔
1128
                    $this->msgCache->add($message, MessageCollector::ERROR);
3✔
1129
                } else {
1130
                    $this->ruleset[$code]['type'] = $type;
6✔
1131
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1132
                        echo str_repeat("\t", $depth);
×
1133
                        echo "\t\t=> message type set to ".(string) $rule->type;
×
1134
                        if ($code !== $ref) {
×
1135
                            echo " for $code";
×
1136
                        }
1137

1138
                        echo PHP_EOL;
×
1139
                    }
1140
                }
1141
            }//end if
1142

1143
            // Custom message.
1144
            if (isset($rule->message) === true
23✔
1145
                && $this->shouldProcessElement($rule->message) === true
23✔
1146
            ) {
1147
                if (isset($this->ruleset[$code]) === false) {
6✔
1148
                    $this->ruleset[$code] = [];
×
1149
                }
1150

1151
                $this->ruleset[$code]['message'] = (string) $rule->message;
6✔
1152
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1153
                    echo str_repeat("\t", $depth);
×
1154
                    echo "\t\t=> message set to ".(string) $rule->message;
×
1155
                    if ($code !== $ref) {
×
1156
                        echo " for $code";
×
1157
                    }
1158

1159
                    echo PHP_EOL;
×
1160
                }
1161
            }
1162

1163
            // Custom properties.
1164
            if (isset($rule->properties) === true
23✔
1165
                && $this->shouldProcessElement($rule->properties) === true
23✔
1166
            ) {
1167
                $propertyScope = 'standard';
17✔
1168
                if ($code === $ref || substr($ref, -9) === 'Sniff.php') {
17✔
1169
                    $propertyScope = 'sniff';
17✔
1170
                }
1171

1172
                foreach ($rule->properties->property as $prop) {
17✔
1173
                    if ($this->shouldProcessElement($prop) === false) {
17✔
1174
                        continue;
6✔
1175
                    }
1176

1177
                    if (isset($this->ruleset[$code]) === false) {
17✔
1178
                        $this->ruleset[$code] = [
17✔
1179
                            'properties' => [],
17✔
1180
                        ];
11✔
1181
                    } else if (isset($this->ruleset[$code]['properties']) === false) {
12✔
1182
                        $this->ruleset[$code]['properties'] = [];
3✔
1183
                    }
1184

1185
                    $name = (string) $prop['name'];
17✔
1186
                    if (isset($prop['type']) === true
17✔
1187
                        && (string) $prop['type'] === 'array'
17✔
1188
                    ) {
1189
                        if (isset($prop['value']) === true) {
12✔
1190
                            $message  = 'Passing an array of values to a property using a comma-separated string'.PHP_EOL;
×
1191
                            $message .= 'is no longer supported since PHP_CodeSniffer 4.0.0.'.PHP_EOL;
×
1192
                            $message .= "The unsupported syntax was used for property \"$name\"".PHP_EOL;
×
1193
                            $message .= "for sniff \"$code\".".PHP_EOL;
×
1194
                            $message .= 'Pass array values via <element [key="..." ]value="..."> nodes instead.';
×
1195
                            $this->msgCache->add($message, MessageCollector::ERROR);
×
1196

1197
                            continue;
×
1198
                        }
1199

1200
                        $values = [];
12✔
1201
                        if (isset($prop['extend']) === true
12✔
1202
                            && (string) $prop['extend'] === 'true'
12✔
1203
                            && isset($this->ruleset[$code]['properties'][$name]['value']) === true
12✔
1204
                        ) {
1205
                            $values = $this->ruleset[$code]['properties'][$name]['value'];
9✔
1206
                        }
1207

1208
                        if (isset($prop->element) === true) {
12✔
1209
                            $printValue = '';
12✔
1210
                            foreach ($prop->element as $element) {
12✔
1211
                                if ($this->shouldProcessElement($element) === false) {
12✔
1212
                                    continue;
6✔
1213
                                }
1214

1215
                                $value = (string) $element['value'];
12✔
1216
                                if (isset($element['key']) === true) {
12✔
1217
                                    $key          = (string) $element['key'];
3✔
1218
                                    $values[$key] = $value;
3✔
1219
                                    $printValue  .= $key.'=>'.$value.',';
3✔
1220
                                } else {
1221
                                    $values[]    = $value;
12✔
1222
                                    $printValue .= $value.',';
12✔
1223
                                }
1224
                            }
1225

1226
                            $printValue = rtrim($printValue, ',');
12✔
1227
                        }
1228

1229
                        $this->ruleset[$code]['properties'][$name] = [
12✔
1230
                            'value' => $values,
12✔
1231
                            'scope' => $propertyScope,
12✔
1232
                        ];
8✔
1233
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
1234
                            echo str_repeat("\t", $depth);
×
1235
                            echo "\t\t=> array property \"$name\" set to \"$printValue\"";
×
1236
                            if ($code !== $ref) {
×
1237
                                echo " for $code";
×
1238
                            }
1239

1240
                            echo PHP_EOL;
8✔
1241
                        }
1242
                    } else {
1243
                        $this->ruleset[$code]['properties'][$name] = [
17✔
1244
                            'value' => (string) $prop['value'],
17✔
1245
                            'scope' => $propertyScope,
17✔
1246
                        ];
11✔
1247
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
17✔
1248
                            echo str_repeat("\t", $depth);
×
1249
                            echo "\t\t=> property \"$name\" set to \"".(string) $prop['value'].'"';
×
1250
                            if ($code !== $ref) {
×
1251
                                echo " for $code";
×
1252
                            }
1253

1254
                            echo PHP_EOL;
×
1255
                        }
1256
                    }//end if
1257
                }//end foreach
1258
            }//end if
1259

1260
            // Ignore patterns.
1261
            foreach ($rule->{'exclude-pattern'} as $pattern) {
23✔
1262
                if ($this->shouldProcessElement($pattern) === false) {
6✔
1263
                    continue;
6✔
1264
                }
1265

1266
                if (isset($this->ignorePatterns[$code]) === false) {
6✔
1267
                    $this->ignorePatterns[$code] = [];
6✔
1268
                }
1269

1270
                if (isset($pattern['type']) === false) {
6✔
1271
                    $pattern['type'] = 'absolute';
6✔
1272
                }
1273

1274
                $this->ignorePatterns[$code][(string) $pattern] = (string) $pattern['type'];
6✔
1275
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1276
                    echo str_repeat("\t", $depth);
×
1277
                    echo "\t\t=> added rule-specific ".(string) $pattern['type'].' ignore pattern';
×
1278
                    if ($code !== $ref) {
×
1279
                        echo " for $code";
×
1280
                    }
1281

1282
                    echo ': '.(string) $pattern.PHP_EOL;
×
1283
                }
1284
            }//end foreach
1285

1286
            // Include patterns.
1287
            foreach ($rule->{'include-pattern'} as $pattern) {
23✔
1288
                if ($this->shouldProcessElement($pattern) === false) {
6✔
1289
                    continue;
6✔
1290
                }
1291

1292
                if (isset($this->includePatterns[$code]) === false) {
6✔
1293
                    $this->includePatterns[$code] = [];
6✔
1294
                }
1295

1296
                if (isset($pattern['type']) === false) {
6✔
1297
                    $pattern['type'] = 'absolute';
6✔
1298
                }
1299

1300
                $this->includePatterns[$code][(string) $pattern] = (string) $pattern['type'];
6✔
1301
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1302
                    echo str_repeat("\t", $depth);
×
1303
                    echo "\t\t=> added rule-specific ".(string) $pattern['type'].' include pattern';
×
1304
                    if ($code !== $ref) {
×
1305
                        echo " for $code";
×
1306
                    }
1307

1308
                    echo ': '.(string) $pattern.PHP_EOL;
×
1309
                }
1310
            }//end foreach
1311
        }//end foreach
1312

1313
    }//end processRule()
8✔
1314

1315

1316
    /**
1317
     * Determine if an element should be processed or ignored.
1318
     *
1319
     * @param \SimpleXMLElement $element An object from a ruleset XML file.
1320
     *
1321
     * @return bool
1322
     */
1323
    private function shouldProcessElement($element)
20✔
1324
    {
1325
        if (isset($element['phpcbf-only']) === false
20✔
1326
            && isset($element['phpcs-only']) === false
20✔
1327
        ) {
1328
            // No exceptions are being made.
1329
            return true;
20✔
1330
        }
1331

1332
        if (PHP_CODESNIFFER_CBF === true
12✔
1333
            && isset($element['phpcbf-only']) === true
12✔
1334
            && (string) $element['phpcbf-only'] === 'true'
12✔
1335
        ) {
1336
            return true;
6✔
1337
        }
1338

1339
        if (PHP_CODESNIFFER_CBF === false
12✔
1340
            && isset($element['phpcs-only']) === true
12✔
1341
            && (string) $element['phpcs-only'] === 'true'
12✔
1342
        ) {
1343
            return true;
6✔
1344
        }
1345

1346
        return false;
12✔
1347

1348
    }//end shouldProcessElement()
1349

1350

1351
    /**
1352
     * Loads and stores sniffs objects used for sniffing files.
1353
     *
1354
     * @param array $files        Paths to the sniff files to register.
1355
     * @param array $restrictions The sniff class names to restrict the allowed
1356
     *                            listeners to.
1357
     * @param array $exclusions   The sniff class names to exclude from the
1358
     *                            listeners list.
1359
     *
1360
     * @return void
1361
     */
1362
    public function registerSniffs($files, $restrictions, $exclusions)
47✔
1363
    {
1364
        $listeners = [];
47✔
1365

1366
        foreach ($files as $file) {
47✔
1367
            // Work out where the position of /StandardName/Sniffs/... is
1368
            // so we can determine what the class will be called.
1369
            $sniffPos = strrpos($file, DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR);
47✔
1370
            if ($sniffPos === false) {
47✔
1371
                continue;
3✔
1372
            }
1373

1374
            $slashPos = strrpos(substr($file, 0, $sniffPos), DIRECTORY_SEPARATOR);
47✔
1375
            if ($slashPos === false) {
47✔
1376
                continue;
×
1377
            }
1378

1379
            $className   = Autoload::loadFile($file);
47✔
1380
            $compareName = Common::cleanSniffClass($className);
47✔
1381

1382
            // If they have specified a list of sniffs to restrict to, check
1383
            // to see if this sniff is allowed.
1384
            if (empty($restrictions) === false
47✔
1385
                && isset($restrictions[$compareName]) === false
47✔
1386
            ) {
1387
                continue;
6✔
1388
            }
1389

1390
            // If they have specified a list of sniffs to exclude, check
1391
            // to see if this sniff is allowed.
1392
            if (empty($exclusions) === false
47✔
1393
                && isset($exclusions[$compareName]) === true
47✔
1394
            ) {
1395
                continue;
6✔
1396
            }
1397

1398
            // Skip abstract classes.
1399
            $reflection = new ReflectionClass($className);
47✔
1400
            if ($reflection->isAbstract() === true) {
47✔
1401
                continue;
3✔
1402
            }
1403

1404
            if ($reflection->implementsInterface('PHP_CodeSniffer\\Sniffs\\Sniff') === false) {
47✔
1405
                $message  = 'All sniffs must implement the PHP_CodeSniffer\\Sniffs\\Sniff interface.'.PHP_EOL;
3✔
1406
                $message .= "Interface not implemented for sniff $className.".PHP_EOL;
3✔
1407
                $message .= 'Contact the sniff author to fix the sniff.';
3✔
1408
                $this->msgCache->add($message, MessageCollector::ERROR);
3✔
1409
                continue;
3✔
1410
            }
1411

1412
            if ($reflection->hasProperty('supportedTokenizers') === true) {
47✔
1413
                // Using the default value as the class is not yet instantiated and this is not a property which should get changed anyway.
1414
                $value = $reflection->getDefaultProperties()['supportedTokenizers'];
15✔
1415

1416
                if (is_array($value) === true
15✔
1417
                    && empty($value) === false
15✔
1418
                    && in_array('PHP', $value, true) === false
15✔
1419
                ) {
1420
                    if ($reflection->implementsInterface('PHP_CodeSniffer\\Sniffs\\DeprecatedSniff') === true) {
15✔
1421
                        // Silently ignore the sniff if the sniff is marked as deprecated.
1422
                        continue;
15✔
1423
                    }
1424

1425
                    $message  = 'Support for scanning files other than PHP, like CSS/JS files, has been removed in PHP_CodeSniffer 4.0.'.PHP_EOL;
15✔
1426
                    $message .= 'The %s sniff is listening for %s.';
15✔
1427
                    $message  = sprintf($message, Common::getSniffCode($className), implode(', ', $value));
15✔
1428
                    $this->msgCache->add($message, MessageCollector::ERROR);
15✔
1429
                    continue;
15✔
1430
                }
1431
            }//end if
1432

1433
            $listeners[$className] = $className;
47✔
1434

1435
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
47✔
1436
                echo "Registered $className".PHP_EOL;
×
1437
            }
1438
        }//end foreach
1439

1440
        $this->sniffs = $listeners;
47✔
1441

1442
    }//end registerSniffs()
16✔
1443

1444

1445
    /**
1446
     * Populates the array of PHP_CodeSniffer_Sniff objects for this file.
1447
     *
1448
     * @return void
1449
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If sniff registration fails.
1450
     */
1451
    public function populateTokenListeners()
14✔
1452
    {
1453
        // Construct a list of listeners indexed by token being listened for.
1454
        $this->tokenListeners = [];
14✔
1455

1456
        foreach ($this->sniffs as $sniffClass => $sniffObject) {
14✔
1457
            try {
1458
                $sniffCode = Common::getSniffCode($sniffClass);
14✔
1459
            } catch (InvalidArgumentException $e) {
3✔
1460
                $message  = "The sniff $sniffClass does not comply with the PHP_CodeSniffer naming conventions.".PHP_EOL;
3✔
1461
                $message .= 'Contact the sniff author to fix the sniff.';
3✔
1462
                $this->msgCache->add($message, MessageCollector::ERROR);
3✔
1463

1464
                // Unregister the sniff.
1465
                unset($this->sniffs[$sniffClass]);
3✔
1466
                continue;
3✔
1467
            }
1468

1469
            $this->sniffs[$sniffClass]    = new $sniffClass();
14✔
1470
            $this->sniffCodes[$sniffCode] = $sniffClass;
14✔
1471

1472
            if ($this->sniffs[$sniffClass] instanceof DeprecatedSniff) {
14✔
1473
                $this->deprecatedSniffs[$sniffCode] = $sniffClass;
3✔
1474
            }
1475

1476
            // Set custom properties.
1477
            if (isset($this->ruleset[$sniffCode]['properties']) === true) {
14✔
1478
                foreach ($this->ruleset[$sniffCode]['properties'] as $name => $settings) {
11✔
1479
                    $this->setSniffProperty($sniffClass, $name, $settings);
11✔
1480
                }
1481
            }
1482

1483
            $tokens = $this->sniffs[$sniffClass]->register();
14✔
1484
            if (is_array($tokens) === false) {
14✔
1485
                $msg = "The sniff {$sniffClass}::register() method must return an array.";
3✔
1486
                $this->msgCache->add($msg, MessageCollector::ERROR);
3✔
1487

1488
                // Unregister the sniff.
1489
                unset($this->sniffs[$sniffClass], $this->sniffCodes[$sniffCode], $this->deprecatedSniffs[$sniffCode]);
3✔
1490
                continue;
3✔
1491
            }
1492

1493
            $ignorePatterns = [];
14✔
1494
            $patterns       = $this->getIgnorePatterns($sniffCode);
14✔
1495
            foreach ($patterns as $pattern => $type) {
14✔
1496
                $replacements = [
2✔
1497
                    '\\,' => ',',
3✔
1498
                    '*'   => '.*',
2✔
1499
                ];
2✔
1500

1501
                $ignorePatterns[] = strtr($pattern, $replacements);
3✔
1502
            }
1503

1504
            $includePatterns = [];
14✔
1505
            $patterns        = $this->getIncludePatterns($sniffCode);
14✔
1506
            foreach ($patterns as $pattern => $type) {
14✔
1507
                $replacements = [
2✔
1508
                    '\\,' => ',',
3✔
1509
                    '*'   => '.*',
2✔
1510
                ];
2✔
1511

1512
                $includePatterns[] = strtr($pattern, $replacements);
3✔
1513
            }
1514

1515
            foreach ($tokens as $token) {
14✔
1516
                if (isset($this->tokenListeners[$token]) === false) {
14✔
1517
                    $this->tokenListeners[$token] = [];
14✔
1518
                }
1519

1520
                if (isset($this->tokenListeners[$token][$sniffClass]) === false) {
14✔
1521
                    $this->tokenListeners[$token][$sniffClass] = [
14✔
1522
                        'class'   => $sniffClass,
14✔
1523
                        'source'  => $sniffCode,
14✔
1524
                        'ignore'  => $ignorePatterns,
14✔
1525
                        'include' => $includePatterns,
14✔
1526
                    ];
9✔
1527
                }
1528
            }
1529
        }//end foreach
1530

1531
    }//end populateTokenListeners()
5✔
1532

1533

1534
    /**
1535
     * Set a single property for a sniff.
1536
     *
1537
     * @param string $sniffClass The class name of the sniff.
1538
     * @param string $name       The name of the property to change.
1539
     * @param array  $settings   Array with the new value of the property and the scope of the property being set.
1540
     *
1541
     * @return void
1542
     *
1543
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When attempting to set a non-existent property on a sniff
1544
     *                                                      which doesn't declare the property or explicitly supports
1545
     *                                                      dynamic properties.
1546
     */
1547
    public function setSniffProperty($sniffClass, $name, $settings)
44✔
1548
    {
1549
        // Setting a property for a sniff we are not using.
1550
        if (isset($this->sniffs[$sniffClass]) === false) {
44✔
1551
            return;
3✔
1552
        }
1553

1554
        $name         = trim($name);
41✔
1555
        $propertyName = $name;
41✔
1556
        if (substr($propertyName, -2) === '[]') {
41✔
1557
            $propertyName = substr($propertyName, 0, -2);
3✔
1558
        }
1559

1560
        $isSettable  = false;
41✔
1561
        $sniffObject = $this->sniffs[$sniffClass];
41✔
1562
        if (property_exists($sniffObject, $propertyName) === true
41✔
1563
            || ($sniffObject instanceof stdClass) === true
24✔
1564
            || method_exists($sniffObject, '__set') === true
41✔
1565
        ) {
1566
            $isSettable = true;
29✔
1567
        }
1568

1569
        if ($isSettable === false) {
41✔
1570
            if ($settings['scope'] === 'sniff') {
18✔
1571
                $notice  = "Property \"$propertyName\" does not exist on sniff ";
6✔
1572
                $notice .= array_search($sniffClass, $this->sniffCodes, true).'.';
6✔
1573
                $this->msgCache->add($notice, MessageCollector::ERROR);
6✔
1574
            }
1575

1576
            return;
18✔
1577
        }
1578

1579
        $value = $settings['value'];
29✔
1580

1581
        if (is_string($value) === true) {
29✔
1582
            $value = trim($value);
29✔
1583
        }
1584

1585
        if ($value === '') {
29✔
1586
            $value = null;
6✔
1587
        }
1588

1589
        // Special case for booleans.
1590
        if ($value === 'true') {
29✔
1591
            $value = true;
9✔
1592
        } else if ($value === 'false') {
29✔
1593
            $value = false;
9✔
1594
        } else if (substr($name, -2) === '[]') {
29✔
1595
            $name   = $propertyName;
3✔
1596
            $values = [];
3✔
1597
            if ($value !== null) {
3✔
1598
                foreach (explode(',', $value) as $val) {
3✔
1599
                    list($k, $v) = explode('=>', $val.'=>');
3✔
1600
                    if ($v !== '') {
3✔
1601
                        $values[trim($k)] = trim($v);
3✔
1602
                    } else {
1603
                        $values[] = trim($k);
3✔
1604
                    }
1605
                }
1606
            }
1607

1608
            $value = $values;
3✔
1609
        }
1610

1611
        $sniffObject->$name = $value;
29✔
1612

1613
    }//end setSniffProperty()
10✔
1614

1615

1616
    /**
1617
     * Gets the array of ignore patterns.
1618
     *
1619
     * Optionally takes a listener to get ignore patterns specified
1620
     * for that sniff only.
1621
     *
1622
     * @param string $listener The listener to get patterns for. If NULL, all
1623
     *                         patterns are returned.
1624
     *
1625
     * @return array
1626
     */
1627
    public function getIgnorePatterns($listener=null)
20✔
1628
    {
1629
        if ($listener === null) {
20✔
1630
            return $this->ignorePatterns;
3✔
1631
        }
1632

1633
        if (isset($this->ignorePatterns[$listener]) === true) {
17✔
1634
            return $this->ignorePatterns[$listener];
6✔
1635
        }
1636

1637
        return [];
11✔
1638

1639
    }//end getIgnorePatterns()
1640

1641

1642
    /**
1643
     * Gets the array of include patterns.
1644
     *
1645
     * Optionally takes a listener to get include patterns specified
1646
     * for that sniff only.
1647
     *
1648
     * @param string $listener The listener to get patterns for. If NULL, all
1649
     *                         patterns are returned.
1650
     *
1651
     * @return array
1652
     */
1653
    public function getIncludePatterns($listener=null)
20✔
1654
    {
1655
        if ($listener === null) {
20✔
1656
            return $this->includePatterns;
3✔
1657
        }
1658

1659
        if (isset($this->includePatterns[$listener]) === true) {
17✔
1660
            return $this->includePatterns[$listener];
6✔
1661
        }
1662

1663
        return [];
11✔
1664

1665
    }//end getIncludePatterns()
1666

1667

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

© 2026 Coveralls, Inc