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

PHPCSStandards / PHP_CodeSniffer / 13822786168

12 Mar 2025 10:30PM UTC coverage: 78.565% (+0.002%) from 78.563%
13822786168

push

github

web-flow
Merge pull request #865 from PHPCSStandards/feature/ruleset-test-property-setting-empty-array

Ruleset: bug fix - correctly handle empty array property setting

24707 of 31448 relevant lines covered (78.56%)

66.43 hits per line

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

88.22
/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\Standards;
18
use RecursiveDirectoryIterator;
19
use RecursiveIteratorIterator;
20
use ReflectionClass;
21
use stdClass;
22

23
class Ruleset
24
{
25

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

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

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

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

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

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

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

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

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

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

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

134

135
    /**
136
     * Initialise the ruleset that the run will use.
137
     *
138
     * @param \PHP_CodeSniffer\Config $config The config data for the run.
139
     *
140
     * @return void
141
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If no sniffs were registered.
142
     */
143
    public function __construct(Config $config)
44✔
144
    {
145
        $this->config = $config;
44✔
146
        $restrictions = $config->sniffs;
44✔
147
        $exclusions   = $config->exclude;
44✔
148
        $sniffs       = [];
44✔
149

150
        $standardPaths = [];
44✔
151
        foreach ($config->standards as $standard) {
44✔
152
            $installed = Standards::getInstalledStandardPath($standard);
44✔
153
            if ($installed === null) {
44✔
154
                $standard = Common::realpath($standard);
14✔
155
                if (is_dir($standard) === true
14✔
156
                    && is_file(Common::realpath($standard.DIRECTORY_SEPARATOR.'ruleset.xml')) === true
14✔
157
                ) {
5✔
158
                    $standard = Common::realpath($standard.DIRECTORY_SEPARATOR.'ruleset.xml');
6✔
159
                }
1✔
160
            } else {
5✔
161
                $standard = $installed;
30✔
162
            }
163

164
            $standardPaths[] = $standard;
44✔
165
        }
15✔
166

167
        foreach ($standardPaths as $standard) {
44✔
168
            $ruleset = @simplexml_load_string(file_get_contents($standard));
44✔
169
            if ($ruleset !== false) {
44✔
170
                $standardName = (string) $ruleset['name'];
44✔
171
                if ($this->name !== '') {
44✔
172
                    $this->name .= ', ';
3✔
173
                }
1✔
174

175
                $this->name .= $standardName;
44✔
176

177
                // Allow autoloading of custom files inside this standard.
178
                if (isset($ruleset['namespace']) === true) {
44✔
179
                    $namespace = (string) $ruleset['namespace'];
6✔
180
                } else {
2✔
181
                    $namespace = basename(dirname($standard));
38✔
182
                }
183

184
                Autoload::addSearchPath(dirname($standard), $namespace);
44✔
185
            }
15✔
186

187
            if (defined('PHP_CODESNIFFER_IN_TESTS') === true && empty($restrictions) === false) {
44✔
188
                // In unit tests, only register the sniffs that the test wants and not the entire standard.
189
                try {
190
                    foreach ($restrictions as $restriction) {
9✔
191
                        $sniffs = array_merge($sniffs, $this->expandRulesetReference($restriction, dirname($standard)));
9✔
192
                    }
2✔
193
                } catch (RuntimeException $e) {
5✔
194
                    // Sniff reference could not be expanded, which probably means this
195
                    // is an installed standard. Let the unit test system take care of
196
                    // setting the correct sniff for testing.
197
                    return;
3✔
198
                }
199

200
                break;
6✔
201
            }
202

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

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

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

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

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

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

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

241
        if ($numSniffs === 0) {
41✔
242
            throw new RuntimeException('ERROR: No sniffs were registered');
3✔
243
        }
244

245
    }//end __construct()
25✔
246

247

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

258
        $sniffCount = count($sniffs);
18✔
259

260
        // Add a dummy entry to the end so we loop one last time
261
        // and echo out the collected info about the last standard.
262
        $sniffs[] = '';
18✔
263

264
        $summaryLine = PHP_EOL."The $this->name standard contains 1 sniff".PHP_EOL;
18✔
265
        if ($sniffCount !== 1) {
18✔
266
            $summaryLine = str_replace('1 sniff', "$sniffCount sniffs", $summaryLine);
15✔
267
        }
5✔
268

269
        echo $summaryLine;
18✔
270

271
        $lastStandard     = null;
18✔
272
        $lastCount        = 0;
18✔
273
        $sniffsInStandard = [];
18✔
274

275
        foreach ($sniffs as $i => $sniff) {
18✔
276
            if ($i === $sniffCount) {
18✔
277
                $currentStandard = null;
18✔
278
            } else {
6✔
279
                $currentStandard = substr($sniff, 0, strpos($sniff, '.'));
18✔
280
                if ($lastStandard === null) {
18✔
281
                    $lastStandard = $currentStandard;
18✔
282
                }
6✔
283
            }
284

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

293
                $subTitle .= ')';
18✔
294

295
                echo PHP_EOL.$subTitle.PHP_EOL;
18✔
296
                echo str_repeat('-', strlen($subTitle)).PHP_EOL;
18✔
297
                echo '  '.implode(PHP_EOL.'  ', $sniffsInStandard).PHP_EOL;
18✔
298

299
                $lastStandard     = $currentStandard;
18✔
300
                $lastCount        = 0;
18✔
301
                $sniffsInStandard = [];
18✔
302

303
                if ($currentStandard === null) {
18✔
304
                    break;
18✔
305
                }
306
            }//end if
4✔
307

308
            if (isset($this->deprecatedSniffs[$sniff]) === true) {
18✔
309
                $sniff .= ' *';
3✔
310
            }
1✔
311

312
            $sniffsInStandard[] = $sniff;
18✔
313
            ++$lastCount;
18✔
314
        }//end foreach
6✔
315

316
        if (count($this->deprecatedSniffs) > 0) {
18✔
317
            echo PHP_EOL.'* Sniffs marked with an asterix are deprecated.'.PHP_EOL;
3✔
318
        }
1✔
319

320
    }//end explain()
12✔
321

322

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

332
    }//end hasSniffDeprecations()
333

334

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

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

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

365
        ksort($this->deprecatedSniffs, (SORT_NATURAL | SORT_FLAG_CASE));
