diff --git a/config.xsd b/config.xsd index 72745ea604f..88d7b527fe4 100644 --- a/config.xsd +++ b/config.xsd @@ -47,6 +47,7 @@ + @@ -494,6 +495,7 @@ + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index 05f65236f0c..ac222f95148 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -513,6 +513,11 @@ class PremiumCar extends StandardCar { Emits [UnusedBaselineEntry](issues/UnusedBaselineEntry.md) when a baseline entry is not being used to suppress an issue. +#### findUnusedIssueHandlerSuppression + +Emits [UnusedIssueHandlerSuppression](issues/UnusedIssueHandlerSuppression.md) when a suppressed issue handler +is not being used to suppress an issue. + ## Project settings #### <projectFiles> diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index f2655635cf1..b9e3d8fe25f 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -298,6 +298,7 @@ - [UnusedDocblockParam](issues/UnusedDocblockParam.md) - [UnusedForeachValue](issues/UnusedForeachValue.md) - [UnusedFunctionCall](issues/UnusedFunctionCall.md) + - [UnusedIssueHandlerSuppression](issues/UnusedIssueHandlerSuppression.md) - [UnusedMethod](issues/UnusedMethod.md) - [UnusedMethodCall](issues/UnusedMethodCall.md) - [UnusedParam](issues/UnusedParam.md) diff --git a/docs/running_psalm/issues/UnusedIssueHandlerSuppression.md b/docs/running_psalm/issues/UnusedIssueHandlerSuppression.md new file mode 100644 index 00000000000..dc796e35265 --- /dev/null +++ b/docs/running_psalm/issues/UnusedIssueHandlerSuppression.md @@ -0,0 +1,17 @@ +# UnusedIssueHandlerSuppression + +Emitted when an issue type suppression in the configuration file is not being used to suppress an issue. + +Enabled by [findUnusedIssueHandlerSuppression](../configuration.md#findunusedissuehandlersuppression) + +```php + + + + +``` diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..5e8e8ac33d0 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -13,6 +13,7 @@ errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" findUnusedBaselineEntry="true" + findUnusedIssueHandlerSuppression="true" > @@ -63,24 +64,6 @@ - - - - - - - - - - - - - - - - - - @@ -104,12 +87,6 @@ - - - - - - diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 8759c0ddbcd..a91336d2cc5 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -230,6 +230,8 @@ final class Config */ public string $base_dir; + public ?string $source_filename = null; + /** * The PHP version to assume as declared in the config file */ @@ -369,6 +371,8 @@ final class Config public bool $find_unused_baseline_entry = true; + public bool $find_unused_issue_handler_suppression = true; + public bool $run_taint_analysis = false; public bool $use_phpstorm_meta_path = true; @@ -935,6 +939,7 @@ private static function fromXmlAndPaths( 'allowNamedArgumentCalls' => 'allow_named_arg_calls', 'findUnusedPsalmSuppress' => 'find_unused_psalm_suppress', 'findUnusedBaselineEntry' => 'find_unused_baseline_entry', + 'findUnusedIssueHandlerSuppression' => 'find_unused_issue_handler_suppression', 'reportInfo' => 'report_info', 'restrictReturnTypes' => 'restrict_return_types', 'limitMethodComplexity' => 'limit_method_complexity', @@ -950,6 +955,7 @@ private static function fromXmlAndPaths( } } + $config->source_filename = $config_path; if ($config->resolve_from_config_file) { $config->base_dir = $base_dir; } else { @@ -1311,6 +1317,12 @@ public function setComposerClassLoader(?ClassLoader $loader = null): void $this->composer_class_loader = $loader; } + /** @return array */ + public function getIssueHandlers(): array + { + return $this->issue_handlers; + } + public function setAdvancedErrorLevel(string $issue_key, array $config, ?string $default_error_level = null): void { $this->issue_handlers[$issue_key] = new IssueHandler(); @@ -1858,6 +1870,30 @@ public static function getParentIssueType(string $issue_type): ?string return null; } + /** @return array{type: string, index: int, count: int}[] */ + public function getIssueHandlerSuppressions(): array + { + $suppressions = []; + foreach ($this->issue_handlers as $key => $handler) { + foreach ($handler->getFilters() as $index => $filter) { + $suppressions[] = [ + 'type' => $key, + 'index' => $index, + 'count' => $filter->suppressions, + ]; + } + } + return $suppressions; + } + + /** @param array{type: string, index: int, count: int}[] $filters */ + public function combineIssueHandlerSuppressions(array $filters): void + { + foreach ($filters as $filter) { + $this->issue_handlers[$filter['type']]->getFilters()[$filter['index']]->suppressions += $filter['count']; + } + } + public function getReportingLevelForFile(string $issue_type, string $file_path): string { if (isset($this->issue_handlers[$issue_type])) { diff --git a/src/Psalm/Config/ErrorLevelFileFilter.php b/src/Psalm/Config/ErrorLevelFileFilter.php index 1778ccfae35..de3ed732c19 100644 --- a/src/Psalm/Config/ErrorLevelFileFilter.php +++ b/src/Psalm/Config/ErrorLevelFileFilter.php @@ -15,6 +15,8 @@ final class ErrorLevelFileFilter extends FileFilter { private string $error_level = ''; + public int $suppressions = 0; + public static function loadFromArray( array $config, string $base_dir, diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php index a5af5aefe4b..aba87f0232b 100644 --- a/src/Psalm/Config/IssueHandler.php +++ b/src/Psalm/Config/IssueHandler.php @@ -25,7 +25,7 @@ final class IssueHandler private string $error_level = Config::REPORT_ERROR; /** - * @var array + * @var list */ private array $custom_levels = []; @@ -50,6 +50,12 @@ public static function loadFromXMLElement(SimpleXMLElement $e, string $base_dir) return $handler; } + /** @return list */ + public function getFilters(): array + { + return $this->custom_levels; + } + public function setCustomLevels(array $customLevels, string $base_dir): void { /** @var array $customLevel */ @@ -71,6 +77,7 @@ public function getReportingLevelForFile(string $file_path): string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allows($file_path)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -82,6 +89,7 @@ public function getReportingLevelForClass(string $fq_classlike_name): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsClass($fq_classlike_name)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -93,6 +101,7 @@ public function getReportingLevelForMethod(string $method_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsMethod(strtolower($method_id))) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -115,6 +124,7 @@ public function getReportingLevelForArgument(string $function_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsMethod(strtolower($function_id))) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -126,6 +136,7 @@ public function getReportingLevelForProperty(string $property_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsProperty($property_id)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -137,6 +148,7 @@ public function getReportingLevelForClassConstant(string $constant_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsClassConstant($constant_id)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -148,6 +160,7 @@ public function getReportingLevelForVariable(string $var_name): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsVariable($var_name)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index e7eb49e1c15..461aae3e153 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -89,6 +89,7 @@ * used_suppressions: array>, * function_docblock_manipulators: array>, * mutable_classes: array, + * issue_handlers: array{type: string, index: int, count: int}[], * } */ @@ -418,6 +419,10 @@ static function (): void { IssueBuffer::addUsedSuppressions($pool_data['used_suppressions']); } + if ($codebase->config->find_unused_issue_handler_suppression) { + $codebase->config->combineIssueHandlerSuppressions($pool_data['issue_handlers']); + } + if ($codebase->taint_flow_graph && $pool_data['taint_data']) { $codebase->taint_flow_graph->addGraph($pool_data['taint_data']); } @@ -1639,6 +1644,7 @@ private function getWorkerData(): array 'used_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUsedSuppressions() : [], 'function_docblock_manipulators' => FunctionDocblockManipulator::getManipulators(), 'mutable_classes' => $codebase->analyzer->mutable_classes, + 'issue_handlers' => $this->config->getIssueHandlerSuppressions() ]; // @codingStandardsIgnoreEnd } diff --git a/src/Psalm/Issue/UnusedIssueHandlerSuppression.php b/src/Psalm/Issue/UnusedIssueHandlerSuppression.php new file mode 100644 index 00000000000..43699843d26 --- /dev/null +++ b/src/Psalm/Issue/UnusedIssueHandlerSuppression.php @@ -0,0 +1,11 @@ +config->find_unused_issue_handler_suppression) { + foreach ($codebase->config->getIssueHandlers() as $type => $handler) { + foreach ($handler->getFilters() as $filter) { + if ($filter->suppressions > 0 && $filter->getErrorLevel() == Config::REPORT_SUPPRESS) { + continue; + } + $issues_data['config'][] = new IssueData( + IssueData::SEVERITY_ERROR, + 0, + 0, + UnusedIssueHandlerSuppression::getIssueType(), + sprintf( + 'Suppressed issue type "%s" for %s was not thrown.', + $type, + str_replace( + $codebase->config->base_dir, + '', + implode(', ', [...$filter->getFiles(), ...$filter->getDirectories()]), + ), + ), + $codebase->config->source_filename ?? '', + '', + '', + '', + 0, + 0, + 0, + 0, + 0, + 0, + UnusedIssueHandlerSuppression::SHORTCODE, + UnusedIssueHandlerSuppression::ERROR_LEVEL, + ); + } + } + } + echo self::getOutput( $issues_data, $project_analyzer->stdout_report_options, diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index d86293a8241..a8d135e4a88 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -18,6 +18,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Issue\UnusedBaselineEntry; +use Psalm\Issue\UnusedIssueHandlerSuppression; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use UnexpectedValueException; @@ -270,6 +271,7 @@ public function providerInvalidCodeParse(): array case 'TraitMethodSignatureMismatch': case 'UncaughtThrowInGlobalScope': case UnusedBaselineEntry::getIssueType(): + case UnusedIssueHandlerSuppression::getIssueType(): continue 2; /** @todo reinstate this test when the issue is restored */