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

PHPCSStandards / PHP_CodeSniffer / 13823399634

12 Mar 2025 11:18PM UTC coverage: 77.494% (+0.07%) from 77.421%
13823399634

push

github

jrfnl
Ruleset: use `MessageCollector` for pre-existing errors

Notes:

**For the "invalid type" message**

Includes updating the message for the "invalid type" message to mention the reference for which the `type` was (incorrectly) being changed.
This should make it more straight forward for ruleset maintainers to find the problem in their ruleset.
It also makes the message more unique, as this message could occur in multiple places in a ruleset and there was no indication of that in the message previously.

Potential future scope for "invalid type" message

It could be considered to downgrade this message from an `ERROR` to a `NOTICE` as an invalid type is not blocking for running the sniffs, though this could lead to results not being as expected if, for instance, the `-n` flag is being used, which is why I've not changed this at this time.

**For the "register() method must return an array" error

Includes some new assertions which won't run until the test suite supports PHPUnit 10+ (PHPCS 4.0). These tests belong with this commit though, so adding them now anyway.

**For the "setting non-existent property" error

Includes minor adjustment to the error message (removal of "Ruleset invalid" and punctuation).

17 of 21 new or added lines in 1 file covered. (80.95%)

67 existing lines in 1 file now uncovered.

19279 of 24878 relevant lines covered (77.49%)

76.76 hits per line

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

89.41
/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 PHP_CodeSniffer\Exceptions\RuntimeException;
15
use PHP_CodeSniffer\Sniffs\DeprecatedSniff;
16
use PHP_CodeSniffer\Util\Common;
17
use PHP_CodeSniffer\Util\MessageCollector;
18
use PHP_CodeSniffer\Util\Standards;
19
use RecursiveDirectoryIterator;
20
use RecursiveIteratorIterator;
21
use ReflectionClass;
22
use stdClass;
23

24
class Ruleset
25
{
26

27
    /**
28
     * A list of CLI args found while processing.
29
     *
30
     * @var []
31
     */
32
    private $cliArgs = [];
33

34
    /**
35
     * The config data for the run.
36
     *
37
     * @var \PHP_CodeSniffer\Config
38
     */
39
    private $config = null;
40

41
    /**
42
     * A list of regular expressions used to ignore specific sniffs for files and folders.
43
     *
44
     * Is also used to set global exclude patterns.
45
     * The key is the regular expression and the value is the type
46
     * of ignore pattern (absolute or relative).
47
     *
48
     * @var array<string, array>
49
     */
50
    public $ignorePatterns = [];
51

52
    /**
53
     * A list of regular expressions used to include specific sniffs for files and folders.
54
     *
55
     * The key is the sniff code and the value is an array with
56
     * the key being a regular expression and the value is the type
57
     * of ignore pattern (absolute or relative).
58
     *
59
     * @var array<string, array<string, string>>
60
     */
61
    public $includePatterns = [];
62

63
    /**
64
     * The name of the coding standard being used.
65
     *
66
     * If a top-level standard includes other standards, or sniffs
67
     * from other standards, only the name of the top-level standard
68
     * will be stored in here.
69
     *
70
     * If multiple top-level standards are being loaded into
71
     * a single ruleset object, this will store a comma separated list
72
     * of the top-level standard names.
73
     *
74
     * @var string
75
     */
76
    public $name = '';
77

78
    /**
79
     * A list of file paths for the ruleset files being used.
80
     *
81
     * @var string[]
82
     */
83
    public $paths = [];
84

85
    /**
86
     * An array of rules from the ruleset.xml file.
87
     *
88
     * It may be empty, indicating that the ruleset does not override
89
     * any of the default sniff settings.
90
     *
91
     * @var array<string, mixed>
92
     */
93
    public $ruleset = [];
94

95
    /**
96
     * The directories that the processed rulesets are in.
97
     *
98
     * @var string[]
99
     */
100
    protected $rulesetDirs = [];
101

102
    /**
103
     * A mapping of sniff codes to fully qualified class names.
104
     *
105
     * The key is the sniff code and the value
106
     * is the fully qualified name of the sniff class.
107
     *
108
     * @var array<string, string>
109
     */
110
    public $sniffCodes = [];
111

112
    /**
113
     * An array of sniff objects that are being used to check files.
114
     *
115
     * The key is the fully qualified name of the sniff class
116
     * and the value is the sniff object.
117
     *
118
     * @var array<string, \PHP_CodeSniffer\Sniffs\Sniff>
119
     */
120
    public $sniffs = [];
121

122
    /**
123
     * An array of token types and the sniffs that are listening for them.
124
     *
125
     * The key is the token name being listened for and the value
126
     * is the sniff object.
127
     *
128
     * @var array<int, array<string, array<string, mixed>>>
129
     */
130
    public $tokenListeners = [];
131

132
    /**
133
     * An array of the names of sniffs which have been marked as deprecated.
134
     *
135
     * The key is the sniff code and the value
136
     * is the fully qualified name of the sniff class.
137
     *
138
     * @var array<string, string>
139
     */
140
    private $deprecatedSniffs = [];
141

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

156

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

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

187
            $standardPaths[] = $standard;
44✔
188
        }
189

190
        foreach ($standardPaths as $standard) {
44✔
191
            $ruleset = @simplexml_load_string(file_get_contents($standard));
44✔
192
            if ($ruleset !== false) {
44✔
193
                $standardName = (string) $ruleset['name'];
44✔
194
                if ($this->name !== '') {
44✔
195
                    $this->name .= ', ';
3✔
196
                }
197

198
                $this->name .= $standardName;
44✔
199

200
                // Allow autoloading of custom files inside this standard.
201
                if (isset($ruleset['namespace']) === true) {
44✔
202
                    $namespace = (string) $ruleset['namespace'];
6✔
203
                } else {
204
                    $namespace = basename(dirname($standard));
38✔
205
                }
206

207
                Autoload::addSearchPath(dirname($standard), $namespace);
44✔
208
            }
209

210
            if (defined('PHP_CODESNIFFER_IN_TESTS') === true && empty($restrictions) === false) {
44✔
211
                // In unit tests, only register the sniffs that the test wants and not the entire standard.
212
                foreach ($restrictions as $restriction) {
9✔
213
                    $sniffs = array_merge($sniffs, $this->expandRulesetReference($restriction, dirname($standard)));
9✔
214
                }
215

216
                if (empty($sniffs) === true) {
9✔
217
                    // Sniff reference could not be expanded, which probably means this
218
                    // is an installed standard. Let the unit test system take care of
219
                    // setting the correct sniff for testing.
220
                    return;
3✔
221
                }
222

223
                break;
6✔
224
            }
225

226
            if (PHP_CODESNIFFER_VERBOSITY === 1) {
35✔
UNCOV
227
                Common::printStatusMessage("Registering sniffs in the $standardName standard... ", 0, true);
×
UNCOV
228
                if (count($config->standards) > 1 || PHP_CODESNIFFER_VERBOSITY > 2) {
×
UNCOV
229
                    Common::printStatusMessage(PHP_EOL, 0, true);
×
230
                }
231
            }
232

233
            $sniffs = array_merge($sniffs, $this->processRuleset($standard));
35✔
234
        }//end foreach
235

236
        // Ignore sniff restrictions if caching is on.
237
        if ($config->cache === true) {
41✔
238
            $restrictions = [];
6✔
239
            $exclusions   = [];
6✔
240
        }
241

242
        $sniffRestrictions = [];