39✔
366

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

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

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

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

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

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

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

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

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

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

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

428
            $messages[] = $message;
18✔
429
        }//end foreach
8✔
430

431
        if (count($messages) === 0) {
24✔
432
            return;
6✔
433
        }
434

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

441
        $maxActualWidth = max($maxActualWidth, min(strlen($summaryLine), $maxMessageWidth));
18✔
442

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

450
        $messages = implode(PHP_EOL, $messages);
18✔
451
        if ($this->config->colors === false) {
18✔
452
            $messages = Common::stripColors($messages);
18✔
453
        }
6✔
454

455
        echo str_repeat('-', min(($maxActualWidth + 4), $reportWidth)).PHP_EOL;
18✔
456
        echo $messages;
18✔
457

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

461
    }//end showSniffDeprecations()
12✔
462

463

464
    /**
465
     * Processes a single ruleset and returns a list of the sniffs it represents.
466
     *
467
     * Rules founds within the ruleset are processed immediately, but sniff classes
468
     * are not registered by this method.
469
     *
470
     * @param string $rulesetPath The path to a ruleset XML file.
471
     * @param int    $depth       How many nested processing steps we are in. This
472
     *                            is only used for debug output.
473
     *
474
     * @return string[]
475
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException - If the ruleset path is invalid.
476
     *                                                      - If a specified autoload file could not be found.
477
     */
478
    public function processRuleset($rulesetPath, $depth=0)
44✔
479
    {
480
        $rulesetPath = Common::realpath($rulesetPath);
44✔
481
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
44✔
482
            echo str_repeat("\t", $depth);
×
483
            echo 'Processing ruleset '.Common::stripBasepath($rulesetPath, $this->config->basepath).PHP_EOL;
×
484
        }
485

486
        libxml_use_internal_errors(true);
44✔
487
        $ruleset = simplexml_load_string(file_get_contents($rulesetPath));
44✔
488
        if ($ruleset === false) {
44✔
489
            $errorMsg = "ERROR: Ruleset $rulesetPath is not valid".PHP_EOL;
9✔
490
            $errors   = libxml_get_errors();
9✔
491
            foreach ($errors as $error) {
9✔
492
                $errorMsg .= '- On line '.$error->line.', column '.$error->column.': '.$error->message;
6✔
493
            }
3✔
494

495
            libxml_clear_errors();
9✔
496
            throw new RuntimeException($errorMsg);
9✔
497
        }
498

499
        libxml_use_internal_errors(false);
35✔
500

501
        $ownSniffs      = [];
35✔
502
        $includedSniffs = [];
35✔
503
        $excludedSniffs = [];
35✔
504

505
        $this->paths[]       = $rulesetPath;
35✔
506
        $rulesetDir          = dirname($rulesetPath);
35✔
507
        $this->rulesetDirs[] = $rulesetDir;
35✔
508

509
        $sniffDir = $rulesetDir.DIRECTORY_SEPARATOR.'Sniffs';
35✔
510
        if (is_dir($sniffDir) === true) {
35✔
511
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
512
                echo str_repeat("\t", $depth);
×
513
                echo "\tAdding sniff files from ".Common::stripBasepath($sniffDir, $this->config->basepath).' directory'.PHP_EOL;
×
514
            }
515

516
            $ownSniffs = $this->expandSniffDirectory($sniffDir, $depth);
9✔
517
        }
3✔
518

519
        // Include custom autoloaders.
520
        foreach ($ruleset->{'autoload'} as $autoload) {
35✔
521
            if ($this->shouldProcessElement($autoload) === false) {
9✔
522
                continue;
6✔
523
            }
524

525
            $autoloadPath = (string) $autoload;
9✔
526

527
            // Try relative autoload paths first.
528
            $relativePath = Common::realPath(dirname($rulesetPath).DIRECTORY_SEPARATOR.$autoloadPath);
9✔
529

530
            if ($relativePath !== false && is_file($relativePath) === true) {
9✔
531
                $autoloadPath = $relativePath;
6✔
532
            } else if (is_file($autoloadPath) === false) {
9✔
533
                throw new RuntimeException('ERROR: The specified autoload file "'.$autoload.'" does not exist');
3✔
534
            }
535

536
            include_once $autoloadPath;
6✔
537

538
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
539
                echo str_repeat("\t", $depth);
×
540
                echo "\t=> included autoloader $autoloadPath".PHP_EOL;
×
541
            }
542
        }//end foreach
11✔
543

544
        // Process custom sniff config settings.
545
        foreach ($ruleset->{'config'} as $config) {
32✔
546
            if ($this->shouldProcessElement($config) === false) {
12✔
547
                continue;
6✔
548
            }
549

550
            Config::setConfigData((string) $config['name'], (string) $config['value'], true);
12✔
551
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
552
                echo str_repeat("\t", $depth);
×
553
                echo "\t=> set config value ".(string) $config['name'].': '.(string) $config['value'].PHP_EOL;
×
554
            }
555
        }
11✔
556

557
        foreach ($ruleset->rule as $rule) {
32✔
558
            if (isset($rule['ref']) === false
32✔
559
                || $this->shouldProcessElement($rule) === false
32✔
560
            ) {
11✔
561
                continue;
9✔
562
            }
563

564
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
32✔
565
                echo str_repeat("\t", $depth);
×
566
                echo "\tProcessing rule \"".$rule['ref'].'"'.PHP_EOL;
×
567
            }
568

569
            $expandedSniffs = $this->expandRulesetReference((string) $rule['ref'], $rulesetDir, $depth);
32✔
570
            $newSniffs      = array_diff($expandedSniffs, $includedSniffs);
32✔
571
            $includedSniffs = array_merge($includedSniffs, $expandedSniffs);
32✔
572

573
            $parts = explode('.', $rule['ref']);
