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