41✔
243
        foreach ($restrictions as $sniffCode) {
41✔
244
            $parts     = explode('.', strtolower($sniffCode));
6✔
245
            $sniffName = $parts[0].'\sniffs\\'.$parts[1].'\\'.$parts[2].'sniff';
6✔
246
            $sniffRestrictions[$sniffName] = true;
6✔
247
        }
248

249
        $sniffExclusions = [];
41✔
250
        foreach ($exclusions as $sniffCode) {
41✔
251
            $parts     = explode('.', strtolower($sniffCode));
3✔
252
            $sniffName = $parts[0].'\sniffs\\'.$parts[1].'\\'.$parts[2].'sniff';
3✔
253
            $sniffExclusions[$sniffName] = true;
3✔
254
        }
255

256
        $this->registerSniffs($sniffs, $sniffRestrictions, $sniffExclusions);
41✔
257
        $this->populateTokenListeners();
41✔
258

259
        $numSniffs = count($this->sniffs);
41✔
260
        if (PHP_CODESNIFFER_VERBOSITY === 1) {
41✔
UNCOV
261
            Common::printStatusMessage("DONE ($numSniffs sniffs registered)");
×
262
        }
263

264
        if ($numSniffs === 0) {
41✔
265
            $this->msgCache->add('No sniffs were registered.', MessageCollector::ERROR);
3✔
266
        }
267

268
        $this->displayCachedMessages();
41✔
269

270
    }//end __construct()
13✔
271

272

273
    /**
274
     * Get the config that this ruleset is using.
275
     *
276
     * @return \PHP_CodeSniffer\Config
277
     */
UNCOV
278
    public function getConfig()
×
279
    {
UNCOV
280
        return $this->config;
×
281

282
    }//end getConfig()
283

284

285
    /**
286
     * Prints a report showing the sniffs contained in a standard.
287
     *
288
     * @return void
289
     */
290
    public function explain()
18✔
291
    {
292
        $sniffs = array_keys($this->sniffCodes);
18✔
293
        sort($sniffs, (SORT_NATURAL | SORT_FLAG_CASE));
18✔
294

295
        $sniffCount = count($sniffs);
18✔
296

297
        // Add a dummy entry to the end so we loop one last time
298
        // and echo out the collected info about the last standard.
299
        $sniffs[] = '';
18✔
300

301
        $summaryLine = PHP_EOL."The $this->name standard contains 1 sniff".PHP_EOL;
18✔
302
        if ($sniffCount !== 1) {
18✔
303
            $summaryLine = str_replace('1 sniff', "$sniffCount sniffs", $summaryLine);
15✔
304
        }
305

306
        echo $summaryLine;
18✔
307

308
        $lastStandard     = null;
18✔
309
        $lastCount        = 0;
18✔
310
        $sniffsInStandard = [];
18✔
311

312
        foreach ($sniffs as $i => $sniff) {
18✔
313
            if ($i === $sniffCount) {
18✔
314
                $currentStandard = null;
18✔
315
            } else {
316
                $currentStandard = substr($sniff, 0, strpos($sniff, '.'));
18✔
317
                if ($lastStandard === null) {
18✔
318
                    $lastStandard = $currentStandard;
18✔
319
                }
320
            }
321

322
            // Reached the first item in the next standard.
323
            // Echo out the info collected from the previous standard.
324
            if ($currentStandard !== $lastStandard) {
18✔
325
                $subTitle = $lastStandard.' ('.$lastCount.' sniff';
18✔
326
                if ($lastCount > 1) {
18✔
327
                    $subTitle .= 's';
15✔
328
                }
329

330
                $subTitle .= ')';
18✔
331

332
                echo PHP_EOL.$subTitle.PHP_EOL;
18✔
333
                echo str_repeat('-', strlen($subTitle)).PHP_EOL;
18✔
334
                echo '  '.implode(PHP_EOL.'  ', $sniffsInStandard).PHP_EOL;
18✔
335

336
                $lastStandard     = $currentStandard;
18✔
337
                $lastCount        = 0;
18✔
338
                $sniffsInStandard = [];
18✔
339

340
                if ($currentStandard === null) {
18✔
341
                    break;
18✔
342
                }
343
            }//end if
344

345
            if (isset($this->deprecatedSniffs[$sniff]) === true) {
18✔
346
                $sniff .= ' *';
3✔
347
            }
348

349
            $sniffsInStandard[] = $sniff;
18✔
350
            ++$lastCount;
18✔
351
        }//end foreach
352

353
        if (count($this->deprecatedSniffs) > 0) {
18✔
354
            echo PHP_EOL.'* Sniffs marked with an asterix are deprecated.'.PHP_EOL;
3✔
355
        }
356

357
    }//end explain()
6✔
358

359

360
    /**
361
     * Checks whether any deprecated sniffs were registered via the ruleset.
362
     *
363
     * @return bool
364
     */
365
    public function hasSniffDeprecations()
57✔
366
    {
367
        return (count($this->deprecatedSniffs) > 0);
57✔
368

369
    }//end hasSniffDeprecations()
370

371

372
    /**
373
     * Prints an information block about deprecated sniffs being used.
374
     *
375
     * @return void
376
     *
377
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When the interface implementation is faulty.
378
     */
379
    public function showSniffDeprecations()