32✔
574
            if (count($parts) === 4
32✔
575
                && $parts[0] !== ''
32✔
576
                && $parts[1] !== ''
32✔
577
                && $parts[2] !== ''
32✔
578
            ) {
11✔
579
                $sniffCode = $parts[0].'.'.$parts[1].'.'.$parts[2];
9✔
580
                if (isset($this->ruleset[$sniffCode]['severity']) === true
9✔
581
                    && $this->ruleset[$sniffCode]['severity'] === 0
9✔
582
                ) {
3✔
583
                    // This sniff code has already been turned off, but now
584
                    // it is being explicitly included again, so turn it back on.
585
                    $this->ruleset[(string) $rule['ref']]['severity'] = 5;
6✔
586
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
587
                        echo str_repeat("\t", $depth);
×
588
                        echo "\t\t* disabling sniff exclusion for specific message code *".PHP_EOL;
×
589
                        echo str_repeat("\t", $depth);
×
590
                        echo "\t\t=> severity set to 5".PHP_EOL;
2✔
591
                    }
592
                } else if (empty($newSniffs) === false) {
9✔
593
                    $newSniff = $newSniffs[0];
6✔
594
                    if (in_array($newSniff, $ownSniffs, true) === false) {
6✔
595
                        // Including a sniff that hasn't been included higher up, but
596
                        // only including a single message from it. So turn off all messages in
597
                        // the sniff, except this one.
598
                        $this->ruleset[$sniffCode]['severity']            = 0;
6✔
599
                        $this->ruleset[(string) $rule['ref']]['severity'] = 5;
6✔
600
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
601
                            echo str_repeat("\t", $depth);
×
602
                            echo "\t\tExcluding sniff \"".$sniffCode.'" except for "'.$parts[3].'"'.PHP_EOL;
×
603
                        }
604
                    }
2✔
605
                }//end if
2✔
606
            }//end if
3✔
607

608
            if (isset($rule->exclude) === true) {
32✔
609
                foreach ($rule->exclude as $exclude) {
15✔
610
                    if (isset($exclude['name']) === false) {
15✔
611
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
612
                            echo str_repeat("\t", $depth);
×
613
                            echo "\t\t* ignoring empty exclude rule *".PHP_EOL;
×
614
                            echo "\t\t\t=> ".$exclude->asXML().PHP_EOL;
×
615
                        }
616

617
                        continue;
3✔
618
                    }
619

620
                    if ($this->shouldProcessElement($exclude) === false) {
12✔
621
                        continue;
6✔
622
                    }
623

624
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
625
                        echo str_repeat("\t", $depth);
×
626
                        echo "\t\tExcluding rule \"".$exclude['name'].'"'.PHP_EOL;
×
627
                    }
628

629
                    // Check if a single code is being excluded, which is a shortcut
630
                    // for setting the severity of the message to 0.
631
                    $parts = explode('.', $exclude['name']);
12✔
632
                    if (count($parts) === 4) {
12✔
633
                        $this->ruleset[(string) $exclude['name']]['severity'] = 0;
6✔
634
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
635
                            echo str_repeat("\t", $depth);
×
636
                            echo "\t\t=> severity set to 0".PHP_EOL;
2✔
637
                        }
638
                    } else {
2✔
639
                        $excludedSniffs = array_merge(
6✔
640
                            $excludedSniffs,
6✔
641
                            $this->expandRulesetReference((string) $exclude['name'], $rulesetDir, ($depth + 1))
6✔
642
                        );
4✔
643
                    }
644
                }//end foreach
5✔
645
            }//end if
5✔
646

647
            $this->processRule($rule, $newSniffs, $depth);
32✔
648
        }//end foreach
11✔
649

650
        // Process custom command line arguments.
651
        $cliArgs = [];
32✔
652
        foreach ($ruleset->{'arg'} as $arg) {
32✔
653
            if ($this->shouldProcessElement($arg) === false) {
9✔
654
                continue;
6✔
655
            }
656

657
            if (isset($arg['name']) === true) {
9✔
658
                $argString = '--'.(string) $arg['name'];
9✔
659
                if (isset($arg['value']) === true) {
9✔
660
                    $argString .= '='.(string) $arg['value'];
9✔
661
                }
3✔
662
            } else {
3✔
663
                $argString = '-'.(string) $arg['value'];
6✔
664
            }
665

666
            $cliArgs[] = $argString;
9✔
667

668
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
669
                echo str_repeat("\t", $depth);
×
670
                echo "\t=> set command line value $argString".PHP_EOL;
×
671
            }
672
        }//end foreach
11✔
673

674
        // Set custom php ini values as CLI args.
675
        foreach ($ruleset->{'ini'} as $arg) {
32✔
676
            if ($this->shouldProcessElement($arg) === false) {
9✔
677
                continue;
6✔
678
            }
679

680
            if (isset($arg['name']) === false) {
9✔
681
                continue;
3✔
682
            }
683

684
            $name      = (string) $arg['name'];
9✔
685
            $argString = $name;
9✔
686
            if (isset($arg['value']) === true) {
9✔
687
                $value      = (string) $arg['value'];
6✔
688
                $argString .= "=$value";
6✔
689
            } else {
2✔
690
                $value = 'true';
3✔
691
            }
692

693
            $cliArgs[] = '-d';
9✔
694
            $cliArgs[] = $argString;
9✔
695

696
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
697
                echo str_repeat("\t", $depth);
×
698
                echo "\t=> set PHP ini value $name to $value".PHP_EOL;
×
699
            }
700
        }//end foreach
11✔
701

702
        if (empty($this->config->files) === true) {
32✔
703
            // Process hard-coded file paths.
704
            foreach ($ruleset->{'file'} as $file) {
32✔
705
                $file      = (string) $file;
12✔
706
                $cliArgs[] = $file;
12✔
707
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
708
                    echo str_repeat("\t", $depth);
×
709
                    echo "\t=> added \"$file\" to the file list".PHP_EOL;
×
710
                }
711
            }
11✔
712
        }
11✔
713

714
        if (empty($cliArgs) === false) {
32✔
715
            // Change the directory so all relative paths are worked
716
            // out based on the location of the ruleset instead of
717
            // the location of the user.
718
            $inPhar = Common::isPharFile($rulesetDir);
18✔
719
            if ($inPhar === false) {
18✔
720
                $currentDir = getcwd();
18✔
721
                chdir($rulesetDir);
18✔
722
            }
6✔
723

724
            $this->config->setCommandLineValues($cliArgs);
18✔
725

726
            if ($inPhar === false) {
18✔
727
                chdir($currentDir);
18✔
728
            }
6✔
729
        }
6✔
730

731
        // Process custom ignore pattern rules.
