Skip to content

Commit 95e3a35

Browse files
authored
Merge pull request #857 from PHPCSStandards/feature/ruleset-improve-error-handling
Ruleset: improve error handling
2 parents 4e2c91d + 45baf5c commit 95e3a35

11 files changed

+1252
-36
lines changed

src/Ruleset.php

+80-28
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHP_CodeSniffer\Exceptions\RuntimeException;
1515
use PHP_CodeSniffer\Sniffs\DeprecatedSniff;
1616
use PHP_CodeSniffer\Util\Common;
17+
use PHP_CodeSniffer\Util\MessageCollector;
1718
use PHP_CodeSniffer\Util\Standards;
1819
use RecursiveDirectoryIterator;
1920
use RecursiveIteratorIterator;
@@ -131,21 +132,36 @@ class Ruleset
131132
*/
132133
private $deprecatedSniffs = [];
133134

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

135150
/**
136151
* Initialise the ruleset that the run will use.
137152
*
138153
* @param \PHP_CodeSniffer\Config $config The config data for the run.
139154
*
140155
* @return void
141-
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If no sniffs were registered.
156+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If blocking errors were encountered when processing the ruleset.
142157
*/
143158
public function __construct(Config $config)
144159
{
145-
$this->config = $config;
146-
$restrictions = $config->sniffs;
147-
$exclusions = $config->exclude;
148-
$sniffs = [];
160+
$this->config = $config;
161+
$restrictions = $config->sniffs;
162+
$exclusions = $config->exclude;
163+
$sniffs = [];
164+
$this->msgCache = new MessageCollector();
149165

150166
$standardPaths = [];
151167
foreach ($config->standards as $standard) {
@@ -186,11 +202,11 @@ public function __construct(Config $config)
186202

187203
if (defined('PHP_CODESNIFFER_IN_TESTS') === true && empty($restrictions) === false) {
188204
// In unit tests, only register the sniffs that the test wants and not the entire standard.
189-
try {
190-
foreach ($restrictions as $restriction) {
191-
$sniffs = array_merge($sniffs, $this->expandRulesetReference($restriction, dirname($standard)));
192-
}
193-
} catch (RuntimeException $e) {
205+
foreach ($restrictions as $restriction) {
206+
$sniffs = array_merge($sniffs, $this->expandRulesetReference($restriction, dirname($standard)));
207+
}
208+
209+
if (empty($sniffs) === true) {
194210
// Sniff reference could not be expanded, which probably means this
195211
// is an installed standard. Let the unit test system take care of
196212
// setting the correct sniff for testing.
@@ -239,9 +255,11 @@ public function __construct(Config $config)
239255
}
240256

241257
if ($numSniffs === 0) {
242-
throw new RuntimeException('ERROR: No sniffs were registered');
258+
$this->msgCache->add('No sniffs were registered.', MessageCollector::ERROR);
243259
}
244260

261+
$this->displayCachedMessages();
262+
245263
}//end __construct()
246264

247265

@@ -461,6 +479,35 @@ public function showSniffDeprecations()
461479
}//end showSniffDeprecations()
462480

463481

482+
/**
483+
* Print any notices encountered while processing the ruleset(s).
484+
*
485+
* Note: these messages aren't shown at the time they are encountered to avoid "one error hiding behind another".
486+
* This way the (end-)user gets to see all of them in one go.
487+
*
488+
* @return void
489+
*
490+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If blocking errors were encountered.
491+
*/
492+
private function displayCachedMessages()
493+
{
494+
// Don't show deprecations/notices/warnings in quiet mode, in explain mode
495+
// or when the documentation is being shown.
496+
// Documentation and explain will call the Ruleset multiple times which
497+
// would lead to duplicate display of the messages.
498+
if ($this->msgCache->containsBlockingErrors() === false
499+
&& ($this->config->quiet === true
500+
|| $this->config->explain === true
501+
|| $this->config->generator !== null)
502+
) {
503+
return;
504+
}
505+
506+
$this->msgCache->display();
507+
508+
}//end displayCachedMessages()
509+
510+
464511
/**
465512
* Processes a single ruleset and returns a list of the sniffs it represents.
466513
*
@@ -993,8 +1040,8 @@ private function expandRulesetReference($ref, $rulesetDir, $depth=0)
9931040
}
9941041
} else {
9951042
if (is_file($ref) === false) {
996-
$error = "ERROR: Referenced sniff \"$ref\" does not exist";
997-
throw new RuntimeException($error);
1043+
$this->msgCache->add("Referenced sniff \"$ref\" does not exist.", MessageCollector::ERROR);
1044+
return [];
9981045
}
9991046

10001047
if (substr($ref, -9) === 'Sniff.php') {
@@ -1083,18 +1130,19 @@ private function processRule($rule, $newSniffs, $depth=0)
10831130

10841131
$type = strtolower((string) $rule->type);
10851132
if ($type !== 'error' && $type !== 'warning') {
1086-
throw new RuntimeException("ERROR: Message type \"$type\" is invalid; must be \"error\" or \"warning\"");
1087-
}
1133+
$message = "Message type \"$type\" for \"$code\" is invalid; must be \"error\" or \"warning\".";
1134+
$this->msgCache->add($message, MessageCollector::ERROR);
1135+
} else {
1136+
$this->ruleset[$code]['type'] = $type;
1137+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
1138+
echo str_repeat("\t", $depth);
1139+
echo "\t\t=> message type set to ".(string) $rule->type;
1140+
if ($code !== $ref) {
1141+
echo " for $code";
1142+
}
10881143

1089-
$this->ruleset[$code]['type'] = $type;
1090-
if (PHP_CODESNIFFER_VERBOSITY > 1) {
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";
1144+
echo PHP_EOL;
10951145
}
1096-
1097-
echo PHP_EOL;
10981146
}
10991147
}//end if
11001148

@@ -1414,8 +1462,12 @@ public function populateTokenListeners()
14141462

14151463
$tokens = $this->sniffs[$sniffClass]->register();
14161464
if (is_array($tokens) === false) {
1417-
$msg = "ERROR: Sniff $sniffClass register() method must return an array";
1418-
throw new RuntimeException($msg);
1465+
$msg = "The sniff {$sniffClass}::register() method must return an array.";
1466+
$this->msgCache->add($msg, MessageCollector::ERROR);
1467+
1468+
// Unregister the sniff.
1469+
unset($this->sniffs[$sniffClass], $this->sniffCodes[$sniffCode], $this->deprecatedSniffs[$sniffCode]);
1470+
continue;
14191471
}
14201472

14211473
$ignorePatterns = [];
@@ -1525,9 +1577,9 @@ public function setSniffProperty($sniffClass, $name, $settings)
15251577

15261578
if ($isSettable === false) {
15271579
if ($settings['scope'] === 'sniff') {
1528-
$notice = "ERROR: Ruleset invalid. Property \"$propertyName\" does not exist on sniff ";
1529-
$notice .= array_search($sniffClass, $this->sniffCodes, true);
1530-
throw new RuntimeException($notice);
1580+
$notice = "Property \"$propertyName\" does not exist on sniff ";
1581+
$notice .= array_search($sniffClass, $this->sniffCodes, true).'.';
1582+
$this->msgCache->add($notice, MessageCollector::ERROR);
15311583
}
15321584

15331585
return;

0 commit comments

Comments
 (0)