51✔
380
    {
381
        if ($this->hasSniffDeprecations() === false) {
51✔
382
            return;
3✔
383
        }
384

385
        // Don't show deprecation notices in quiet mode, in explain mode
386
        // or when the documentation is being shown.
387
        // Documentation and explain will mark a sniff as deprecated natively
388
        // and also call the Ruleset multiple times which would lead to duplicate
389
        // display of the deprecation messages.
390
        if ($this->config->quiet === true
48✔
391
            || $this->config->explain === true
45✔
392
            || $this->config->generator !== null
48✔
393
        ) {
394
            return;
9✔
395
        }
396

397
        $reportWidth = $this->config->reportWidth;
39✔
398
        // Message takes report width minus the leading dash + two spaces, minus a one space gutter at the end.
399
        $maxMessageWidth = ($reportWidth - 4);
39✔
400
        $maxActualWidth  = 0;
39✔
401

402
        ksort($this->deprecatedSniffs, (SORT_NATURAL | SORT_FLAG_CASE));
39✔
403

404
        $messages        = [];
39✔
405
        $messageTemplate = 'This sniff has been deprecated since %s and will be removed in %s. %s';
39✔
406
        $errorTemplate   = 'ERROR: The %s::%s() method must return a %sstring, received %s';
39✔
407

408
        foreach ($this->deprecatedSniffs as $sniffCode => $className) {
39✔
409
            if (isset($this->sniffs[$className]) === false) {
39✔
410
                // Should only be possible in test situations, but some extra defensive coding is never a bad thing.
411
                continue;
6✔
412
            }
413

414
            // Verify the interface was implemented correctly.
415
            // Unfortunately can't be safeguarded via type declarations yet.
416
            $deprecatedSince = $this->sniffs[$className]->getDeprecationVersion();
33✔
417
            if (is_string($deprecatedSince) === false) {
33✔
418
                throw new RuntimeException(
3✔
419
                    sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', gettype($deprecatedSince))
3✔
420
                );
2✔
421
            }
422

423
            if ($deprecatedSince === '') {
30✔
424
                throw new RuntimeException(
3✔
425
                    sprintf($errorTemplate, $className, 'getDeprecationVersion', 'non-empty ', '""')
3✔
426
                );
2✔
427
            }
428

429
            $removedIn = $this->sniffs[$className]->getRemovalVersion();
27✔
430
            if (is_string($removedIn) === false) {
27✔
431
                throw new RuntimeException(
3✔
432
                    sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', gettype($removedIn))
3✔
433
                );
2✔
434
            }
435

436
            if ($removedIn === '') {
24✔
437
                throw new RuntimeException(
3✔
438
                    sprintf($errorTemplate, $className, 'getRemovalVersion', 'non-empty ', '""')
3✔
439
                );
2✔
440
            }
441

442
            $customMessage = $this->sniffs[$className]->getDeprecationMessage();
21✔
443
            if (is_string($customMessage) === false) {
21✔
444
                throw new RuntimeException(
3✔
445
                    sprintf($errorTemplate, $className, 'getDeprecationMessage', '', gettype($customMessage))
3✔
446
                );
2✔
447
            }
448

449
            // Truncate the error code if there is not enough report width.
450
            if (strlen($sniffCode) > $maxMessageWidth) {
18✔
451
                $sniffCode = substr($sniffCode, 0, ($maxMessageWidth - 3)).'...';
3✔
452
            }
453

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

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

460
            $notice         = trim(sprintf($messageTemplate, $deprecatedSince, $removedIn, $customMessage));
18✔
461
            $maxActualWidth = max($maxActualWidth, min(strlen($notice), $maxMessageWidth));
18✔
462
            $wrapped        = wordwrap($notice, $maxMessageWidth, PHP_EOL);
18✔
463
            $message       .= '   '.implode(PHP_EOL.'   ', explode(PHP_EOL, $wrapped));
18✔
464

465
            $messages[] = $message;
18✔
466
        }//end foreach
467

468
        if (count($messages) === 0) {
24✔
469
            return;
6✔
470
        }
471

472
        $summaryLine = "WARNING: The $this->name standard uses 1 deprecated sniff";
18✔
473
        $sniffCount  = count($messages);
18✔
474
        if ($sniffCount !== 1) {
18✔
475
            $summaryLine = str_replace('1 deprecated sniff', "$sniffCount deprecated sniffs", $summaryLine);
6✔
476
        }
477

478
        $maxActualWidth = max($maxActualWidth, min(strlen($summaryLine), $maxMessageWidth));
18✔
479

480
        $summaryLine = wordwrap($summaryLine, $reportWidth, PHP_EOL);
18✔
481
        if ($this->config->colors === true) {
18✔
UNCOV
482
            echo "\033[33m".$summaryLine."\033[0m".PHP_EOL;
×
483
        } else {
484
            echo $summaryLine.PHP_EOL;
18✔
485
        }
486

487
        $messages = implode(PHP_EOL, $messages);
18✔
488
        if ($this->config->colors === false) {
18✔
489
            $messages = Common::stripColors($messages);
18✔
490
        }
491

492
        echo str_repeat('-', min(($maxActualWidth + 4), $reportWidth)).PHP_EOL;
18✔
493
        echo $messages;
18✔
494

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

498
    }//end showSniffDeprecations()
6✔
499

500

501
    /**
502
     * Print any notices encountered while processing the ruleset(s).
503
     *
504
     * Note: these messages aren't shown at the time they are encountered to avoid "one error hiding behind another".
505
     * This way the (end-)user gets to see all of them in one go.
506
     *
507
     * @return void
508
     *
509
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If blocking errors were encountered.
510
     */
511
    private function displayCachedMessages()
50✔
512
    {
513
        // Don't show deprecations/notices/warnings in quiet mode, in explain mode
514
        // or when the documentation is being shown.
515
        // Documentation and explain will call the Ruleset multiple times which
516
        // would lead to duplicate display of the messages.
517
        if ($this->msgCache->containsBlockingErrors() === false
50✔
518
            && ($this->config->quiet === true
47✔
519
            || $this->config->explain === true
45✔
520
            || $this->config->generator !== null)
50✔
521
        ) {
522
            return;
18✔
523
        }
524

525
        $this->msgCache->display();
41✔
526

527
    }//end displayCachedMessages()
8✔
528

529

530
    /**
531
     * Processes a single ruleset and returns a list of the sniffs it represents.
532
     *
533
     * Rules founds within the ruleset are processed immediately, but sniff classes
534
     * are not registered by this method.
535
     *
536
     * @param string $rulesetPath The path to a ruleset XML file.
537
     * @param int    $depth       How many nested processing steps we are in. This
538
     *                            is only used for debug output.
539
     *
540
     * @return string[]
541
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException - If the ruleset path is invalid.
542
     *                                                      - If a specified autoload file could not be found.
543
     */
544
    public function processRuleset($rulesetPath, $depth=0)
44✔
545
    {
546
        $rulesetPath = Common::realpath($rulesetPath);
44✔
547
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
44✔
UNCOV
548
            Common::printStatusMessage('Processing ruleset '.Common::stripBasepath($rulesetPath, $this->config->basepath), $depth);
×
549
        }
550

551
        libxml_use_internal_errors(true);
44✔
552
        $ruleset = simplexml_load_string(file_get_contents($rulesetPath));
44✔
553
        if ($ruleset === false) {
44✔
554
            $errorMsg = "ERROR: Ruleset $rulesetPath is not valid".PHP_EOL;
9✔
555
            $errors   = libxml_get_errors();
9✔
556
            foreach ($errors as $error) {
9✔
557
                $errorMsg .= '- On line '.$error->line.', column '.$error->column.': '.$error->message;
6✔
558
            }
559

560
            libxml_clear_errors();
9✔
561
            throw new RuntimeException($errorMsg);
9✔
562
        }
563

564
        libxml_use_internal_errors(false);
35✔
565

566
        $ownSniffs      = [];
35✔
567
        $includedSniffs = [];
35✔
568
        $excludedSniffs = [];
35✔
569

570
        $this->paths[]       = $rulesetPath;
35✔
571
        $rulesetDir          = dirname($rulesetPath);
35✔
572
        $this->rulesetDirs[] = $rulesetDir;
35✔
573

574
        $sniffDir = $rulesetDir.DIRECTORY_SEPARATOR.'Sniffs';
35✔
575
        if (is_dir($sniffDir) === true) {
35✔
576
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
UNCOV
577
                Common::printStatusMessage('Adding sniff files from '.Common::stripBasepath($sniffDir, $this->config->basepath).' directory', ($depth + 1));
×
578
            }
579

580
            $ownSniffs = $this->expandSniffDirectory($sniffDir, $depth);
9✔
581
        }
582

583
        foreach ($ruleset->children() as $child) {
35✔
584
            if ($this->shouldProcessElement($child) === false) {
35✔
585
                continue;
12✔
586
            }
587

588
            switch ($child->getName()) {
35✔
589
            case 'arg':
35✔
590
                // Process custom command line arguments.
591
                if (isset($child['name']) === true) {
9✔
592
                    $argString = '--'.(string) $child['name'];
9✔
593
                    if (isset($child['value']) === true) {
9✔
594
                        $argString .= '='.(string) $child['value'];
9✔
595
                    }
596
                } else {
597
                    $argString = '-'.(string) $child['value'];
6✔
598
                }
599

600
                $this->cliArgs[] = $argString;
9✔
601

602
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
UNCOV
603
                    Common::printStatusMessage("=> set command line value $argString", ($depth + 1));
×
604
                }
605
                break;
9✔
606
            case 'autoload':
35✔
607
                // Include custom autoloaders.
608
                $autoloadPath = (string) $child;
9✔
609

610
                // Try relative autoload paths first.
611
                $relativePath = Common::realPath(dirname($rulesetPath).DIRECTORY_SEPARATOR.$autoloadPath);
9✔
612

613
                if ($relativePath !== false && is_file($relativePath) === true) {
9✔
614
                    $autoloadPath = $relativePath;
6✔
615
                } else if (is_file($autoloadPath) === false) {
9✔
616
                    throw new RuntimeException('ERROR: The specified autoload file "'.$autoloadPath.'" does not exist');
3✔
617
                }
618

619
                include_once $autoloadPath;
6✔
620

621
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
622
                    Common::printStatusMessage("=> included autoloader $autoloadPath", ($depth + 1));
×
623
                }