732
        foreach ($ruleset->{'exclude-pattern'} as $pattern) {
32✔
733
            if ($this->shouldProcessElement($pattern) === false) {
6✔
734
                continue;
6✔
735
            }
736

737
            if (isset($pattern['type']) === false) {
6✔
738
                $pattern['type'] = 'absolute';
6✔
739
            }
2✔
740

741
            $this->ignorePatterns[(string) $pattern] = (string) $pattern['type'];
6✔
742
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
743
                echo str_repeat("\t", $depth);
×
744
                echo "\t=> added global ".(string) $pattern['type'].' ignore pattern: '.(string) $pattern.PHP_EOL;
×
745
            }
746
        }
11✔
747

748
        $includedSniffs = array_unique(array_merge($ownSniffs, $includedSniffs));
32✔
749
        $excludedSniffs = array_unique($excludedSniffs);
32✔
750

751
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
32✔
752
            $included = count($includedSniffs);
×
753
            $excluded = count($excludedSniffs);
×
754
            echo str_repeat("\t", $depth);
×
755
            echo "=> Ruleset processing complete; included $included sniffs and excluded $excluded".PHP_EOL;
×
756
        }
757

758
        // Merge our own sniff list with our externally included
759
        // sniff list, but filter out any excluded sniffs.
760
        $files = [];
32✔
761
        foreach ($includedSniffs as $sniff) {
32✔
762
            if (in_array($sniff, $excludedSniffs, true) === true) {
32✔
763
                continue;
6✔
764
            } else {
765
                $files[] = Common::realpath($sniff);
32✔
766
            }
767
        }
11✔
768

769
        return $files;
32✔
770

771
    }//end processRuleset()
772

773

774
    /**
775
     * Expands a directory into a list of sniff files within.
776
     *
777
     * @param string $directory The path to a directory.
778
     * @param int    $depth     How many nested processing steps we are in. This
779
     *                          is only used for debug output.
780
     *
781
     * @return array
782
     */
783
    private function expandSniffDirectory($directory, $depth=0)
6✔
784
    {
785
        $sniffs = [];
6✔
786

787
        $rdi = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
6✔
788
        $di  = new RecursiveIteratorIterator($rdi, 0, RecursiveIteratorIterator::CATCH_GET_CHILD);
6✔
789

790
        $dirLen = strlen($directory);
6✔
791

792
        foreach ($di as $file) {
6✔
793
            $filename = $file->getFilename();
6✔
794

795
            // Skip hidden files.
796
            if (substr($filename, 0, 1) === '.') {
6✔
797
                continue;
6✔
798
            }
799

800
            // We are only interested in PHP and sniff files.
801
            $fileParts = explode('.', $filename);
6✔
802
            if (array_pop($fileParts) !== 'php') {
6✔
803
                continue;
3✔
804
            }
805

806
            $basename = basename($filename, '.php');
6✔
807
            if (substr($basename, -5) !== 'Sniff') {
6✔
808
                continue;
3✔
809
            }
810

811
            $path = $file->getPathname();
6✔
812

813
            // Skip files in hidden directories within the Sniffs directory of this
814
            // standard. We use the offset with strpos() to allow hidden directories
815
            // before, valid example:
816
            // /home/foo/.composer/vendor/squiz/custom_tool/MyStandard/Sniffs/...
817
            if (strpos($path, DIRECTORY_SEPARATOR.'.', $dirLen) !== false) {
6✔
818
                continue;
3✔
819
            }
820

821
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
822
                echo str_repeat("\t", $depth);
×
823
                echo "\t\t=> ".Common::stripBasepath($path, $this->config->basepath).PHP_EOL;
×
824
            }
825

826
            $sniffs[] = $path;
6✔
827
        }//end foreach
2✔
828

829
        return $sniffs;
6✔
830

831
    }//end expandSniffDirectory()
832

833

834
    /**
835
     * Expands a ruleset reference into a list of sniff files.
836
     *
837
     * @param string $ref        The reference from the ruleset XML file.
838
     * @param string $rulesetDir The directory of the ruleset XML file, used to
839
     *                           evaluate relative paths.
840
     * @param int    $depth      How many nested processing steps we are in. This
841
     *                           is only used for debug output.
842
     *
843
     * @return array
844
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the reference is invalid.
845
     */
846
    private function expandRulesetReference($ref, $rulesetDir, $depth=0)
53✔
847
    {
848
        // Ignore internal sniffs codes as they are used to only
849
        // hide and change internal messages.
850
        if (substr($ref, 0, 9) === 'Internal.') {
53✔
851
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
852
                echo str_repeat("\t", $depth);
×
853
                echo "\t\t* ignoring internal sniff code *".PHP_EOL;
×
854
            }
855

856
            return [];
3✔
857
        }
858

859
        // As sniffs can't begin with a full stop, assume references in
860
        // this format are relative paths and attempt to convert them
861
        // to absolute paths. If this fails, let the reference run through
862
        // the normal checks and have it fail as normal.
863
        if (substr($ref, 0, 1) === '.') {
53✔
864
            $realpath = Common::realpath($rulesetDir.'/'.$ref);
12✔
865
            if ($realpath !== false) {
12✔
866
                $ref = $realpath;
6✔
867
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
868
                    echo str_repeat("\t", $depth);
×
869
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
×
870
                }
871
            }
2✔
872
        }
4✔
873

874
        // As sniffs can't begin with a tilde, assume references in
875
        // this format are relative to the user's home directory.
876
        if (substr($ref, 0, 2) === '~/') {
53✔
877
            $realpath = Common::realpath($ref);
9✔
878
            if ($realpath !== false) {
9✔
879
                $ref = $realpath;
3✔
880
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
881
                    echo str_repeat("\t", $depth);
×
882
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
×
883
                }
884
            }
1✔
885
        }
3✔
886

