comparison vendor/squizlabs/php_codesniffer/src/Ruleset.php @ 4:a9cd425dd02b

Update, including to Drupal core 8.6.10
author Chris Cannam
date Thu, 28 Feb 2019 13:11:55 +0000
parents
children 12f9dff5fda9
comparison
equal deleted inserted replaced
3:307d7a7fd348 4:a9cd425dd02b
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/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
10 */
11
12 namespace PHP_CodeSniffer;
13
14 use PHP_CodeSniffer\Util;
15 use PHP_CodeSniffer\Exceptions\RuntimeException;
16
17 class Ruleset
18 {
19
20 /**
21 * The name of the coding standard being used.
22 *
23 * If a top-level standard includes other standards, or sniffs
24 * from other standards, only the name of the top-level standard
25 * will be stored in here.
26 *
27 * If multiple top-level standards are being loaded into
28 * a single ruleset object, this will store a comma separated list
29 * of the top-level standard names.
30 *
31 * @var string
32 */
33 public $name = '';
34
35 /**
36 * A list of file paths for the ruleset files being used.
37 *
38 * @var string[]
39 */
40 public $paths = [];
41
42 /**
43 * A list of regular expressions used to ignore specific sniffs for files and folders.
44 *
45 * Is also used to set global exclude patterns.
46 * The key is the regular expression and the value is the type
47 * of ignore pattern (absolute or relative).
48 *
49 * @var array<string, string>
50 */
51 public $ignorePatterns = [];
52
53 /**
54 * A list of regular expressions used to include specific sniffs for files and folders.
55 *
56 * The key is the sniff code and the value is an array with
57 * the key being a regular expression and the value is the type
58 * of ignore pattern (absolute or relative).
59 *
60 * @var array<string, array<string, string>>
61 */
62 public $includePatterns = [];
63
64 /**
65 * An array of sniff objects that are being used to check files.
66 *
67 * The key is the fully qualified name of the sniff class
68 * and the value is the sniff object.
69 *
70 * @var array<string, \PHP_CodeSniffer\Sniff>
71 */
72 public $sniffs = [];
73
74 /**
75 * A mapping of sniff codes to fully qualified class names.
76 *
77 * The key is the sniff code and the value
78 * is the fully qualified name of the sniff class.
79 *
80 * @var array<string, string>
81 */
82 public $sniffCodes = [];
83
84 /**
85 * An array of token types and the sniffs that are listening for them.
86 *
87 * The key is the token name being listened for and the value
88 * is the sniff object.
89 *
90 * @var array<int, \PHP_CodeSniffer\Sniff>
91 */
92 public $tokenListeners = [];
93
94 /**
95 * An array of rules from the ruleset.xml file.
96 *
97 * It may be empty, indicating that the ruleset does not override
98 * any of the default sniff settings.
99 *
100 * @var array<string, mixed>
101 */
102 public $ruleset = [];
103
104 /**
105 * The directories that the processed rulesets are in.
106 *
107 * @var string[]
108 */
109 protected $rulesetDirs = [];
110
111 /**
112 * The config data for the run.
113 *
114 * @var \PHP_CodeSniffer\Config
115 */
116 private $config = null;
117
118
119 /**
120 * Initialise the ruleset that the run will use.
121 *
122 * @param \PHP_CodeSniffer\Config $config The config data for the run.
123 *
124 * @return void
125 */
126 public function __construct(Config $config)
127 {
128 // Ignore sniff restrictions if caching is on.
129 $restrictions = [];
130 $exclusions = [];
131 if ($config->cache === false) {
132 $restrictions = $config->sniffs;
133 $exclusions = $config->exclude;
134 }
135
136 $this->config = $config;
137 $sniffs = [];
138
139 $standardPaths = [];
140 foreach ($config->standards as $standard) {
141 $installed = Util\Standards::getInstalledStandardPath($standard);
142 if ($installed === null) {
143 $standard = Util\Common::realpath($standard);
144 if (is_dir($standard) === true
145 && is_file(Util\Common::realpath($standard.DIRECTORY_SEPARATOR.'ruleset.xml')) === true
146 ) {
147 $standard = Util\Common::realpath($standard.DIRECTORY_SEPARATOR.'ruleset.xml');
148 }
149 } else {
150 $standard = $installed;
151 }
152
153 $standardPaths[] = $standard;
154 }
155
156 foreach ($standardPaths as $standard) {
157 $ruleset = @simplexml_load_string(file_get_contents($standard));
158 if ($ruleset !== false) {
159 $standardName = (string) $ruleset['name'];
160 if ($this->name !== '') {
161 $this->name .= ', ';
162 }
163
164 $this->name .= $standardName;
165 $this->paths[] = $standard;
166
167 // Allow autoloading of custom files inside this standard.
168 if (isset($ruleset['namespace']) === true) {
169 $namespace = (string) $ruleset['namespace'];
170 } else {
171 $namespace = basename(dirname($standard));
172 }
173
174 Autoload::addSearchPath(dirname($standard), $namespace);
175 }
176
177 if (defined('PHP_CODESNIFFER_IN_TESTS') === true && empty($restrictions) === false) {
178 // In unit tests, only register the sniffs that the test wants and not the entire standard.
179 try {
180 foreach ($restrictions as $restriction) {
181 $sniffs = array_merge($sniffs, $this->expandRulesetReference($restriction, dirname($standard)));
182 }
183 } catch (RuntimeException $e) {
184 // Sniff reference could not be expanded, which probably means this
185 // is an installed standard. Let the unit test system take care of
186 // setting the correct sniff for testing.
187 return;
188 }
189
190 break;
191 }
192
193 if (PHP_CODESNIFFER_VERBOSITY === 1) {
194 echo "Registering sniffs in the $standardName standard... ";
195 if (count($config->standards) > 1 || PHP_CODESNIFFER_VERBOSITY > 2) {
196 echo PHP_EOL;
197 }
198 }
199
200 $sniffs = array_merge($sniffs, $this->processRuleset($standard));
201 }//end foreach
202
203 $sniffRestrictions = [];
204 foreach ($restrictions as $sniffCode) {
205 $parts = explode('.', strtolower($sniffCode));
206 $sniffName = $parts[0].'\sniffs\\'.$parts[1].'\\'.$parts[2].'sniff';
207 $sniffRestrictions[$sniffName] = true;
208 }
209
210 $sniffExclusions = [];
211 foreach ($exclusions as $sniffCode) {
212 $parts = explode('.', strtolower($sniffCode));
213 $sniffName = $parts[0].'\sniffs\\'.$parts[1].'\\'.$parts[2].'sniff';
214 $sniffExclusions[$sniffName] = true;
215 }
216
217 $this->registerSniffs($sniffs, $sniffRestrictions, $sniffExclusions);
218 $this->populateTokenListeners();
219
220 $numSniffs = count($this->sniffs);
221 if (PHP_CODESNIFFER_VERBOSITY === 1) {
222 echo "DONE ($numSniffs sniffs registered)".PHP_EOL;
223 }
224
225 if ($numSniffs === 0) {
226 throw new RuntimeException('No sniffs were registered');
227 }
228
229 }//end __construct()
230
231
232 /**
233 * Prints a report showing the sniffs contained in a standard.
234 *
235 * @return void
236 */
237 public function explain()
238 {
239 $sniffs = array_keys($this->sniffCodes);
240 sort($sniffs);
241
242 ob_start();
243
244 $lastStandard = null;
245 $lastCount = '';
246 $sniffCount = count($sniffs);
247
248 // Add a dummy entry to the end so we loop
249 // one last time and clear the output buffer.
250 $sniffs[] = '';
251
252 echo PHP_EOL."The $this->name standard contains $sniffCount sniffs".PHP_EOL;
253
254 ob_start();
255
256 foreach ($sniffs as $i => $sniff) {
257 if ($i === $sniffCount) {
258 $currentStandard = null;
259 } else {
260 $currentStandard = substr($sniff, 0, strpos($sniff, '.'));
261 if ($lastStandard === null) {
262 $lastStandard = $currentStandard;
263 }
264 }
265
266 if ($currentStandard !== $lastStandard) {
267 $sniffList = ob_get_contents();
268 ob_end_clean();
269
270 echo PHP_EOL.$lastStandard.' ('.$lastCount.' sniff';
271 if ($lastCount > 1) {
272 echo 's';
273 }
274
275 echo ')'.PHP_EOL;
276 echo str_repeat('-', (strlen($lastStandard.$lastCount) + 10));
277 echo PHP_EOL;
278 echo $sniffList;
279
280 $lastStandard = $currentStandard;
281 $lastCount = 0;
282
283 if ($currentStandard === null) {
284 break;
285 }
286
287 ob_start();
288 }//end if
289
290 echo ' '.$sniff.PHP_EOL;
291 $lastCount++;
292 }//end foreach
293
294 }//end explain()
295
296
297 /**
298 * Processes a single ruleset and returns a list of the sniffs it represents.
299 *
300 * Rules founds within the ruleset are processed immediately, but sniff classes
301 * are not registered by this method.
302 *
303 * @param string $rulesetPath The path to a ruleset XML file.
304 * @param int $depth How many nested processing steps we are in. This
305 * is only used for debug output.
306 *
307 * @return string[]
308 * @throws RuntimeException If the ruleset path is invalid.
309 */
310 public function processRuleset($rulesetPath, $depth=0)
311 {
312 $rulesetPath = Util\Common::realpath($rulesetPath);
313 if (PHP_CODESNIFFER_VERBOSITY > 1) {
314 echo str_repeat("\t", $depth);
315 echo 'Processing ruleset '.Util\Common::stripBasepath($rulesetPath, $this->config->basepath).PHP_EOL;
316 }
317
318 $ruleset = @simplexml_load_string(file_get_contents($rulesetPath));
319 if ($ruleset === false) {
320 throw new RuntimeException("Ruleset $rulesetPath is not valid");
321 }
322
323 $ownSniffs = [];
324 $includedSniffs = [];
325 $excludedSniffs = [];
326
327 $rulesetDir = dirname($rulesetPath);
328 $this->rulesetDirs[] = $rulesetDir;
329
330 $sniffDir = $rulesetDir.DIRECTORY_SEPARATOR.'Sniffs';
331 if (is_dir($sniffDir) === true) {
332 if (PHP_CODESNIFFER_VERBOSITY > 1) {
333 echo str_repeat("\t", $depth);
334 echo "\tAdding sniff files from ".Util\Common::stripBasepath($sniffDir, $this->config->basepath).' directory'.PHP_EOL;
335 }
336
337 $ownSniffs = $this->expandSniffDirectory($sniffDir, $depth);
338 }
339
340 // Included custom autoloaders.
341 foreach ($ruleset->{'autoload'} as $autoload) {
342 if ($this->shouldProcessElement($autoload) === false) {
343 continue;
344 }
345
346 $autoloadPath = (string) $autoload;
347 if (is_file($autoloadPath) === false) {
348 $autoloadPath = Util\Common::realPath(dirname($rulesetPath).DIRECTORY_SEPARATOR.$autoloadPath);
349 }
350
351 if ($autoloadPath === false) {
352 throw new RuntimeException('The specified autoload file "'.$autoload.'" does not exist');
353 }
354
355 include_once $autoloadPath;
356
357 if (PHP_CODESNIFFER_VERBOSITY > 1) {
358 echo str_repeat("\t", $depth);
359 echo "\t=> included autoloader $autoloadPath".PHP_EOL;
360 }
361 }//end foreach
362
363 // Process custom sniff config settings.
364 foreach ($ruleset->{'config'} as $config) {
365 if ($this->shouldProcessElement($config) === false) {
366 continue;
367 }
368
369 Config::setConfigData((string) $config['name'], (string) $config['value'], true);
370 if (PHP_CODESNIFFER_VERBOSITY > 1) {
371 echo str_repeat("\t", $depth);
372 echo "\t=> set config value ".(string) $config['name'].': '.(string) $config['value'].PHP_EOL;
373 }
374 }
375
376 foreach ($ruleset->rule as $rule) {
377 if (isset($rule['ref']) === false
378 || $this->shouldProcessElement($rule) === false
379 ) {
380 continue;
381 }
382
383 if (PHP_CODESNIFFER_VERBOSITY > 1) {
384 echo str_repeat("\t", $depth);
385 echo "\tProcessing rule \"".$rule['ref'].'"'.PHP_EOL;
386 }
387
388 $expandedSniffs = $this->expandRulesetReference((string) $rule['ref'], $rulesetDir, $depth);
389 $newSniffs = array_diff($expandedSniffs, $includedSniffs);
390 $includedSniffs = array_merge($includedSniffs, $expandedSniffs);
391
392 $parts = explode('.', $rule['ref']);
393 if (count($parts) === 4
394 && $parts[0] !== ''
395 && $parts[1] !== ''
396 && $parts[2] !== ''
397 ) {
398 $sniffCode = $parts[0].'.'.$parts[1].'.'.$parts[2];
399 if (isset($this->ruleset[$sniffCode]['severity']) === true
400 && $this->ruleset[$sniffCode]['severity'] === 0
401 ) {
402 // This sniff code has already been turned off, but now
403 // it is being explicitly included again, so turn it back on.
404 $this->ruleset[(string) $rule['ref']]['severity'] = 5;
405 if (PHP_CODESNIFFER_VERBOSITY > 1) {
406 echo str_repeat("\t", $depth);
407 echo "\t\t* disabling sniff exclusion for specific message code *".PHP_EOL;
408 echo str_repeat("\t", $depth);
409 echo "\t\t=> severity set to 5".PHP_EOL;
410 }
411 } else if (empty($newSniffs) === false) {
412 $newSniff = $newSniffs[0];
413 if (in_array($newSniff, $ownSniffs) === false) {
414 // Including a sniff that hasn't been included higher up, but
415 // only including a single message from it. So turn off all messages in
416 // the sniff, except this one.
417 $this->ruleset[$sniffCode]['severity'] = 0;
418 $this->ruleset[(string) $rule['ref']]['severity'] = 5;
419 if (PHP_CODESNIFFER_VERBOSITY > 1) {
420 echo str_repeat("\t", $depth);
421 echo "\t\tExcluding sniff \"".$sniffCode.'" except for "'.$parts[3].'"'.PHP_EOL;
422 }
423 }
424 }//end if
425 }//end if
426
427 if (isset($rule->exclude) === true) {
428 foreach ($rule->exclude as $exclude) {
429 if (isset($exclude['name']) === false) {
430 if (PHP_CODESNIFFER_VERBOSITY > 1) {
431 echo str_repeat("\t", $depth);
432 echo "\t\t* ignoring empty exclude rule *".PHP_EOL;
433 echo "\t\t\t=> ".$exclude->asXML().PHP_EOL;
434 }
435
436 continue;
437 }
438
439 if ($this->shouldProcessElement($exclude) === false) {
440 continue;
441 }
442
443 if (PHP_CODESNIFFER_VERBOSITY > 1) {
444 echo str_repeat("\t", $depth);
445 echo "\t\tExcluding rule \"".$exclude['name'].'"'.PHP_EOL;
446 }
447
448 // Check if a single code is being excluded, which is a shortcut
449 // for setting the severity of the message to 0.
450 $parts = explode('.', $exclude['name']);
451 if (count($parts) === 4) {
452 $this->ruleset[(string) $exclude['name']]['severity'] = 0;
453 if (PHP_CODESNIFFER_VERBOSITY > 1) {
454 echo str_repeat("\t", $depth);
455 echo "\t\t=> severity set to 0".PHP_EOL;
456 }
457 } else {
458 $excludedSniffs = array_merge(
459 $excludedSniffs,
460 $this->expandRulesetReference($exclude['name'], $rulesetDir, ($depth + 1))
461 );
462 }
463 }//end foreach
464 }//end if
465
466 $this->processRule($rule, $newSniffs, $depth);
467 }//end foreach
468
469 // Process custom command line arguments.
470 $cliArgs = [];
471 foreach ($ruleset->{'arg'} as $arg) {
472 if ($this->shouldProcessElement($arg) === false) {
473 continue;
474 }
475
476 if (isset($arg['name']) === true) {
477 $argString = '--'.(string) $arg['name'];
478 if (isset($arg['value']) === true) {
479 $argString .= '='.(string) $arg['value'];
480 }
481 } else {
482 $argString = '-'.(string) $arg['value'];
483 }
484
485 $cliArgs[] = $argString;
486
487 if (PHP_CODESNIFFER_VERBOSITY > 1) {
488 echo str_repeat("\t", $depth);
489 echo "\t=> set command line value $argString".PHP_EOL;
490 }
491 }//end foreach
492
493 // Set custom php ini values as CLI args.
494 foreach ($ruleset->{'ini'} as $arg) {
495 if ($this->shouldProcessElement($arg) === false) {
496 continue;
497 }
498
499 if (isset($arg['name']) === false) {
500 continue;
501 }
502
503 $name = (string) $arg['name'];
504 $argString = $name;
505 if (isset($arg['value']) === true) {
506 $value = (string) $arg['value'];
507 $argString .= "=$value";
508 } else {
509 $value = 'true';
510 }
511
512 $cliArgs[] = '-d';
513 $cliArgs[] = $argString;
514
515 if (PHP_CODESNIFFER_VERBOSITY > 1) {
516 echo str_repeat("\t", $depth);
517 echo "\t=> set PHP ini value $name to $value".PHP_EOL;
518 }
519 }//end foreach
520
521 if (empty($this->config->files) === true) {
522 // Process hard-coded file paths.
523 foreach ($ruleset->{'file'} as $file) {
524 $file = (string) $file;
525 $cliArgs[] = $file;
526 if (PHP_CODESNIFFER_VERBOSITY > 1) {
527 echo str_repeat("\t", $depth);
528 echo "\t=> added \"$file\" to the file list".PHP_EOL;
529 }
530 }
531 }
532
533 if (empty($cliArgs) === false) {
534 // Change the directory so all relative paths are worked
535 // out based on the location of the ruleset instead of
536 // the location of the user.
537 $inPhar = Util\Common::isPharFile($rulesetDir);
538 if ($inPhar === false) {
539 $currentDir = getcwd();
540 chdir($rulesetDir);
541 }
542
543 $this->config->setCommandLineValues($cliArgs);
544
545 if ($inPhar === false) {
546 chdir($currentDir);
547 }
548 }
549
550 // Process custom ignore pattern rules.
551 foreach ($ruleset->{'exclude-pattern'} as $pattern) {
552 if ($this->shouldProcessElement($pattern) === false) {
553 continue;
554 }
555
556 if (isset($pattern['type']) === false) {
557 $pattern['type'] = 'absolute';
558 }
559
560 $this->ignorePatterns[(string) $pattern] = (string) $pattern['type'];
561 if (PHP_CODESNIFFER_VERBOSITY > 1) {
562 echo str_repeat("\t", $depth);
563 echo "\t=> added global ".(string) $pattern['type'].' ignore pattern: '.(string) $pattern.PHP_EOL;
564 }
565 }
566
567 $includedSniffs = array_unique(array_merge($ownSniffs, $includedSniffs));
568 $excludedSniffs = array_unique($excludedSniffs);
569
570 if (PHP_CODESNIFFER_VERBOSITY > 1) {
571 $included = count($includedSniffs);
572 $excluded = count($excludedSniffs);
573 echo str_repeat("\t", $depth);
574 echo "=> Ruleset processing complete; included $included sniffs and excluded $excluded".PHP_EOL;
575 }
576
577 // Merge our own sniff list with our externally included
578 // sniff list, but filter out any excluded sniffs.
579 $files = [];
580 foreach ($includedSniffs as $sniff) {
581 if (in_array($sniff, $excludedSniffs) === true) {
582 continue;
583 } else {
584 $files[] = Util\Common::realpath($sniff);
585 }
586 }
587
588 return $files;
589
590 }//end processRuleset()
591
592
593 /**
594 * Expands a directory into a list of sniff files within.
595 *
596 * @param string $directory The path to a directory.
597 * @param int $depth How many nested processing steps we are in. This
598 * is only used for debug output.
599 *
600 * @return array
601 */
602 private function expandSniffDirectory($directory, $depth=0)
603 {
604 $sniffs = [];
605
606 $rdi = new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
607 $di = new \RecursiveIteratorIterator($rdi, 0, \RecursiveIteratorIterator::CATCH_GET_CHILD);
608
609 $dirLen = strlen($directory);
610
611 foreach ($di as $file) {
612 $filename = $file->getFilename();
613
614 // Skip hidden files.
615 if (substr($filename, 0, 1) === '.') {
616 continue;
617 }
618
619 // We are only interested in PHP and sniff files.
620 $fileParts = explode('.', $filename);
621 if (array_pop($fileParts) !== 'php') {
622 continue;
623 }
624
625 $basename = basename($filename, '.php');
626 if (substr($basename, -5) !== 'Sniff') {
627 continue;
628 }
629
630 $path = $file->getPathname();
631
632 // Skip files in hidden directories within the Sniffs directory of this
633 // standard. We use the offset with strpos() to allow hidden directories
634 // before, valid example:
635 // /home/foo/.composer/vendor/squiz/custom_tool/MyStandard/Sniffs/...
636 if (strpos($path, DIRECTORY_SEPARATOR.'.', $dirLen) !== false) {
637 continue;
638 }
639
640 if (PHP_CODESNIFFER_VERBOSITY > 1) {
641 echo str_repeat("\t", $depth);
642 echo "\t\t=> ".Util\Common::stripBasepath($path, $this->config->basepath).PHP_EOL;
643 }
644
645 $sniffs[] = $path;
646 }//end foreach
647
648 return $sniffs;
649
650 }//end expandSniffDirectory()
651
652
653 /**
654 * Expands a ruleset reference into a list of sniff files.
655 *
656 * @param string $ref The reference from the ruleset XML file.
657 * @param string $rulesetDir The directory of the ruleset XML file, used to
658 * evaluate relative paths.
659 * @param int $depth How many nested processing steps we are in. This
660 * is only used for debug output.
661 *
662 * @return array
663 * @throws RuntimeException If the reference is invalid.
664 */
665 private function expandRulesetReference($ref, $rulesetDir, $depth=0)
666 {
667 // Ignore internal sniffs codes as they are used to only
668 // hide and change internal messages.
669 if (substr($ref, 0, 9) === 'Internal.') {
670 if (PHP_CODESNIFFER_VERBOSITY > 1) {
671 echo str_repeat("\t", $depth);
672 echo "\t\t* ignoring internal sniff code *".PHP_EOL;
673 }
674
675 return [];
676 }
677
678 // As sniffs can't begin with a full stop, assume references in
679 // this format are relative paths and attempt to convert them
680 // to absolute paths. If this fails, let the reference run through
681 // the normal checks and have it fail as normal.
682 if (substr($ref, 0, 1) === '.') {
683 $realpath = Util\Common::realpath($rulesetDir.'/'.$ref);
684 if ($realpath !== false) {
685 $ref = $realpath;
686 if (PHP_CODESNIFFER_VERBOSITY > 1) {
687 echo str_repeat("\t", $depth);
688 echo "\t\t=> ".Util\Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
689 }
690 }
691 }
692
693 // As sniffs can't begin with a tilde, assume references in
694 // this format are relative to the user's home directory.
695 if (substr($ref, 0, 2) === '~/') {
696 $realpath = Util\Common::realpath($ref);
697 if ($realpath !== false) {
698 $ref = $realpath;
699 if (PHP_CODESNIFFER_VERBOSITY > 1) {
700 echo str_repeat("\t", $depth);
701 echo "\t\t=> ".Util\Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
702 }
703 }
704 }
705
706 if (is_file($ref) === true) {
707 if (substr($ref, -9) === 'Sniff.php') {
708 // A single external sniff.
709 $this->rulesetDirs[] = dirname(dirname(dirname($ref)));
710 return [$ref];
711 }
712 } else {
713 // See if this is a whole standard being referenced.
714 $path = Util\Standards::getInstalledStandardPath($ref);
715 if (Util\Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) {
716 // If the ruleset exists inside the phar file, use it.
717 if (file_exists($path.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
718 $path = $path.DIRECTORY_SEPARATOR.'ruleset.xml';
719 } else {
720 $path = null;
721 }
722 }
723
724 if ($path !== null) {
725 $ref = $path;
726 if (PHP_CODESNIFFER_VERBOSITY > 1) {
727 echo str_repeat("\t", $depth);
728 echo "\t\t=> ".Util\Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
729 }
730 } else if (is_dir($ref) === false) {
731 // Work out the sniff path.
732 $sepPos = strpos($ref, DIRECTORY_SEPARATOR);
733 if ($sepPos !== false) {
734 $stdName = substr($ref, 0, $sepPos);
735 $path = substr($ref, $sepPos);
736 } else {
737 $parts = explode('.', $ref);
738 $stdName = $parts[0];
739 if (count($parts) === 1) {
740 // A whole standard?
741 $path = '';
742 } else if (count($parts) === 2) {
743 // A directory of sniffs?
744 $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1];
745 } else {
746 // A single sniff?
747 $path = DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR.$parts[1].DIRECTORY_SEPARATOR.$parts[2].'Sniff.php';
748 }
749 }
750
751 $newRef = false;
752 $stdPath = Util\Standards::getInstalledStandardPath($stdName);
753 if ($stdPath !== null && $path !== '') {
754 if (Util\Common::isPharFile($stdPath) === true
755 && strpos($stdPath, 'ruleset.xml') === false
756 ) {
757 // Phar files can only return the directory,
758 // since ruleset can be omitted if building one standard.
759 $newRef = Util\Common::realpath($stdPath.$path);
760 } else {
761 $newRef = Util\Common::realpath(dirname($stdPath).$path);
762 }
763 }
764
765 if ($newRef === false) {
766 // The sniff is not locally installed, so check if it is being
767 // referenced as a remote sniff outside the install. We do this
768 // by looking through all directories where we have found ruleset
769 // files before, looking for ones for this particular standard,
770 // and seeing if it is in there.
771 foreach ($this->rulesetDirs as $dir) {
772 if (strtolower(basename($dir)) !== strtolower($stdName)) {
773 continue;
774 }
775
776 $newRef = Util\Common::realpath($dir.$path);
777
778 if ($newRef !== false) {
779 $ref = $newRef;
780 }
781 }
782 } else {
783 $ref = $newRef;
784 }
785
786 if (PHP_CODESNIFFER_VERBOSITY > 1) {
787 echo str_repeat("\t", $depth);
788 echo "\t\t=> ".Util\Common::stripBasepath($ref, $this->config->basepath).PHP_EOL;
789 }
790 }//end if
791 }//end if
792
793 if (is_dir($ref) === true) {
794 if (is_file($ref.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
795 // We are referencing an external coding standard.
796 if (PHP_CODESNIFFER_VERBOSITY > 1) {
797 echo str_repeat("\t", $depth);
798 echo "\t\t* rule is referencing a standard using directory name; processing *".PHP_EOL;
799 }
800
801 return $this->processRuleset($ref.DIRECTORY_SEPARATOR.'ruleset.xml', ($depth + 2));
802 } else {
803 // We are referencing a whole directory of sniffs.
804 if (PHP_CODESNIFFER_VERBOSITY > 1) {
805 echo str_repeat("\t", $depth);
806 echo "\t\t* rule is referencing a directory of sniffs *".PHP_EOL;
807 echo str_repeat("\t", $depth);
808 echo "\t\tAdding sniff files from directory".PHP_EOL;
809 }
810
811 return $this->expandSniffDirectory($ref, ($depth + 1));
812 }
813 } else {
814 if (is_file($ref) === false) {
815 $error = "Referenced sniff \"$ref\" does not exist";
816 throw new RuntimeException($error);
817 }
818
819 if (substr($ref, -9) === 'Sniff.php') {
820 // A single sniff.
821 return [$ref];
822 } else {
823 // Assume an external ruleset.xml file.
824 if (PHP_CODESNIFFER_VERBOSITY > 1) {
825 echo str_repeat("\t", $depth);
826 echo "\t\t* rule is referencing a standard using ruleset path; processing *".PHP_EOL;
827 }
828
829 return $this->processRuleset($ref, ($depth + 2));
830 }
831 }//end if
832
833 }//end expandRulesetReference()
834
835
836 /**
837 * Processes a rule from a ruleset XML file, overriding built-in defaults.
838 *
839 * @param SimpleXMLElement $rule The rule object from a ruleset XML file.
840 * @param string[] $newSniffs An array of sniffs that got included by this rule.
841 * @param int $depth How many nested processing steps we are in.
842 * This is only used for debug output.
843 *
844 * @return void
845 * @throws RuntimeException If rule settings are invalid.
846 */
847 private function processRule($rule, $newSniffs, $depth=0)
848 {
849 $ref = (string) $rule['ref'];
850 $todo = [$ref];
851
852 $parts = explode('.', $ref);
853 if (count($parts) <= 2) {
854 // We are processing a standard or a category of sniffs.
855 foreach ($newSniffs as $sniffFile) {
856 $parts = explode(DIRECTORY_SEPARATOR, $sniffFile);
857 $sniffName = array_pop($parts);
858 $sniffCategory = array_pop($parts);
859 array_pop($parts);
860 $sniffStandard = array_pop($parts);
861 $todo[] = $sniffStandard.'.'.$sniffCategory.'.'.substr($sniffName, 0, -9);
862 }
863 }
864
865 foreach ($todo as $code) {
866 // Custom severity.
867 if (isset($rule->severity) === true
868 && $this->shouldProcessElement($rule->severity) === true
869 ) {
870 if (isset($this->ruleset[$code]) === false) {
871 $this->ruleset[$code] = [];
872 }
873
874 $this->ruleset[$code]['severity'] = (int) $rule->severity;
875 if (PHP_CODESNIFFER_VERBOSITY > 1) {
876 echo str_repeat("\t", $depth);
877 echo "\t\t=> severity set to ".(int) $rule->severity;
878 if ($code !== $ref) {
879 echo " for $code";
880 }
881
882 echo PHP_EOL;
883 }
884 }
885
886 // Custom message type.
887 if (isset($rule->type) === true
888 && $this->shouldProcessElement($rule->type) === true
889 ) {
890 if (isset($this->ruleset[$code]) === false) {
891 $this->ruleset[$code] = [];
892 }
893
894 $type = strtolower((string) $rule->type);
895 if ($type !== 'error' && $type !== 'warning') {
896 throw new RuntimeException("Message type \"$type\" is invalid; must be \"error\" or \"warning\"");
897 }
898
899 $this->ruleset[$code]['type'] = $type;
900 if (PHP_CODESNIFFER_VERBOSITY > 1) {
901 echo str_repeat("\t", $depth);
902 echo "\t\t=> message type set to ".(string) $rule->type;
903 if ($code !== $ref) {
904 echo " for $code";
905 }
906
907 echo PHP_EOL;
908 }
909 }//end if
910
911 // Custom message.
912 if (isset($rule->message) === true
913 && $this->shouldProcessElement($rule->message) === true
914 ) {
915 if (isset($this->ruleset[$code]) === false) {
916 $this->ruleset[$code] = [];
917 }
918
919 $this->ruleset[$code]['message'] = (string) $rule->message;
920 if (PHP_CODESNIFFER_VERBOSITY > 1) {
921 echo str_repeat("\t", $depth);
922 echo "\t\t=> message set to ".(string) $rule->message;
923 if ($code !== $ref) {
924 echo " for $code";
925 }
926
927 echo PHP_EOL;
928 }
929 }
930
931 // Custom properties.
932 if (isset($rule->properties) === true
933 && $this->shouldProcessElement($rule->properties) === true
934 ) {
935 foreach ($rule->properties->property as $prop) {
936 if ($this->shouldProcessElement($prop) === false) {
937 continue;
938 }
939
940 if (isset($this->ruleset[$code]) === false) {
941 $this->ruleset[$code] = [
942 'properties' => [],
943 ];
944 } else if (isset($this->ruleset[$code]['properties']) === false) {
945 $this->ruleset[$code]['properties'] = [];
946 }
947
948 $name = (string) $prop['name'];
949 if (isset($prop['type']) === true
950 && (string) $prop['type'] === 'array'
951 ) {
952 $values = [];
953 if (isset($prop['extend']) === true
954 && (string) $prop['extend'] === 'true'
955 && isset($this->ruleset[$code]['properties'][$name]) === true
956 ) {
957 $values = $this->ruleset[$code]['properties'][$name];
958 }
959
960 if (isset($prop->element) === true) {
961 $printValue = '';
962 foreach ($prop->element as $element) {
963 if ($this->shouldProcessElement($element) === false) {
964 continue;
965 }
966
967 $value = (string) $element['value'];
968 if (isset($element['key']) === true) {
969 $key = (string) $element['key'];
970 $values[$key] = $value;
971 $printValue .= $key.'=>'.$value.',';
972 } else {
973 $values[] = $value;
974 $printValue .= $value.',';
975 }
976 }
977
978 $printValue = rtrim($printValue, ',');
979 } else {
980 $value = (string) $prop['value'];
981 $printValue = $value;
982 foreach (explode(',', $value) as $val) {
983 list($k, $v) = explode('=>', $val.'=>');
984 if ($v !== '') {
985 $values[trim($k)] = trim($v);
986 } else {
987 $values[] = trim($k);
988 }
989 }
990 }//end if
991
992 $this->ruleset[$code]['properties'][$name] = $values;
993 if (PHP_CODESNIFFER_VERBOSITY > 1) {
994 echo str_repeat("\t", $depth);
995 echo "\t\t=> array property \"$name\" set to \"$printValue\"";
996 if ($code !== $ref) {
997 echo " for $code";
998 }
999
1000 echo PHP_EOL;
1001 }
1002 } else {
1003 $this->ruleset[$code]['properties'][$name] = (string) $prop['value'];
1004 if (PHP_CODESNIFFER_VERBOSITY > 1) {
1005 echo str_repeat("\t", $depth);
1006 echo "\t\t=> property \"$name\" set to \"".(string) $prop['value'].'"';
1007 if ($code !== $ref) {
1008 echo " for $code";
1009 }
1010
1011 echo PHP_EOL;
1012 }
1013 }//end if
1014 }//end foreach
1015 }//end if
1016
1017 // Ignore patterns.
1018 foreach ($rule->{'exclude-pattern'} as $pattern) {
1019 if ($this->shouldProcessElement($pattern) === false) {
1020 continue;
1021 }
1022
1023 if (isset($this->ignorePatterns[$code]) === false) {
1024 $this->ignorePatterns[$code] = [];
1025 }
1026
1027 if (isset($pattern['type']) === false) {
1028 $pattern['type'] = 'absolute';
1029 }
1030
1031 $this->ignorePatterns[$code][(string) $pattern] = (string) $pattern['type'];
1032 if (PHP_CODESNIFFER_VERBOSITY > 1) {
1033 echo str_repeat("\t", $depth);
1034 echo "\t\t=> added rule-specific ".(string) $pattern['type'].' ignore pattern';
1035 if ($code !== $ref) {
1036 echo " for $code";
1037 }
1038
1039 echo ': '.(string) $pattern.PHP_EOL;
1040 }
1041 }//end foreach
1042
1043 // Include patterns.
1044 foreach ($rule->{'include-pattern'} as $pattern) {
1045 if ($this->shouldProcessElement($pattern) === false) {
1046 continue;
1047 }
1048
1049 if (isset($this->includePatterns[$code]) === false) {
1050 $this->includePatterns[$code] = [];
1051 }
1052
1053 if (isset($pattern['type']) === false) {
1054 $pattern['type'] = 'absolute';
1055 }
1056
1057 $this->includePatterns[$code][(string) $pattern] = (string) $pattern['type'];
1058 if (PHP_CODESNIFFER_VERBOSITY > 1) {
1059 echo str_repeat("\t", $depth);
1060 echo "\t\t=> added rule-specific ".(string) $pattern['type'].' include pattern';
1061 if ($code !== $ref) {
1062 echo " for $code";
1063 }
1064
1065 echo ': '.(string) $pattern.PHP_EOL;
1066 }
1067 }//end foreach
1068 }//end foreach
1069
1070 }//end processRule()
1071
1072
1073 /**
1074 * Determine if an element should be processed or ignored.
1075 *
1076 * @param SimpleXMLElement $element An object from a ruleset XML file.
1077 *
1078 * @return bool
1079 */
1080 private function shouldProcessElement($element)
1081 {
1082 if (isset($element['phpcbf-only']) === false
1083 && isset($element['phpcs-only']) === false
1084 ) {
1085 // No exceptions are being made.
1086 return true;
1087 }
1088
1089 if (PHP_CODESNIFFER_CBF === true
1090 && isset($element['phpcbf-only']) === true
1091 && (string) $element['phpcbf-only'] === 'true'
1092 ) {
1093 return true;
1094 }
1095
1096 if (PHP_CODESNIFFER_CBF === false
1097 && isset($element['phpcs-only']) === true
1098 && (string) $element['phpcs-only'] === 'true'
1099 ) {
1100 return true;
1101 }
1102
1103 return false;
1104
1105 }//end shouldProcessElement()
1106
1107
1108 /**
1109 * Loads and stores sniffs objects used for sniffing files.
1110 *
1111 * @param array $files Paths to the sniff files to register.
1112 * @param array $restrictions The sniff class names to restrict the allowed
1113 * listeners to.
1114 * @param array $exclusions The sniff class names to exclude from the
1115 * listeners list.
1116 *
1117 * @return void
1118 */
1119 public function registerSniffs($files, $restrictions, $exclusions)
1120 {
1121 $listeners = [];
1122
1123 foreach ($files as $file) {
1124 // Work out where the position of /StandardName/Sniffs/... is
1125 // so we can determine what the class will be called.
1126 $sniffPos = strrpos($file, DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR);
1127 if ($sniffPos === false) {
1128 continue;
1129 }
1130
1131 $slashPos = strrpos(substr($file, 0, $sniffPos), DIRECTORY_SEPARATOR);
1132 if ($slashPos === false) {
1133 continue;
1134 }
1135
1136 $className = Autoload::loadFile($file);
1137 $compareName = Util\Common::cleanSniffClass($className);
1138
1139 // If they have specified a list of sniffs to restrict to, check
1140 // to see if this sniff is allowed.
1141 if (empty($restrictions) === false
1142 && isset($restrictions[$compareName]) === false
1143 ) {
1144 continue;
1145 }
1146
1147 // If they have specified a list of sniffs to exclude, check
1148 // to see if this sniff is allowed.
1149 if (empty($exclusions) === false
1150 && isset($exclusions[$compareName]) === true
1151 ) {
1152 continue;
1153 }
1154
1155 // Skip abstract classes.
1156 $reflection = new \ReflectionClass($className);
1157 if ($reflection->isAbstract() === true) {
1158 continue;
1159 }
1160
1161 $listeners[$className] = $className;
1162
1163 if (PHP_CODESNIFFER_VERBOSITY > 2) {
1164 echo "Registered $className".PHP_EOL;
1165 }
1166 }//end foreach
1167
1168 $this->sniffs = $listeners;
1169
1170 }//end registerSniffs()
1171
1172
1173 /**
1174 * Populates the array of PHP_CodeSniffer_Sniff's for this file.
1175 *
1176 * @return void
1177 * @throws RuntimeException If sniff registration fails.
1178 */
1179 public function populateTokenListeners()
1180 {
1181 // Construct a list of listeners indexed by token being listened for.
1182 $this->tokenListeners = [];
1183
1184 foreach ($this->sniffs as $sniffClass => $sniffObject) {
1185 $this->sniffs[$sniffClass] = null;
1186 $this->sniffs[$sniffClass] = new $sniffClass();
1187
1188 $sniffCode = Util\Common::getSniffCode($sniffClass);
1189 $this->sniffCodes[$sniffCode] = $sniffClass;
1190
1191 // Set custom properties.
1192 if (isset($this->ruleset[$sniffCode]['properties']) === true) {
1193 foreach ($this->ruleset[$sniffCode]['properties'] as $name => $value) {
1194 $this->setSniffProperty($sniffClass, $name, $value);
1195 }
1196 }
1197
1198 $tokenizers = [];
1199 $vars = get_class_vars($sniffClass);
1200 if (isset($vars['supportedTokenizers']) === true) {
1201 foreach ($vars['supportedTokenizers'] as $tokenizer) {
1202 $tokenizers[$tokenizer] = $tokenizer;
1203 }
1204 } else {
1205 $tokenizers = ['PHP' => 'PHP'];
1206 }
1207
1208 $tokens = $this->sniffs[$sniffClass]->register();
1209 if (is_array($tokens) === false) {
1210 $msg = "Sniff $sniffClass register() method must return an array";
1211 throw new RuntimeException($msg);
1212 }
1213
1214 $ignorePatterns = [];
1215 $patterns = $this->getIgnorePatterns($sniffCode);
1216 foreach ($patterns as $pattern => $type) {
1217 $replacements = [
1218 '\\,' => ',',
1219 '*' => '.*',
1220 ];
1221
1222 $ignorePatterns[] = strtr($pattern, $replacements);
1223 }
1224
1225 $includePatterns = [];
1226 $patterns = $this->getIncludePatterns($sniffCode);
1227 foreach ($patterns as $pattern => $type) {
1228 $replacements = [
1229 '\\,' => ',',
1230 '*' => '.*',
1231 ];
1232
1233 $includePatterns[] = strtr($pattern, $replacements);
1234 }
1235
1236 foreach ($tokens as $token) {
1237 if (isset($this->tokenListeners[$token]) === false) {
1238 $this->tokenListeners[$token] = [];
1239 }
1240
1241 if (isset($this->tokenListeners[$token][$sniffClass]) === false) {
1242 $this->tokenListeners[$token][$sniffClass] = [
1243 'class' => $sniffClass,
1244 'source' => $sniffCode,
1245 'tokenizers' => $tokenizers,
1246 'ignore' => $ignorePatterns,
1247 'include' => $includePatterns,
1248 ];
1249 }
1250 }
1251 }//end foreach
1252
1253 }//end populateTokenListeners()
1254
1255
1256 /**
1257 * Set a single property for a sniff.
1258 *
1259 * @param string $sniffClass The class name of the sniff.
1260 * @param string $name The name of the property to change.
1261 * @param string $value The new value of the property.
1262 *
1263 * @return void
1264 */
1265 public function setSniffProperty($sniffClass, $name, $value)
1266 {
1267 // Setting a property for a sniff we are not using.
1268 if (isset($this->sniffs[$sniffClass]) === false) {
1269 return;
1270 }
1271
1272 $name = trim($name);
1273 if (is_string($value) === true) {
1274 $value = trim($value);
1275 }
1276
1277 if ($value === '') {
1278 $value = null;
1279 }
1280
1281 // Special case for booleans.
1282 if ($value === 'true') {
1283 $value = true;
1284 } else if ($value === 'false') {
1285 $value = false;
1286 } else if (substr($name, -2) === '[]') {
1287 $name = substr($name, 0, -2);
1288 $values = [];
1289 if ($value !== null) {
1290 foreach (explode(',', $value) as $val) {
1291 list($k, $v) = explode('=>', $val.'=>');
1292 if ($v !== '') {
1293 $values[trim($k)] = trim($v);
1294 } else {
1295 $values[] = trim($k);
1296 }
1297 }
1298 }
1299
1300 $value = $values;
1301 }
1302
1303 $this->sniffs[$sniffClass]->$name = $value;
1304
1305 }//end setSniffProperty()
1306
1307
1308 /**
1309 * Gets the array of ignore patterns.
1310 *
1311 * Optionally takes a listener to get ignore patterns specified
1312 * for that sniff only.
1313 *
1314 * @param string $listener The listener to get patterns for. If NULL, all
1315 * patterns are returned.
1316 *
1317 * @return array
1318 */
1319 public function getIgnorePatterns($listener=null)
1320 {
1321 if ($listener === null) {
1322 return $this->ignorePatterns;
1323 }
1324
1325 if (isset($this->ignorePatterns[$listener]) === true) {
1326 return $this->ignorePatterns[$listener];
1327 }
1328
1329 return [];
1330
1331 }//end getIgnorePatterns()
1332
1333
1334 /**
1335 * Gets the array of include patterns.
1336 *
1337 * Optionally takes a listener to get include patterns specified
1338 * for that sniff only.
1339 *
1340 * @param string $listener The listener to get patterns for. If NULL, all
1341 * patterns are returned.
1342 *
1343 * @return array
1344 */
1345 public function getIncludePatterns($listener=null)
1346 {
1347 if ($listener === null) {
1348 return $this->includePatterns;
1349 }
1350
1351 if (isset($this->includePatterns[$listener]) === true) {
1352 return $this->includePatterns[$listener];
1353 }
1354
1355 return [];
1356
1357 }//end getIncludePatterns()
1358
1359
1360 }//end class