624
                break;
6✔
625
            case 'config':
32✔
626
                // Process custom sniff config settings.
627
                $this->config->setConfigData((string) $child['name'], (string) $child['value'], true);
12✔
628
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
UNCOV
629
                    Common::printStatusMessage('=> set config value '.(string) $child['name'].': '.(string) $child['value'], ($depth + 1));
×
630
                }
631
                break;
12✔
632
            case 'exclude-pattern':
32✔
633
                // Process custom ignore pattern rules.
634
                if (isset($child['type']) === false) {
6✔
635
                    $child['type'] = 'absolute';
6✔
636
                }
637

638
                $this->ignorePatterns[(string) $child] = (string) $child['type'];
6✔
639
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
640
                    Common::printStatusMessage('=> added global '.(string) $child['type'].' ignore pattern: '.(string) $child, ($depth + 1));
×
641
                }
642
                break;
6✔
643
            case 'file':
32✔
644
                // Process hard-coded file paths.
645
                if (empty($this->config->files) === false) {
12✔
UNCOV
646
                    break;
×
647
                }
648

649
                $file            = (string) $child;
12✔
650
                $this->cliArgs[] = $file;
12✔
651
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
UNCOV
652
                    Common::printStatusMessage("=> added \"$file\" to the file list", ($depth + 1));
×
653
                }
654
                break;
12✔
655
            case 'ini':
32✔
656
                // Set custom php ini values as CLI args.
657
                if (isset($child['name']) === false) {
9✔
658
                    break;
3✔
659
                }
660

661
                $name      = (string) $child['name'];
9✔
662
                $argString = $name;
9✔
663
                if (isset($child['value']) === true) {
9✔
664
                    $value      = (string) $child['value'];
6✔
665
                    $argString .= "=$value";
6✔
666
                } else {
667
                    $value = 'true';
3✔
668
                }
669

670
                $this->cliArgs[] = '-d';
9✔
671
                $this->cliArgs[] = $argString;
9✔
672

673
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
UNCOV
674
                    Common::printStatusMessage("=> set PHP ini value $name to $value", ($depth + 1));
×
675
                }
676
                break;
9✔
677
            case 'rule':
32✔
678
                if (isset($child['ref']) === false) {
32✔
679
                    break;
3✔
680
                }
681

682
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
32✔
UNCOV
683
                    Common::printStatusMessage('Processing rule "'.$child['ref'].'"', ($depth + 1));
×
684
                }
685

686
                $expandedSniffs = $this->expandRulesetReference((string) $child['ref'], $rulesetDir, $depth);
32✔
687
                $newSniffs      = array_diff($expandedSniffs, $includedSniffs);
32✔
688
                $includedSniffs = array_merge($includedSniffs, $expandedSniffs);
32✔
689

690
                $parts = explode('.', $child['ref']);
32✔
691
                if (count($parts) === 4
32✔
692
                    && $parts[0] !== ''
32✔
693
                    && $parts[1] !== ''
32✔
694
                    && $parts[2] !== ''
32✔
695
                ) {
696
                    $sniffCode = $parts[0].'.'.$parts[1].'.'.$parts[2];
9✔
697
                    if (isset($this->ruleset[$sniffCode]['severity']) === true
9✔
698
                        && $this->ruleset[$sniffCode]['severity'] === 0
9✔
699
                    ) {
700
                        // This sniff code has already been turned off, but now
701
                        // it is being explicitly included again, so turn it back on.
702
                        $this->ruleset[(string) $child['ref']]['severity'] = 5;
6✔
703
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
704
                            Common::printStatusMessage('* disabling sniff exclusion for specific message code *', ($depth + 2));
×
705
                            Common::printStatusMessage('=> severity set to 5', ($depth + 2));
4✔
706
                        }
707
                    } else if (empty($newSniffs) === false) {
9✔
708
                        $newSniff = $newSniffs[0];
6✔
709
                        if (in_array($newSniff, $ownSniffs, true) === false) {
6✔
710
                            // Including a sniff that hasn't been included higher up, but
711
                            // only including a single message from it. So turn off all messages in
712
                            // the sniff, except this one.
713
                            $this->ruleset[$sniffCode]['severity'] = 0;
6✔
714
                            $this->ruleset[(string) $child['ref']]['severity'] = 5;
6✔
715
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
716
                                Common::printStatusMessage('Excluding sniff "'.$sniffCode.'" except for "'.$parts[3].'"', ($depth + 2));
×
717
                            }
718
                        }
719
                    }//end if
720
                }//end if
721

722
                if (isset($child->exclude) === true) {
32✔
723
                    foreach ($child->exclude as $exclude) {
15✔
724
                        if (isset($exclude['name']) === false) {
15✔
725
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
UNCOV
726
                                Common::printStatusMessage('* ignoring empty exclude rule *', ($depth + 2));
×
UNCOV
727
                                Common::printStatusMessage('=> '.$exclude->asXML(), ($depth + 3));
×
728
                            }
729

730
                            continue;
3✔
731
                        }
732

733
                        if ($this->shouldProcessElement($exclude) === false) {
12✔
734
                            continue;
6✔
735
                        }
736

737
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
738
                            Common::printStatusMessage('Excluding rule "'.$exclude['name'].'"', ($depth + 2));
×
739
                        }
740

741
                        // Check if a single code is being excluded, which is a shortcut
742
                        // for setting the severity of the message to 0.
743
                        $parts = explode('.', $exclude['name']);
12✔
744
                        if (count($parts) === 4) {
12✔
745
                            $this->ruleset[(string) $exclude['name']]['severity'] = 0;
6✔
746
                            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
747
                                Common::printStatusMessage('=> severity set to 0', ($depth + 2));
4✔
748
                            }
749
                        } else {
750
                            $excludedSniffs = array_merge(
6✔
751
                                $excludedSniffs,
6✔
752
                                $this->expandRulesetReference((string) $exclude['name'], $rulesetDir, ($depth + 1))
6✔
753
                            );
4✔
754
                        }
755
                    }//end foreach
756
                }//end if
757

758
                $this->processRule($child, $newSniffs, $depth);
32✔
759
                break;
32✔
760
            }//end switch
761
        }//end foreach
762

763
        if ($depth === 0 && empty($this->cliArgs) === false) {
32✔
764
            // Change the directory so all relative paths are worked
765
            // out based on the location of the ruleset instead of
766
            // the location of the user.
767
            $inPhar = Common::isPharFile($rulesetDir);
18✔
768
            if ($inPhar === false) {
18✔
769
                $currentDir = getcwd();
18✔
770
                chdir($rulesetDir);
18✔
771
            }
772

773
            $this->config->setCommandLineValues($this->cliArgs);
18✔
774

775
            if ($inPhar === false) {
18✔
776
                chdir($currentDir);
18✔
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✔
UNCOV
784
            $included = count($includedSniffs);
×
UNCOV
785
            $excluded = count($excludedSniffs);
×
UNCOV
786
            Common::printStatusMessage("=> Ruleset processing complete; included $included sniffs and excluded $excluded", $depth);
×
787
        }
788

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

800
        return $files;
32✔
801

802
    }//end processRuleset()
803