887
        if (is_file($ref) === true) {
53✔
888
            if (substr($ref, -9) === 'Sniff.php') {
11✔
889
                // A single external sniff.
890
                $this->rulesetDirs[] = dirname(dirname(dirname($ref)));
11✔
891
                return [$ref];
11✔
892
            }
893
        } else {
1✔
894
            // See if this is a whole standard being referenced.
895
            $path = Standards::getInstalledStandardPath($ref);
48✔
896
            if ($path !== null && Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) {
48✔
897
                // If the ruleset exists inside the phar file, use it.
898
                if (file_exists($path.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
×
899
                    $path .= DIRECTORY_SEPARATOR.'ruleset.xml';
×
900
                } else {
901
                    $path = null;
×
902
                }
903
            }
904

905
            if ($path !== null) {
48✔
906
                $ref = $path;
6✔
907
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
908
                    echo str_repeat("\t", $depth);
×
909
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
2✔
910
                }
911
            } else if (is_dir($ref) === false) {
46✔
912
                // Work out the sniff path.
913
                $sepPos = strpos($ref, DIRECTORY_SEPARATOR);
39✔
914
                if ($sepPos !== false) {
39✔
915
                    $stdName = substr($ref, 0, $sepPos);
9✔
916
                    $path    = substr($ref, $sepPos);
9✔
917
                } else {
3✔
918
                    $parts   = explode('.', $ref);
30✔
919
                    $stdName = $parts[0];
30✔
920
                    if (count($parts) === 1) {
30✔
921
                        // A whole standard?
922
                        $path = '';
3✔
923
                    } else if (count($parts) === 2) {
28✔
924
                        // A directory of sniffs?
925
                        $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1];
6✔
926
                    } else {
2✔
927
                        // A single sniff?
928
                        $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1].DIRECTORY_SEPARATOR.$parts[2].'Sniff.php';
24✔
929
                    }
930
                }
931

932
                $newRef  = false;
39✔
933
                $stdPath = Standards::getInstalledStandardPath($stdName);
39✔
934
                if ($stdPath !== null && $path !== '') {
39✔
935
                    if (Common::isPharFile($stdPath) === true
15✔
936
                        && strpos($stdPath, 'ruleset.xml') === false
15✔
937
                    ) {
5✔
938
                        // Phar files can only return the directory,
939
                        // since ruleset can be omitted if building one standard.
940
                        $newRef = Common::realpath($stdPath.$path);
×
941
                    } else {
942
                        $newRef = Common::realpath(dirname($stdPath).$path);
15✔
943
                    }
944
                }
5✔
945

946
                if ($newRef === false) {
39✔
947
                    // The sniff is not locally installed, so check if it is being
948
                    // referenced as a remote sniff outside the install. We do this
949
                    // by looking through all directories where we have found ruleset
950
                    // files before, looking for ones for this particular standard,
951
                    // and seeing if it is in there.
952
                    foreach ($this->rulesetDirs as $dir) {
33✔
953
                        if (strtolower(basename($dir)) !== strtolower($stdName)) {
33✔
954
                            continue;
33✔
955
                        }
956

957
                        $newRef = Common::realpath($dir.$path);
×
958

959
                        if ($newRef !== false) {
×
960
                            $ref = $newRef;
×
961
                        }
962
                    }
11✔
963
                } else {
11✔
964
                    $ref = $newRef;
6✔
965
                }
966

967
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
39✔
968
                    echo str_repeat("\t", $depth);
×
969
                    echo "\t\t=> ".Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
×
970
                }
971
            }//end if
13✔
972
        }//end if
973

974
        if (is_dir($ref) === true) {
48✔
975
            if (is_file($ref.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
9✔
976
                // We are referencing an external coding standard.
977
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
3✔
978
                    echo str_repeat("\t", $depth);
×
979
                    echo "\t\t* rule is referencing a standard using directory name; processing *".PHP_EOL;
×
980
                }
981

982
                return $this->processRuleset($ref.DIRECTORY_SEPARATOR.'ruleset.xml', ($depth + 2));
3✔
983
            } else {
984
                // We are referencing a whole directory of sniffs.
985
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
986
                    echo str_repeat("\t", $depth);
×
987
                    echo "\t\t* rule is referencing a directory of sniffs *".PHP_EOL;
×
988
                    echo str_repeat("\t", $depth);
×
989
                    echo "\t\tAdding sniff files from directory".PHP_EOL;
×
990
                }
991

992
                return $this->expandSniffDirectory($ref, ($depth + 1));
9✔
993
            }
994
        } else {
995
            if (is_file($ref) === false) {
42✔
996
                $error = "ERROR: Referenced sniff \"$ref\" does not exist";
33✔
997
                throw new RuntimeException($error);
33✔
998
            }
999

1000
            if (substr($ref, -9) === 'Sniff.php') {
9✔
1001
                // A single sniff.
1002
                return [$ref];
6✔
1003
            } else {
1004
                // Assume an external ruleset.xml file.
1005
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1006
                    echo str_repeat("\t", $depth);
×
1007
                    echo "\t\t* rule is referencing a standard using ruleset path; processing *".PHP_EOL;
×
1008
                }
1009

1010
                return $this->processRuleset($ref, ($depth + 2));
6✔
1011
            }
1012
        }//end if
1013

1014
    }//end expandRulesetReference()
1015

1016

1017
    /**
1018
     * Processes a rule from a ruleset XML file, overriding built-in defaults.
1019
     *
1020
     * @param \SimpleXMLElement $rule      The rule object from a ruleset XML file.
1021
     * @param string[]          $newSniffs An array of sniffs that got included by this rule.
1022
     * @param int               $depth     How many nested processing steps we are in.
1023
     *                                     This is only used for debug output.
1024
     *
1025
     * @return void
1026
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If rule settings are invalid.
1027
     */
1028
    private function processRule($rule, $newSniffs, $depth=0)
