mirror of
https://github.com/danog/psalm.git
synced 2024-12-02 09:37:59 +01:00
Warn when an issue handler suppression is unused
This commit is contained in:
parent
e6564c6126
commit
3448c47931
@ -47,6 +47,7 @@
|
||||
<xs:attribute name="findUnusedPsalmSuppress" type="xs:boolean" default="false" />
|
||||
<!-- TODO: Update default to true in Psalm 6 -->
|
||||
<xs:attribute name="findUnusedBaselineEntry" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="findUnusedIssueHandlerSuppression" type="xs:boolean" default="true" />
|
||||
<xs:attribute name="hideExternalErrors" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="hoistConstants" type="xs:boolean" default="false" />
|
||||
<xs:attribute name="ignoreInternalFunctionFalseReturn" type="xs:boolean" default="false" />
|
||||
@ -494,6 +495,7 @@
|
||||
<xs:element name="UnusedClosureParam" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="UnusedConstructor" type="MethodIssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="UnusedDocblockParam" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="UnusedIssueHandlerSuppression" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="UnusedForeachValue" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="UnusedFunctionCall" type="FunctionIssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="UnusedMethod" type="MethodIssueHandlerType" minOccurs="0" />
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
17
docs/running_psalm/issues/UnusedIssueHandlerSuppression.md
Normal file
17
docs/running_psalm/issues/UnusedIssueHandlerSuppression.md
Normal file
@ -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
|
||||
<?php
|
||||
$a = 'Hello, World!';
|
||||
echo $a;
|
||||
```
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issueHandlers>
|
||||
<PossiblyNullOperand errorLevel="suppress"/>
|
||||
</issueHandlers>
|
||||
```
|
@ -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 @@ final class Config
|
||||
'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 @@ final class Config
|
||||
}
|
||||
}
|
||||
|
||||
$config->source_filename = $config_path;
|
||||
if ($config->resolve_from_config_file) {
|
||||
$config->base_dir = $base_dir;
|
||||
} else {
|
||||
@ -1311,6 +1317,12 @@ final class Config
|
||||
$this->composer_class_loader = $loader;
|
||||
}
|
||||
|
||||
/** @return array<string, IssueHandler> */
|
||||
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 @@ final class Config
|
||||
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])) {
|
||||
|
@ -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,
|
||||
|
@ -25,7 +25,7 @@ final class IssueHandler
|
||||
private string $error_level = Config::REPORT_ERROR;
|
||||
|
||||
/**
|
||||
* @var array<ErrorLevelFileFilter>
|
||||
* @var list<ErrorLevelFileFilter>
|
||||
*/
|
||||
private array $custom_levels = [];
|
||||
|
||||
@ -50,6 +50,12 @@ final class IssueHandler
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/** @return list<ErrorLevelFileFilter> */
|
||||
public function getFilters(): array
|
||||
{
|
||||
return $this->custom_levels;
|
||||
}
|
||||
|
||||
public function setCustomLevels(array $customLevels, string $base_dir): void
|
||||
{
|
||||
/** @var array $customLevel */
|
||||
@ -71,6 +77,7 @@ final class IssueHandler
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allows($file_path)) {
|
||||
$custom_level->suppressions++;
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
@ -82,6 +89,7 @@ final class IssueHandler
|
||||
{
|
||||
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 @@ final class IssueHandler
|
||||
{
|
||||
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 @@ final class IssueHandler
|
||||
{
|
||||
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 @@ final class IssueHandler
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsProperty($property_id)) {
|
||||
$custom_level->suppressions++;
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
@ -137,6 +148,7 @@ final class IssueHandler
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsClassConstant($constant_id)) {
|
||||
$custom_level->suppressions++;
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
@ -148,6 +160,7 @@ final class IssueHandler
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsVariable($var_name)) {
|
||||
$custom_level->suppressions++;
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,7 @@ use const PHP_INT_MAX;
|
||||
* used_suppressions: array<string, array<int, bool>>,
|
||||
* function_docblock_manipulators: array<string, array<int, FunctionDocblockManipulator>>,
|
||||
* mutable_classes: array<string, bool>,
|
||||
* issue_handlers: array{type: string, index: int, count: int}[],
|
||||
* }
|
||||
*/
|
||||
|
||||
@ -418,6 +419,10 @@ final class Analyzer
|
||||
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 @@ final class Analyzer
|
||||
'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
|
||||
}
|
||||
|
11
src/Psalm/Issue/UnusedIssueHandlerSuppression.php
Normal file
11
src/Psalm/Issue/UnusedIssueHandlerSuppression.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class UnusedIssueHandlerSuppression extends CodeIssue
|
||||
{
|
||||
public const ERROR_LEVEL = -1;
|
||||
public const SHORTCODE = 326;
|
||||
}
|
@ -17,6 +17,7 @@ use Psalm\Issue\ConfigIssue;
|
||||
use Psalm\Issue\MixedIssue;
|
||||
use Psalm\Issue\TaintedInput;
|
||||
use Psalm\Issue\UnusedBaselineEntry;
|
||||
use Psalm\Issue\UnusedIssueHandlerSuppression;
|
||||
use Psalm\Issue\UnusedPsalmSuppress;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;
|
||||
@ -645,6 +646,43 @@ final class IssueBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if ($codebase->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,
|
||||
|
@ -18,6 +18,7 @@ use Psalm\Internal\Provider\FakeFileProvider;
|
||||
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 @@ class DocumentationTest extends TestCase
|
||||
case 'TraitMethodSignatureMismatch':
|
||||
case 'UncaughtThrowInGlobalScope':
|
||||
case UnusedBaselineEntry::getIssueType():
|
||||
case UnusedIssueHandlerSuppression::getIssueType():
|
||||
continue 2;
|
||||
|
||||
/** @todo reinstate this test when the issue is restored */
|
||||
|
Loading…
Reference in New Issue
Block a user