804

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

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

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

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

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

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

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

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

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

852
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
853
                Common::printStatusMessage('=> '.Common::stripBasepath($path, $this->config->basepath), ($depth + 2));
×
854
            }
855

856
            $sniffs[] = $path;
6✔
857
        }//end foreach
858

859
        return $sniffs;
6✔
860

861
    }//end expandSniffDirectory()
862

863

864
    /**
865
     * Expands a ruleset reference into a list of sniff files.
866
     *
867
     * @param string $ref        The reference from the ruleset XML file.
868
     * @param string $rulesetDir The directory of the ruleset XML file, used to
869
     *                           evaluate relative paths.
870
     * @param int    $depth      How many nested processing steps we are in. This
871
     *                           is only used for debug output.
872
     *
873
     * @return array
874
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the reference is invalid.
875
     */
876
    private function expandRulesetReference($ref, $rulesetDir, $depth=0)
53✔
877
    {
878
        // Ignore internal sniffs codes as they are used to only
879
        // hide and change internal messages.
880
        if (substr($ref, 0, 9) === 'Internal.') {
53✔
881
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
UNCOV
882
                Common::printStatusMessage('* ignoring internal sniff code *', ($depth + 2));
×
883
            }
884

885
            return [];
3✔
886
        }
887

888
        // As sniffs can't begin with a full stop, assume references in
889
        // this format are relative paths and attempt to convert them
890
        // to absolute paths. If this fails, let the reference run through
891
        // the normal checks and have it fail as normal.
892
        if (substr($ref, 0, 1) === '.') {
53✔
893
            $realpath = Common::realpath($rulesetDir.'/'.$ref);
12✔
894
            if ($realpath !== false) {
12✔
895
                $ref = $realpath;
6✔
896
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
897
                    Common::printStatusMessage('=> '.Common::stripBasepath($ref, $this->config->basepath), ($depth + 2));
×
898
                }
899
            }
900
        }
901

902
        // As sniffs can't begin with a tilde, assume references in
903
        // this format are relative to the user's home directory.
904
        if (substr($ref, 0, 2) === '~/') {
53✔
905
            $realpath = Common::realpath($ref);
9✔
906
            if ($realpath !== false) {
9✔
907
                $ref = $realpath;
3✔
908
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
UNCOV
909
                    Common::printStatusMessage('=> '.Common::stripBasepath($ref, $this->config->basepath), ($depth + 2));
×
910
                }
911
            }
912
        }
913

914
        if (is_file($ref) === true) {
53✔
915
            if (substr($ref, -9) === 'Sniff.php') {
11✔
916
                // A single external sniff.
917
                $this->rulesetDirs[] = dirname(dirname(dirname($ref)));
11✔
918
                return [$ref];
11✔
919
            }
920
        } else {
921
            // See if this is a whole standard being referenced.
922
            $path = Standards::getInstalledStandardPath($ref);
48✔
923
            if ($path !== null && Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) {
48✔
924
                // If the ruleset exists inside the phar file, use it.
UNCOV
925
                if (file_exists($path.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
×
UNCOV
926
                    $path .= DIRECTORY_SEPARATOR.'ruleset.xml';
×
927
                } else {
UNCOV
928
                    $path = null;
×
929
                }
930
            }
931

932
            if ($path !== null) {
48✔
933
                $ref = $path;
6✔
934
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
935
                    Common::printStatusMessage('=> '.Common::stripBasepath($ref, $this->config->basepath), ($depth + 2));
4✔
936
                }
937
            } else if (is_dir($ref) === false) {
45✔
938
                // Work out the sniff path.
939
                $sepPos = strpos($ref, DIRECTORY_SEPARATOR);
39✔
940
                if ($sepPos !== false) {
39✔
941
                    $stdName = substr($ref, 0, $sepPos);
9✔
942
                    $path    = substr($ref, $sepPos);
9✔
943
                } else {
944
                    $parts   = explode('.', $ref);
30✔
945
                    $stdName = $parts[0];
30✔
946
                    if (count($parts) === 1) {
30✔
947
                        // A whole standard?
948
                        $path = '';
3✔
949
                    } else if (count($parts) === 2) {
27✔
950
                        // A directory of sniffs?
951
                        $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1];
6✔
952
                    } else {
953
                        // A single sniff?
954
                        $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1].DIRECTORY_SEPARATOR.$parts[2].'Sniff.php';
24✔
955
                    }
956
                }
957

958
                $newRef  = false;
39✔
959
                $stdPath = Standards::getInstalledStandardPath($stdName);
39✔
960
                if ($stdPath !== null && $path !== '') {
39✔
961
                    if (Common::isPharFile($stdPath) === true
15✔
962
                        && strpos($stdPath, 'ruleset.xml') === false
15✔
963
                    ) {
964
                        // Phar files can only return the directory,
965
                        // since ruleset can be omitted if building one standard.
UNCOV
966
                        $newRef = Common::realpath($stdPath.$path);
×
967
                    } else {
968
                        $newRef = Common::realpath(dirname($stdPath).$path);
15✔
969
                    }
970
                }
971

972
                if ($newRef === false) {
39✔
973
                    // The sniff is not locally installed, so check if it is being
974
                    // referenced as a remote sniff outside the install. We do this
975
                    // by looking through all directories where we have found ruleset
976
                    // files before, looking for ones for this particular standard,
977
                    // and seeing if it is in there.
978
                    foreach ($this->rulesetDirs as $dir) {
33✔
979
                        if (strtolower(basename($dir)) !== strtolower($stdName)) {
33✔
980
                            continue;
33✔
981
                        }
982

UNCOV
983
                        $newRef = Common::realpath($dir.$path);
×
984

UNCOV
985
                        if ($newRef !== false) {
×
UNCOV
986
                            $ref = $newRef;
×
987
                        }
988
                    }
989
                } else {
990
                    $ref = $newRef;
6✔
991
                }
992

993
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
39✔
UNCOV
994
                    Common::printStatusMessage('=> '.Common::stripBasepath($ref, $this->config->basepath), ($depth + 2));
×
995
                }
996
            }//end if
997
        }//end if
998

999
        if (is_dir($ref) === true) {
48✔
1000
            if (is_file($ref.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
9✔
1001
                // We are referencing an external coding standard.
1002
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
UNCOV
1003
                    Common::printStatusMessage('* rule is referencing a standard using directory name; processing *', ($depth + 2));
×
1004
                }
1005

1006
                return $this->processRuleset($ref.DIRECTORY_SEPARATOR.'ruleset.xml', ($depth + 2));
3✔
1007
            } else {
1008
                // We are referencing a whole directory of sniffs.
1009
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
UNCOV
1010
                    Common::printStatusMessage('* rule is referencing a directory of sniffs *', ($depth + 2));
×
UNCOV
1011
                    Common::printStatusMessage('Adding sniff files from directory', ($depth + 2));
×
1012
                }
1013

1014
                return $this->expandSniffDirectory($ref, ($depth + 1));
9✔
1015
            }
1016
        } else {
1017
            if (is_file($ref) === false) {
42✔
1018
                $this->msgCache->add("Referenced sniff \"$ref\" does not exist.", MessageCollector::ERROR);
33✔
1019
                return [];
33✔
1020
            }
1021

1022
            if (substr($ref, -9) === 'Sniff.php') {
9✔
1023
                // A single sniff.
1024
                return [$ref];
6✔
1025
            } else {
1026
                // Assume an external ruleset.xml file.
1027
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
1028
                    Common::printStatusMessage('* rule is referencing a standard using ruleset path; processing *', ($depth + 2));
×
1029
                }
1030

1031
                return $this->processRuleset($ref, ($depth + 2));
6✔
1032
            }