23✔
1029
    {
1030
        $ref  = (string) $rule['ref'];
23✔
1031
        $todo = [$ref];
23✔
1032

1033
        $parts      = explode('.', $ref);
23✔
1034
        $partsCount = count($parts);
23✔
1035
        if ($partsCount <= 2
15✔
1036
            || $partsCount > count(array_filter($parts))
20✔
1037
            || in_array($ref, $newSniffs) === true
21✔
1038
        ) {
8✔
1039
            // We are processing a standard, a category of sniffs or a relative path inclusion.
1040
            foreach ($newSniffs as $sniffFile) {
20✔
1041
                $parts = explode(DIRECTORY_SEPARATOR, $sniffFile);
14✔
1042
                if (count($parts) === 1 && DIRECTORY_SEPARATOR === '\\') {
14✔
1043
                    // Path using forward slashes while running on Windows.
1044
                    $parts = explode('/', $sniffFile);
×
1045
                }
1046

1047
                $sniffName     = array_pop($parts);
14✔
1048
                $sniffCategory = array_pop($parts);
14✔
1049
                array_pop($parts);
14✔
1050
                $sniffStandard = array_pop($parts);
14✔
1051
                $todo[]        = $sniffStandard.'.'.$sniffCategory.'.'.substr($sniffName, 0, -9);
14✔
1052
            }
7✔
1053
        }
7✔
1054

1055
        foreach ($todo as $code) {
23✔
1056
            // Custom severity.
1057
            if (isset($rule->severity) === true
23✔
1058
                && $this->shouldProcessElement($rule->severity) === true
23✔
1059
            ) {
8✔
1060
                if (isset($this->ruleset[$code]) === false) {
9✔
1061
                    $this->ruleset[$code] = [];
9✔
1062
                }
3✔
1063

1064
                $this->ruleset[$code]['severity'] = (int) $rule->severity;
9✔
1065
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
1066
                    echo str_repeat("\t", $depth);
×
1067
                    echo "\t\t=> severity set to ".(int) $rule->severity;
×
1068
                    if ($code !== $ref) {
×
1069
                        echo " for $code";
×
1070
                    }
1071

1072
                    echo PHP_EOL;
×
1073
                }
1074
            }
3✔
1075

1076
            // Custom message type.
1077
            if (isset($rule->type) === true
23✔
1078
                && $this->shouldProcessElement($rule->type) === true
23✔
1079
            ) {
8✔
1080
                if (isset($this->ruleset[$code]) === false) {
9✔
1081
                    $this->ruleset[$code] = [];
3✔
1082
                }
1✔
1083

1084
                $type = strtolower((string) $rule->type);
9✔
1085
                if ($type !== 'error' && $type !== 'warning') {
9✔
1086
                    throw new RuntimeException("ERROR: Message type \"$type\" is invalid; must be \"error\" or \"warning\"");
3✔
1087
                }
1088

1089
                $this->ruleset[$code]['type'] = $type;
6✔
1090
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1091
                    echo str_repeat("\t", $depth);
×
1092
                    echo "\t\t=> message type set to ".(string) $rule->type;
×
1093
                    if ($code !== $ref) {
×
1094
                        echo " for $code";
×
1095
                    }
1096

1097
                    echo PHP_EOL;
×
1098
                }
1099
            }//end if
2✔
1100

1101
            // Custom message.
1102
            if (isset($rule->message) === true
20✔
1103
                && $this->shouldProcessElement($rule->message) === true
20✔
1104
            ) {
7✔
1105
                if (isset($this->ruleset[$code]) === false) {
6✔
1106
                    $this->ruleset[$code] = [];
×
1107
                }
1108

1109
                $this->ruleset[$code]['message'] = (string) $rule->message;
6✔
1110
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1111
                    echo str_repeat("\t", $depth);
×
1112
                    echo "\t\t=> message set to ".(string) $rule->message;
×
1113
                    if ($code !== $ref) {
×
1114
                        echo " for $code";
×
1115
                    }
1116

1117
                    echo PHP_EOL;
×
1118
                }
1119
            }
2✔
1120

1121
            // Custom properties.
1122
            if (isset($rule->properties) === true
20✔
1123
                && $this->shouldProcessElement($rule->properties) === true
20✔
1124
            ) {
7✔
1125
                $propertyScope = 'standard';
17✔
1126
                if ($code === $ref || substr($ref, -9) === 'Sniff.php') {
17✔
1127
                    $propertyScope = 'sniff';
17✔
1128
                }
6✔
1129

1130
                foreach ($rule->properties->property as $prop) {
17✔
1131
                    if ($this->shouldProcessElement($prop) === false) {
17✔
1132
                        continue;
6✔
1133
                    }
1134

1135
                    if (isset($this->ruleset[$code]) === false) {
17✔
1136
                        $this->ruleset[$code] = [
17✔
1137
                            'properties' => [],
17✔
1138
                        ];
6✔
1139
                    } else if (isset($this->ruleset[$code]['properties']) === false) {
14✔
1140
                        $this->ruleset[$code]['properties'] = [];
3✔
1141
                    }
1✔
1142

1143
                    $name = (string) $prop['name'];
17✔
1144
                    if (isset($prop['type']) === true
17✔
1145
                        && (string) $prop['type'] === 'array'
17✔
1146
                    ) {
6✔
1147
                        $values = [];
12✔
1148
                        if (isset($prop['extend']) === true
12✔
1149
                            && (string) $prop['extend'] === 'true'
12✔
1150
                            && isset($this->ruleset[$code]['properties'][$name]['value']) === true
12✔
1151
                        ) {
4✔
1152
                            $values = $this->ruleset[$code]['properties'][$name]['value'];
9✔
1153
                        }
3✔
1154

1155
                        if (isset($prop->element) === true) {
12✔
1156
                            $printValue = '';
12✔
1157
                            foreach ($prop->element as $element) {
12✔
1158
                                if ($this->shouldProcessElement($element) === false) {
12✔
1159
                                    continue;
6✔
1160
                                }
1161

1162
                                $value = (string) $element['value'];
12✔
1163
                                if (isset($element['key']) === true) {
12✔
1164
                                    $key          = (string) $element['key'];
3✔
1165
                                    $values[$key] = $value;
3✔
1166
                                    $printValue  .= $key.'=>'.$value.',';
3✔
1167
                                } else {
1✔
1168
                                    $values[]    = $value;
12✔
1169
                                    $printValue .= $value.',';
12✔
1170
                                }
1171
                            }
4✔
1172

1173
                            $printValue = rtrim($printValue, ',');
12✔
1174
                        } else {
4✔
1175
                            $value      = (string) $prop['value'];
3✔
1176
                            $printValue = $value;
3✔
1177
                            if ($value !== '') {
3✔
1178
                                foreach (explode(',', $value) as $val) {
3✔
1179
                                    list($k, $v) = explode('=>', $val.'=>');
3✔
1180
                                    if ($v !== '') {
3✔
1181
                                        $values[trim($k)] = trim($v);
3✔
1182
                                    } else {
1✔
1183
                                        $values[] = trim($k);
3✔
1184
                                    }
1185
                                }
1✔
1186
                            }
1✔
1187
                        }//end if
1188

1189
                        $this->ruleset[$code]['properties'][$name] = [
12✔
1190
                            'value' => $values,
12✔
1191
                            'scope' => $propertyScope,
12✔
1192
                        ];
4✔
1193
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
12✔
1194
                            echo str_repeat("\t", $depth);
×
1195
                            echo "\t\t=> array property \"$name\" set to \"$printValue\"";
×
1196
                            if ($code !== $ref) {
×
1197
                                echo " for $code";
×
1198
                            }
1199

1200
                            echo PHP_EOL;
4✔
1201
                        }
1202
                    } else {
4✔
1203
                        $this->ruleset[$code]['properties'][$name] = [
17✔
1204
                            'value' => (string) $prop['value'],
17✔
1205
                            'scope' => $propertyScope,
17✔
1206
                        ];
6✔
1207
                        if (PHP_CODESNIFFER_VERBOSITY > 1) {
17✔
1208
                            echo str_repeat("\t", $depth);
×
1209
                            echo "\t\t=> property \"$name\" set to \"".(string) $prop['value'].'"';
×
1210
                            if ($code !== $ref) {
×
1211
                                echo " for $code";
×
1212
                            }
1213

1214
                            echo PHP_EOL;
×
1215
                        }
1216
                    }//end if
1217
                }//end foreach
6✔
1218
            }//end if
6✔
1219

1220
            // Ignore patterns.
1221
            foreach ($rule->{'exclude-pattern'} as $pattern) {
20✔
1222
                if ($this->shouldProcessElement($pattern) === false) {
6✔
1223
                    continue;
6✔
1224
                }
1225

1226
                if (isset($this->ignorePatterns[$code]) === false) {
6✔
1227
                    $this->ignorePatterns[$code] = [];
6✔
1228
                }
2✔
1229

1230
                if (isset($pattern['type']) === false) {
6✔
1231
                    $pattern['type'] = 'absolute';
6✔
1232
                }
2✔
1233

1234
                $this->ignorePatterns[$code][(string) $pattern] = (string) $pattern['type'];
6✔
1235
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1236
                    echo str_repeat("\t", $depth);
×
1237
                    echo "\t\t=> added rule-specific ".(string) $pattern['type'].' ignore pattern';
×
1238
                    if ($code !== $ref) {
×
1239
                        echo " for $code";
×
1240
                    }
1241

1242
                    echo ': '.(string) $pattern.PHP_EOL;
×
1243
                }
1244
            }//end foreach
7✔
1245

1246
            // Include patterns.
1247
            foreach ($rule->{'include-pattern'} as $pattern) {
20✔
1248
                if ($this->shouldProcessElement($pattern) === false) {
6✔
1249
                    continue;
6✔
1250
                }
1251

1252
                if (isset($this->includePatterns[$code]) === false) {
6✔
1253
                    $this->includePatterns[$code] = [];
6✔
1254
                }
2✔
1255

1256
                if (isset($pattern['type']) === false) {
6✔
1257
                    $pattern['type'] = 'absolute';
6✔
1258
                }
2✔
1259

1260
                $this->includePatterns[$code][(string) $pattern] = (string) $pattern['type'];
6✔
1261
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
1262
                    echo str_repeat("\t", $depth);
×
1263
                    echo "\t\t=> added rule-specific ".(string) $pattern['type'].' include pattern';
×
1264
                    if ($code !== $ref) {
×
1265
                        echo " for $code";
×
1266
                    }
1267

1268
                    echo ': '.(string) $pattern.PHP_EOL;
×
1269
                }
1270
            }//end foreach
7✔
1271
        }//end foreach
7✔
1272

1273
    }//end processRule()
13✔
1274

1275

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

1292
        if (PHP_CODESNIFFER_CBF === true
12✔
1293
            && isset($element['phpcbf-only']) === true
12✔
1294
            && (string) $element['phpcbf-only'] === 'true'
12✔
1295
        ) {
4✔
1296
            return true;
6✔
1297
        }
1298

1299
        if (PHP_CODESNIFFER_CBF === false
12✔
1300
            && isset($element['phpcs-only']) === true
12✔
1301
            && (string) $element['phpcs-only'] === 'true'
12✔
1302
        ) {
4✔
1303
            return true;
6✔
1304
        }
1305

1306
        return false;
12✔
1307

1308
    }//end shouldProcessElement()
1309

1310

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

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

1334
            $slashPos = strrpos(substr($file, 0, $sniffPos), DIRECTORY_SEPARATOR);
26✔
1335
            if ($slashPos === false) {
26✔
1336
                continue;
×
1337
            }
1338

1339
            $className   = Autoload::loadFile($file);
26✔
1340
            $compareName = Common::cleanSniffClass($className);
26✔
1341

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

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

1358
            // Skip abstract classes.
1359
            $reflection = new ReflectionClass($className);
26✔
1360
            if ($reflection->isAbstract() === true) {
26✔
1361
                continue;
3✔
1362
            }
1363

1364
            $listeners[$className] = $className;
26✔
1365

1366
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
26✔
1367
                echo "Registered $className".PHP_EOL;
×
1368
            }
1369
        }//end foreach
9✔
1370

1371
        $this->sniffs = $listeners;
26✔
1372

1373
    }//end registerSniffs()
17✔
1374

1375

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

1387
        foreach ($this->sniffs as $sniffClass => $sniffObject) {
11✔
1388
            $this->sniffs[$sniffClass] = null;
11✔
1389
            $this->sniffs[$sniffClass] = new $sniffClass();
11✔
1390

1391
            $sniffCode = Common::getSniffCode($sniffClass);
11✔
1392
            $this->sniffCodes[$sniffCode] = $sniffClass;
11✔
1393

1394
            if ($this->sniffs[$sniffClass] instanceof DeprecatedSniff) {
11✔
1395
                $this->deprecatedSniffs[$sniffCode] = $sniffClass;
3✔
1396
            }
1✔
1397

1398
            // Set custom properties.
1399
            if (isset($this->ruleset[$sniffCode]['properties']) === true) {
11✔
1400
                foreach ($this->ruleset[$sniffCode]['properties'] as $name => $settings) {
11✔
1401
                    $this->setSniffProperty($sniffClass, $name, $settings);
11✔
1402
                }
4✔
1403
            }
4✔
1404

1405
            $tokenizers = [];
11✔
1406
            $vars       = get_class_vars($sniffClass);
11✔
1407
            if (isset($vars['supportedTokenizers']) === true) {
11✔
1408
                foreach ($vars['supportedTokenizers'] as $tokenizer) {
9✔
1409
                    $tokenizers[$tokenizer] = $tokenizer;
9✔
1410
                }
3✔
1411
            } else {
3✔
1412
                $tokenizers = ['PHP' => 'PHP'];
8✔
1413
            }
1414

1415
            $tokens = $this->sniffs[$sniffClass]->register();
11✔
1416
            if (is_array($tokens) === false) {
11✔
1417
                $msg = "ERROR: Sniff $sniffClass register() method must return an array";
3✔
1418
                throw new RuntimeException($msg);
3✔
1419
            }
1420

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

1429
                $ignorePatterns[] = strtr($pattern, $replacements);
3✔
1430
            }