1033
        }//end if
1034

1035
    }//end expandRulesetReference()
1036

1037

1038
    /**
1039
     * Processes a rule from a ruleset XML file, overriding built-in defaults.
1040
     *
1041
     * @param \SimpleXMLElement $rule      The rule object from a ruleset XML file.
1042
     * @param string[]          $newSniffs An array of sniffs that got included by this rule.
1043
     * @param int               $depth     How many nested processing steps we are in.
1044
     *                                     This is only used for debug output.
1045
     *
1046
     * @return void
1047
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If rule settings are invalid.
1048
     */
1049
    private function processRule($rule, $newSniffs, $depth=0)
23✔
1050
    {
1051
        $ref  = (string) $rule['ref'];
23✔
1052
        $todo = [$ref];
23✔
1053

1054
        $parts      = explode('.', $ref);
23✔
1055
        $partsCount = count($parts);
23✔
1056
        if ($partsCount <= 2
23✔
1057
            || $partsCount > count(array_filter($parts))
18✔
1058
            || in_array($ref, $newSniffs) === true
23✔
1059
        ) {
1060
            // We are processing a standard, a category of sniffs or a relative path inclusion.
1061
            foreach ($newSniffs as $sniffFile) {
20✔
1062
                $parts = explode(DIRECTORY_SEPARATOR, $sniffFile);
14✔
1063
                if (count($parts) === 1 && DIRECTORY_SEPARATOR === '\\') {
14✔
1064
                    // Path using forward slashes while running on Windows.
1065
                    $parts = explode('/', $sniffFile);
×
1066
                }
1067

1068
                $sniffName     = array_pop($parts);
14✔
1069
                $sniffCategory = array_pop($parts);
14✔
1070
                array_pop($parts);
14✔
1071
                $sniffStandard = array_pop($parts);
14✔
1072
                $todo[]        = $sniffStandard.'.'.$sniffCategory.'.'.substr($sniffName, 0, -9);
14✔
1073
            }
1074
        }
1075

1076
        foreach ($todo as $code) {
23✔
1077
            // Custom severity.
1078
            if (isset($rule->severity) === true
23✔
1079
                && $this->shouldProcessElement($rule->severity) === true
23✔
1080
            ) {
1081
                if (isset($this->ruleset[$code]) === false) {
9✔
1082
                    $this->ruleset[$code] = [];
9✔
1083
                }
1084

1085
                $this->ruleset[$code]['severity'] = (int) $rule->severity;
9✔
1086
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
UNCOV
1087
                    $statusMessage = '=> severity set to '.(int) $rule->severity;
×
1088
                    if ($code !== $ref) {
×
UNCOV
1089
                        $statusMessage .= " for $code";
×
1090
                    }
1091

UNCOV
1092
                    Common::printStatusMessage($statusMessage, ($depth + 2));
×
1093
                }
1094
            }
1095

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

1104
                $type = strtolower((string) $rule->type);
9✔
1105
                if ($type !== 'error' && $type !== 'warning') {
9✔
1106
                    $message = "Message type \"$type\" for \"$code\" is invalid; must be \"error\" or \"warning\".";
3✔
1107
                    $this->msgCache->add($message, MessageCollector::ERROR);
3✔
1108
                } else {
1109
                    $this->ruleset[$code]['type'] = $type;
6✔
1110
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
NEW
1111
                        $statusMessage = '=> message type set to '.(string) $rule->type;
×
NEW
1112
                        if ($code !== $ref) {
×
NEW
1113
                            $statusMessage .= " for $code";
×
1114
                        }
1115

NEW
1116
                        Common::printStatusMessage($statusMessage, ($depth + 2));
×
1117
                    }
1118
                }
1119
            }//end if
1120

1121
            // Custom message.
1122
            if (isset($rule->message) === true
23✔
1123
                && $this->shouldProcessElement($rule->message) === true
23✔
1124
            ) {
1125
                if (isset($this->ruleset[$code]) === false) {
6✔
UNCOV
1126
                    $this->ruleset[$code] = [];
×
1127
                }
1128

1129
                $this->ruleset[$code]['message'] = (string) $rule->message;
6✔
1130
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
1131
                    $statusMessage = '=> message set to '.(string) $rule->message;
×
UNCOV
1132
                    if ($code !== $ref) {
×
UNCOV
1133
                        $statusMessage .= " for $code";
×
1134
                    }
1135

UNCOV
1136
                    Common::printStatusMessage($statusMessage, ($depth + 2));
×
1137
                }
1138
            }
1139

1140
            // Custom properties.
1141
            if (isset($rule->properties) === true
23✔
1142
                && $this->shouldProcessElement($rule->properties) === true
23✔
1143
            ) {
1144
                $propertyScope = 'standard';
17✔
1145
                if ($code === $ref || substr($ref, -9) === 'Sniff.php') {
17✔
1146
                    $propertyScope = 'sniff';
17✔
1147
                }
1148

1149
                foreach ($rule->properties->property as $prop) {
17✔
1150
                    if ($this->shouldProcessElement($prop) === false) {
17✔
1151
                        continue;
6✔
1152
                    }
1153

1154
                    if (isset($this->ruleset[$code]) === false) {
17✔
1155
                        $this->ruleset[$code] = [
17✔
1156
                            'properties' => [],
17✔
1157
                        ];
11✔
1158
                    } else if (isset($this->ruleset[$code]['properties']) === false) {
12✔
1159
                        $this->ruleset[$code]['properties'] = [];
3✔
1160
                    }
1161

1162
                    $name = (string) $prop['name'];
17✔
1163
                    if (isset($prop['type']) === true
17✔
1164
                        && (string) $prop['type'] === 'array'
17✔
1165
                    ) {
1166
                        $values = [];
12✔
1167
                        if (isset($prop['extend']) === true
12✔
1168
                            && (string) $prop['extend'] === 'true'
12✔
1169
                            && isset($this->ruleset[$code]['properties'][$name]['value']) === true
12✔
1170
                        ) {
1171
                            $values = $this->ruleset[$code]['properties'][$name]['value'];
9✔
1172
                        }
1173

1174
                        if (isset($prop->element) === true) {
12✔
1175
                            $printValue = '';
12✔
1176
                            foreach ($prop->element as $element) {
12✔
1177
                                if ($this->shouldProcessElement($element) === false) {
12✔
1178
                                    continue;
6✔
1179
                                }
1180

1181
                                $value = (string) $element['value'];
12✔
1182
                                if (isset($element['key']) === true) {
12✔
1183
                                    $key          = (string) $element['key'];
3✔
1184
                                    $values[$key] = $value;
3✔
1185
                                    $printValue  .= $key.'=>'.$value.',';
3✔
1186
                                } else {
1187
                                    $values[]    = $value;
12✔
1188
                                    $printValue .= $value.',';
12✔
1189
                                }
1190
                            }
1191

1192
                            $printValue = rtrim($printValue, ',');
12✔
1193
                        }
1194

1195
                        $this->ruleset[$code]['properties'][$name] = [
12✔
1196
                            'value' => $values,
12✔
1197
                            'scope' => $propertyScope,
12✔
1198
                        ];
8✔
1199
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
UNCOV
1200
                            $statusMessage = "=> array property \"$name\" set to \"$printValue\"";
×
UNCOV
1201
                            if ($code !== $ref) {
×
UNCOV
1202
                                $statusMessage .= " for $code";
×
1203
                            }
1204

1205
                            Common::printStatusMessage($statusMessage, ($depth + 2));
8✔
1206
                        }
1207
                    } else {
1208
                        $this->ruleset[$code]['properties'][$name] = [
17✔
1209
                            'value' => (string) $prop['value'],
17✔
1210
                            'scope' => $propertyScope,
17✔
1211
                        ];
11✔
1212
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
17✔
UNCOV
1213
                            $statusMessage = "=> property \"$name\" set to \"".(string) $prop['value'].'"';
×
UNCOV
1214
                            if ($code !== $ref) {
×
UNCOV
1215
                                $statusMessage .= " for $code";
×
1216
                            }
1217

UNCOV
1218
                            Common::printStatusMessage($statusMessage, ($depth + 2));
×
1219
                        }
1220
                    }//end if