4✔
1431

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

1440
                $includePatterns[] = strtr($pattern, $replacements);
3✔
1441
            }
4✔
1442

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

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

1460
    }//end populateTokenListeners()
7✔
1461

1462

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

1483
        $name         = trim($name);
65✔
1484
        $propertyName = $name;
65✔
1485
        if (substr($propertyName, -2) === '[]') {
65✔
1486
            $propertyName = substr($propertyName, 0, -2);
3✔
1487
        }
1✔
1488

1489
        /*
1490
         * BC-compatibility layer for $settings using the pre-PHPCS 3.8.0 format.
1491
         *
1492
         * Prior to PHPCS 3.8.0, `$settings` was expected to only contain the new _value_
1493
         * for the property (which could be an array).
1494
         * Since PHPCS 3.8.0, `$settings` is expected to be an array with two keys: 'scope'
1495
         * and 'value', where 'scope' indicates whether the property should be set to the given 'value'
1496
         * for one individual sniff or for all sniffs in a standard.
1497
         *
1498
         * This BC-layer is only for integrations with PHPCS which may call this method directly
1499
         * and will be removed in PHPCS 4.0.0.
1500
         */
1501

1502
        if (is_array($settings) === false
65✔
1503
            || isset($settings['scope'], $settings['value']) === false
65✔
1504
        ) {
22✔
1505
            // This will be an "old" format value.
1506
            $settings = [
8✔
1507
                'value' => $settings,
24✔
1508
                'scope' => 'standard',
24✔
1509
            ];
16✔
1510

1511
            trigger_error(
24✔
1512
                __FUNCTION__.': the format of the $settings parameter has changed from (mixed) $value to array(\'scope\' => \'sniff|standard\', \'value\' => $value). Please update your integration code. See PR #3629 for more information.',
24✔
1513
                E_USER_DEPRECATED
16✔
1514
            );
16✔
1515
        }
7✔
1516

1517
        $isSettable  = false;
65✔
1518
        $sniffObject = $this->sniffs[$sniffClass];
65✔
1519
        if (property_exists($sniffObject, $propertyName) === true
65✔
1520
            || ($sniffObject instanceof stdClass) === true
38✔
1521
            || method_exists($sniffObject, '__set') === true
51✔
1522
        ) {
22✔
1523
            $isSettable = true;
53✔
1524
        }
18✔
1525

1526
        if ($isSettable === false) {
65✔
1527
            if ($settings['scope'] === 'sniff') {
18✔
1528
                $notice  = "ERROR: Ruleset invalid. Property \"$propertyName\" does not exist on sniff ";
6✔
1529
                $notice .= array_search($sniffClass, $this->sniffCodes, true);
6✔
1530
                throw new RuntimeException($notice);
6✔
1531
            }
1532

1533
            return;
12✔
1534
        }
1535

1536
        $value = $settings['value'];
53✔
1537

1538
        if (is_string($value) === true) {
53✔
1539
            $value = trim($value);
53✔
1540
        }
18✔
1541

1542
        if ($value === '') {
53✔
1543
            $value = null;
6✔
1544
        }
2✔
1545

1546
        // Special case for booleans.
1547
        if ($value === 'true') {
53✔
1548
            $value = true;
9✔
1549
        } else if ($value === 'false') {
53✔
1550
            $value = false;
9✔
1551
        } else if (substr($name, -2) === '[]') {
53✔
1552
            $name   = $propertyName;
3✔
1553
            $values = [];
3✔
1554
            if ($value !== null) {
3✔
1555
                foreach (explode(',', $value) as $val) {
3✔
1556
                    list($k, $v) = explode('=>', $val.'=>');
3✔
1557
                    if ($v !== '') {
3✔
1558
                        $values[trim($k)] = trim($v);
3✔
1559
                    } else {
1✔
1560
                        $values[] = trim($k);
3✔
1561
                    }
1562
                }
1✔
1563
            }
1✔
1564

1565
            $value = $values;
3✔
1566
        }
1✔
1567

1568
        $sniffObject->$name = $value;
53✔
1569

1570
    }//end setSniffProperty()
35✔
1571

1572

1573
    /**
1574
     * Gets the array of ignore patterns.
1575
     *
1576
     * Optionally takes a listener to get ignore patterns specified
1577
     * for that sniff only.
1578
     *
1579
     * @param string $listener The listener to get patterns for. If NULL, all
1580
     *                         patterns are returned.
1581
     *
1582
     * @return array
1583
     */
1584
    public function getIgnorePatterns($listener=null)
20✔
1585
    {
1586
        if ($listener === null) {
20✔
1587
            return $this->ignorePatterns;
3✔
1588
        }
1589

1590
        if (isset($this->ignorePatterns[$listener]) === true) {
17✔
1591
            return $this->ignorePatterns[$listener];
6✔
1592
        }
1593

1594
        return [];
11✔
1595

1596
    }//end getIgnorePatterns()
1597

1598

1599
    /**
1600
     * Gets the array of include patterns.
1601
     *
1602
     * Optionally takes a listener to get include patterns specified
1603
     * for that sniff only.
1604
     *
1605
     * @param string $listener The listener to get patterns for. If NULL, all
1606
     *                         patterns are returned.
1607
     *
1608
     * @return array
1609
     */
1610
    public function getIncludePatterns($listener=null)
20✔
1611
    {
1612
        if ($listener === null) {
20✔
1613
            return $this->includePatterns;
3✔
1614
        }
1615

1616
        if (isset($this->includePatterns[$listener]) === true) {
17✔
1617
            return $this->includePatterns[$listener];
6✔
1618
        }
1619

1620
        return [];
11✔
1621

1622
    }//end getIncludePatterns()
1623

1624

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