1221
                }//end foreach
1222
            }//end if
1223

1224
            // Ignore patterns.
1225
            foreach ($rule->{'exclude-pattern'} as $pattern) {
23✔
1226
                if ($this->shouldProcessElement($pattern) === false) {
6✔
1227
                    continue;
6✔
1228
                }
1229

1230
                if (isset($this->ignorePatterns[$code]) === false) {
6✔
1231
                    $this->ignorePatterns[$code] = [];
6✔
1232
                }
1233

1234
                if (isset($pattern['type']) === false) {
6✔
1235
                    $pattern['type'] = 'absolute';
6✔
1236
                }
1237

1238
                $this->ignorePatterns[$code][(string) $pattern] = (string) $pattern['type'];
6✔
1239
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
1240
                    $statusMessage = '=> added rule-specific '.(string) $pattern['type'].' ignore pattern';
×
UNCOV
1241
                    if ($code !== $ref) {
×
UNCOV
1242
                        $statusMessage .= " for $code";
×
1243
                    }
1244

UNCOV
1245
                    $statusMessage .= ': '.(string) $pattern;
×
UNCOV
1246
                    Common::printStatusMessage($statusMessage, ($depth + 2));
×
1247
                }
1248
            }//end foreach
1249

1250
            // Include patterns.
1251
            foreach ($rule->{'include-pattern'} as $pattern) {
23✔
1252
                if ($this->shouldProcessElement($pattern) === false) {
6✔
1253
                    continue;
6✔
1254
                }
1255

1256
                if (isset($this->includePatterns[$code]) === false) {
6✔
1257
                    $this->includePatterns[$code] = [];
6✔
1258
                }
1259

1260
                if (isset($pattern['type']) === false) {
6✔
1261
                    $pattern['type'] = 'absolute';
6✔
1262
                }
1263

1264
                $this->includePatterns[$code][(string) $pattern] = (string) $pattern['type'];
6✔
1265
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
UNCOV
1266
                    $statusMessage = '=> added rule-specific '.(string) $pattern['type'].' include pattern';
×
UNCOV
1267
                    if ($code !== $ref) {
×
UNCOV
1268
                        $statusMessage .= " for $code";
×
1269
                    }
1270

UNCOV
1271
                    $statusMessage .= ': '.(string) $pattern;
×
UNCOV
1272
                    Common::printStatusMessage($statusMessage, ($depth + 2));
×
1273
                }
1274
            }//end foreach
1275
        }//end foreach
1276

1277
    }//end processRule()
8✔
1278

1279

1280
    /**
1281
     * Determine if an element should be processed or ignored.
1282
     *
1283
     * @param \SimpleXMLElement $element An object from a ruleset XML file.
1284
     *
1285
     * @return bool
1286
     */
1287
    private function shouldProcessElement($element)
20✔
1288
    {
1289
        if (isset($element['phpcbf-only']) === false
20✔
1290
            && isset($element['phpcs-only']) === false
20✔
1291
        ) {
1292
            // No exceptions are being made.
1293
            return true;
20✔
1294
        }
1295

1296
        if (PHP_CODESNIFFER_CBF === true
12✔
1297
            && isset($element['phpcbf-only']) === true
12✔
1298
            && (string) $element['phpcbf-only'] === 'true'
12✔
1299
        ) {
1300
            return true;
6✔
1301
        }
1302

1303
        if (PHP_CODESNIFFER_CBF === false
12✔
1304
            && isset($element['phpcs-only']) === true
12✔
1305
            && (string) $element['phpcs-only'] === 'true'
12✔
1306
        ) {
1307
            return true;
6✔
1308
        }
1309

1310
        return false;
12✔
1311

1312
    }//end shouldProcessElement()
1313

1314

1315
    /**
1316
     * Loads and stores sniffs objects used for sniffing files.
1317
     *
1318
     * @param array $files        Paths to the sniff files to register.
1319
     * @param array $restrictions The sniff class names to restrict the allowed
1320
     *                            listeners to.
1321
     * @param array $exclusions   The sniff class names to exclude from the
1322
     *                            listeners list.
1323
     *
1324
     * @return void
1325
     */
1326
    public function registerSniffs($files, $restrictions, $exclusions)
26✔
1327
    {
1328
        $listeners = [];
26✔
1329

1330
        foreach ($files as $file) {
26✔
1331
            // Work out where the position of /StandardName/Sniffs/... is
1332
            // so we can determine what the class will be called.
1333
            $sniffPos = strrpos($file, DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR);
26✔
1334
            if ($sniffPos === false) {
26✔
1335
                continue;
3✔
1336
            }
1337

1338
            $slashPos = strrpos(substr($file, 0, $sniffPos), DIRECTORY_SEPARATOR);
26✔
1339
            if ($slashPos === false) {
26✔
UNCOV
1340
                continue;
×
1341
            }
1342

1343
            $className   = Autoload::loadFile($file);
26✔
1344
            $compareName = Common::cleanSniffClass($className);
26✔
1345

1346
            // If they have specified a list of sniffs to restrict to, check
1347
            // to see if this sniff is allowed.
1348
            if (empty($restrictions) === false
26✔
1349
                && isset($restrictions[$compareName]) === false
26✔
1350
            ) {
1351
                continue;
6✔
1352
            }
1353

1354
            // If they have specified a list of sniffs to exclude, check
1355
            // to see if this sniff is allowed.
1356
            if (empty($exclusions) === false
26✔
1357
                && isset($exclusions[$compareName]) === true
26✔
1358
            ) {
1359
                continue;
6✔
1360
            }
1361

1362
            // Skip abstract classes.
1363
            $reflection = new ReflectionClass($className);
26✔
1364
            if ($reflection->isAbstract() === true) {
26✔
1365
                continue;
3✔
1366
            }
1367

1368
            $listeners[$className] = $className;
26✔
1369

1370
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
26✔
UNCOV
1371
                Common::printStatusMessage("Registered $className");
×
1372
            }
1373
        }//end foreach
1374

1375
        $this->sniffs = $listeners;
26✔
1376

1377
    }//end registerSniffs()
9✔
1378

1379

1380
    /**
1381
     * Populates the array of PHP_CodeSniffer_Sniff objects for this file.
1382
     *
1383
     * @return void
1384
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If sniff registration fails.
1385
     */
1386
    public function populateTokenListeners()
11✔
1387
    {
1388
        // Construct a list of listeners indexed by token being listened for.
1389
        $this->tokenListeners = [];
11✔
1390

1391
        foreach ($this->sniffs as $sniffClass => $sniffObject) {
11✔
1392
            $this->sniffs[$sniffClass] = null;
11✔
1393
            $this->sniffs[$sniffClass] = new $sniffClass();
11✔
1394

1395
            $sniffCode = Common::getSniffCode($sniffClass);
11✔
1396
            $this->sniffCodes[$sniffCode] = $sniffClass;
11✔
1397

1398
            if ($this->sniffs[$sniffClass] instanceof DeprecatedSniff) {
11✔
1399
                $this->deprecatedSniffs[$sniffCode] = $sniffClass;
3✔
1400
            }
1401

1402
            // Set custom properties.
1403
            if (isset($this->ruleset[$sniffCode]['properties']) === true) {
11✔
1404
                foreach ($this->ruleset[$sniffCode]['properties'] as $name => $settings) {
11✔
1405
                    $this->setSniffProperty($sniffClass, $name, $settings);
11✔
1406
                }
1407
            }
1408

1409
            $tokens = $this->sniffs[$sniffClass]->register();
11✔
1410
            if (is_array($tokens) === false) {
11✔
1411
                $msg = "The sniff {$sniffClass}::register() method must return an array.";
3✔
1412
                $this->msgCache->add($msg, MessageCollector::ERROR);
3✔
1413

1414
                // Unregister the sniff.
1415
                unset($this->sniffs[$sniffClass], $this->sniffCodes[$sniffCode], $this->deprecatedSniffs[$sniffCode]);
3✔
1416
                continue;
3✔
1417
            }
1418

1419
            $ignorePatterns = [];
11✔
1420
            $patterns       = $this->getIgnorePatterns($sniffCode);
11✔
1421
            foreach ($patterns as $pattern => $type) {
11✔
1422
                $replacements = [
2✔
1423
                    '\\,' => ',',
3✔
1424
                    '*'   => '.*',
2✔
1425
                ];
2✔
1426

1427
                $ignorePatterns[] = strtr($pattern, $replacements);
3✔
1428
            }
1429

1430
            $includePatterns = [];
11✔
1431
            $patterns        = $this->getIncludePatterns($sniffCode);
11✔
1432
            foreach ($patterns as $pattern => $type) {
11✔
1433
                $replacements = [
2✔
1434
                    '\\,' => ',',
3✔
1435
                    '*'   => '.*',
2✔
1436
                ];
2✔
1437

1438
                $includePatterns[] = strtr($pattern, $replacements);
3✔
1439
            }
1440

1441
            foreach ($tokens as $token) {
11✔
1442
                if (isset($this->tokenListeners[$token]) === false) {
11✔
1443
                    $this->tokenListeners[$token] = [];
11✔
1444
                }
1445

1446
                if (isset($this->tokenListeners[$token][$sniffClass]) === false) {
11✔
1447
                    $this->tokenListeners[$token][$sniffClass] = [
11✔
1448
                        'class'   => $sniffClass,
11✔
1449
                        'source'  => $sniffCode,
11✔
1450
                        'ignore'  => $ignorePatterns,
11✔
1451
                        'include' => $includePatterns,
11✔
1452
                    ];
7✔
1453
                }
1454
            }
1455
        }//end foreach
1456

1457
    }//end populateTokenListeners()
4✔
1458

1459

1460
    /**
1461
     * Set a single property for a sniff.
1462
     *
1463
     * @param string $sniffClass The class name of the sniff.
1464
     * @param string $name       The name of the property to change.
1465
     * @param array  $settings   Array with the new value of the property and the scope of the property being set.
1466
     *
1467
     * @return void
1468
     *
1469
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When attempting to set a non-existent property on a sniff
1470
     *                                                      which doesn't declare the property or explicitly supports
1471
     *                                                      dynamic properties.
1472
     */
1473
    public function setSniffProperty($sniffClass, $name, $settings)
44✔
1474
    {
1475
        // Setting a property for a sniff we are not using.
1476
        if (isset($this->sniffs[$sniffClass]) === false) {
44✔
1477
            return;
3✔
1478
        }
1479

1480
        $name         = trim($name);
41✔
1481
        $propertyName = $name;
41✔
1482
        if (substr($propertyName, -2) === '[]') {
41✔
1483
            $propertyName = substr($propertyName, 0, -2);
3✔
1484
        }
1485

1486
        $isSettable  = false;
41✔
1487
        $sniffObject = $this->sniffs[$sniffClass];
41✔
1488
        if (property_exists($sniffObject, $propertyName) === true
41✔
1489
            || ($sniffObject instanceof stdClass) === true
24✔
1490
            || method_exists($sniffObject, '__set') === true
41✔
1491
        ) {
1492
            $isSettable = true;
29✔
1493
        }
1494

1495
        if ($isSettable === false) {
41✔
1496
            if ($settings['scope'] === 'sniff') {
18✔
1497
                $notice  = "Property \"$propertyName\" does not exist on sniff ";
6✔
1498
                $notice .= array_search($sniffClass, $this->sniffCodes, true).'.';
6✔
1499
                $this->msgCache->add($notice, MessageCollector::ERROR);
6✔
1500
            }
1501

1502
            return;
18✔
1503
        }
1504

1505
        $value = $settings['value'];
29✔
1506

1507
        if (is_string($value) === true) {
29✔
1508
            $value = trim($value);
29✔
1509
        }
1510

1511
        if ($value === '') {
29✔
1512
            $value = null;
6✔
1513
        }
1514

1515
        // Special case for booleans.
1516
        if ($value === 'true') {
29✔
1517
            $value = true;
9✔
1518
        } else if ($value === 'false') {
29✔
1519
            $value = false;
9✔
1520
        } else if (substr($name, -2) === '[]') {
29✔
1521
            $name   = $propertyName;
3✔
1522
            $values = [];
3✔
1523
            if ($value !== null) {
3✔
1524
                foreach (explode(',', $value) as $val) {
3✔
1525
                    list($k, $v) = explode('=>', $val.'=>');
3✔
1526
                    if ($v !== '') {
3✔
1527
                        $values[trim($k)] = trim($v);
3✔
1528
                    } else {
1529
                        $values[] = trim($k);
3✔
1530
                    }
1531
                }
1532
            }
1533

1534
            $value = $values;
3✔
1535
        }
1536

1537
        $sniffObject->$name = $value;
29✔
1538

1539
    }//end setSniffProperty()
10✔
1540

1541

1542
    /**
1543
     * Gets the array of ignore patterns.
1544
     *
1545
     * Optionally takes a listener to get ignore patterns specified
1546
     * for that sniff only.
1547
     *
1548
     * @param string $listener The listener to get patterns for. If NULL, all
1549
     *                         patterns are returned.
1550
     *
1551
     * @return array
1552
     */
1553
    public function getIgnorePatterns($listener=null)
20✔
1554
    {
1555
        if ($listener === null) {
20✔
1556
            return $this->ignorePatterns;
3✔
1557
        }
1558

1559
        if (isset($this->ignorePatterns[$listener]) === true) {
17✔
1560
            return $this->ignorePatterns[$listener];
6✔
1561
        }
1562

1563
        return [];
11✔
1564

1565
    }//end getIgnorePatterns()
1566

1567

1568
    /**
1569
     * Gets the array of include patterns.
1570
     *
1571
     * Optionally takes a listener to get include patterns specified
1572
     * for that sniff only.
1573
     *
1574
     * @param string $listener The listener to get patterns for. If NULL, all
1575
     *                         patterns are returned.
1576
     *
1577
     * @return array
1578
     */
1579
    public function getIncludePatterns($listener=null)
20✔
1580
    {
1581
        if ($listener === null) {
20✔
1582
            return $this->includePatterns;
3✔
1583
        }
1584

1585
        if (isset($this->includePatterns[$listener]) === true) {
17✔
1586
            return $this->includePatterns[$listener];
6✔
1587
        }
1588

1589
        return [];
11✔
1590

1591
    }//end getIncludePatterns()
1592

1593

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