1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-02 09:37:59 +01:00

Fix #1444 - track unused suppressions

This commit is contained in:
Matthew Brown 2019-08-18 14:27:50 -04:00
parent 2146f73782
commit 2a5e0d8f39
16 changed files with 190 additions and 11 deletions

View File

@ -344,6 +344,7 @@
<xs:element name="UnusedVariable" type="IssueHandlerType" minOccurs="0" /> <xs:element name="UnusedVariable" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnusedParam" type="IssueHandlerType" minOccurs="0" /> <xs:element name="UnusedParam" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnusedProperty" type="PropertyIssueHandlerType" minOccurs="0" /> <xs:element name="UnusedProperty" type="PropertyIssueHandlerType" minOccurs="0" />
<xs:element name="UnusedPsalmSuppress" type="FunctionIssueHandlerType" minOccurs="0" />
<xs:element name="UnusedClass" type="ClassIssueHandlerType" minOccurs="0" /> <xs:element name="UnusedClass" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="UnusedMethod" type="MethodIssueHandlerType" minOccurs="0" /> <xs:element name="UnusedMethod" type="MethodIssueHandlerType" minOccurs="0" />
</xs:choice> </xs:choice>

View File

@ -2431,6 +2431,15 @@ $a = new A();
echo $a->getFoo(); echo $a->getFoo();
``` ```
### UnusedPsalmSuppress
Emitted when `--find-unused-psalm-suppress` is turned on and Psalm cannot find any uses of a given `@psalm-suppress` annotation
```php
/** @psalm-suppress InvalidArgument */
echo strpos("hello", "e");
```
### UnusedVariable ### UnusedVariable
Emitted when `--find-dead-code` is turned on and Psalm cannot find any references to a variable, once instantiated Emitted when `--find-dead-code` is turned on and Psalm cannot find any references to a variable, once instantiated

View File

@ -14,6 +14,7 @@ class Raw extends \Psalm\CodeLocation
public function __construct( public function __construct(
string $file_contents, string $file_contents,
string $file_path, string $file_path,
string $file_name,
int $file_start, int $file_start,
int $file_end int $file_end
) { ) {
@ -22,7 +23,7 @@ class Raw extends \Psalm\CodeLocation
$this->raw_file_start = $this->file_start; $this->raw_file_start = $this->file_start;
$this->raw_file_end = $this->file_end; $this->raw_file_end = $this->file_end;
$this->file_path = $file_path; $this->file_path = $file_path;
$this->file_name = $file_path; $this->file_name = $file_name;
$this->single_line = false; $this->single_line = false;
$this->preview_start = $this->file_start; $this->preview_start = $this->file_start;

View File

@ -272,7 +272,10 @@ class Codebase
*/ */
public $php_minor_version = PHP_MINOR_VERSION; public $php_minor_version = PHP_MINOR_VERSION;
/**
* @var bool
*/
public $track_unused_suppressions = false;
public function __construct( public function __construct(
Config $config, Config $config,
@ -1032,7 +1035,13 @@ class Codebase
$file_contents = $this->getFileContents($file_path); $file_contents = $this->getFileContents($file_path);
return new CodeLocation\Raw($file_contents, $file_path, (int) $symbol_parts[0], (int) $symbol_parts[1]); return new CodeLocation\Raw(
$file_contents,
$file_path,
$this->config->shortenFileName($file_path),
(int) $symbol_parts[0],
(int) $symbol_parts[1]
);
} }
try { try {

View File

@ -518,7 +518,7 @@ class CommentAnalyzer
if (isset($parsed_docblock['specials']['psalm-suppress'])) { if (isset($parsed_docblock['specials']['psalm-suppress'])) {
foreach ($parsed_docblock['specials']['psalm-suppress'] as $offset => $suppress_entry) { foreach ($parsed_docblock['specials']['psalm-suppress'] as $offset => $suppress_entry) {
$info->suppressed_issues[$offset] = preg_split('/[\s]+/', $suppress_entry)[0]; $info->suppressed_issues[$offset + $comment->getFilePos()] = preg_split('/[\s]+/', $suppress_entry)[0];
} }
} }
@ -866,8 +866,8 @@ class CommentAnalyzer
} }
if (isset($parsed_docblock['specials']['psalm-suppress'])) { if (isset($parsed_docblock['specials']['psalm-suppress'])) {
foreach ($parsed_docblock['specials']['psalm-suppress'] as $suppress_entry) { foreach ($parsed_docblock['specials']['psalm-suppress'] as $offset => $suppress_entry) {
$info->suppressed_issues[] = preg_split('/[\s]+/', $suppress_entry)[0]; $info->suppressed_issues[$offset + $comment->getFilePos()] = preg_split('/[\s]+/', $suppress_entry)[0];
} }
} }

View File

@ -1016,6 +1016,12 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
} }
} }
if ($codebase->track_unused_suppressions) {
foreach ($storage->suppressed_issues as $offset => $issue_name) {
IssueBuffer::addUnusedSuppression($this->getFilePath(), $offset, $issue_name);
}
}
foreach ($storage->throws as $expected_exception => $_) { foreach ($storage->throws as $expected_exception => $_) {
if (isset($storage->throw_locations[$expected_exception])) { if (isset($storage->throw_locations[$expected_exception])) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(

View File

@ -558,6 +558,14 @@ class ProjectAnalyzer
$this->codebase->taint = new Taint(); $this->codebase->taint = new Taint();
} }
/**
* @return void
*/
public function trackUnusedSuppressions()
{
$this->codebase->track_unused_suppressions = true;
}
public function interpretRefactors() : void public function interpretRefactors() : void
{ {
if (!$this->codebase->alter_code) { if (!$this->codebase->alter_code) {

View File

@ -297,8 +297,13 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
); );
if ($suppressed) { if ($suppressed) {
$new_issues = array_diff($suppressed, $this->source->getSuppressedIssues()); $new_issues = [];
/** @psalm-suppress MixedTypeCoercion */
foreach ($suppressed as $offset => $issue_type) {
$new_issues[$offset + $docblock->getFilePos()] = $issue_type;
IssueBuffer::addUnusedSuppression($this->getFilePath(), $offset, $issue_type);
}
$this->addSuppressedIssues($new_issues); $this->addSuppressedIssues($new_issues);
} }
} }

View File

@ -69,7 +69,8 @@ use function usort;
* class_method_locations: array<string, array<int, \Psalm\CodeLocation>>, * class_method_locations: array<string, array<int, \Psalm\CodeLocation>>,
* class_property_locations: array<string, array<int, \Psalm\CodeLocation>>, * class_property_locations: array<string, array<int, \Psalm\CodeLocation>>,
* possible_method_param_types: array<string, array<int, \Psalm\Type\Union>>, * possible_method_param_types: array<string, array<int, \Psalm\Type\Union>>,
* taint_data: ?\Psalm\Internal\Codebase\Taint * taint_data: ?\Psalm\Internal\Codebase\Taint,
* unused_suppressions: array<string, array<int, int>>
* } * }
*/ */
@ -408,6 +409,7 @@ class Analyzer
'class_property_locations' => $rerun ? [] : $file_reference_provider->getAllClassPropertyLocations(), 'class_property_locations' => $rerun ? [] : $file_reference_provider->getAllClassPropertyLocations(),
'possible_method_param_types' => $rerun ? [] : $analyzer->getPossibleMethodParamTypes(), 'possible_method_param_types' => $rerun ? [] : $analyzer->getPossibleMethodParamTypes(),
'taint_data' => $codebase->taint, 'taint_data' => $codebase->taint,
'unused_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUnusedSuppressions() : [],
]; ];
// @codingStandardsIgnoreEnd // @codingStandardsIgnoreEnd
}, },
@ -427,6 +429,10 @@ class Analyzer
foreach ($forked_pool_data as $pool_data) { foreach ($forked_pool_data as $pool_data) {
IssueBuffer::addIssues($pool_data['issues']); IssueBuffer::addIssues($pool_data['issues']);
if ($codebase->track_unused_suppressions) {
IssueBuffer::addUnusedSuppressions($pool_data['unused_suppressions']);
}
if ($codebase->taint && $pool_data['taint_data']) { if ($codebase->taint && $pool_data['taint_data']) {
$codebase->taint->addThreadData($pool_data['taint_data']); $codebase->taint->addThreadData($pool_data['taint_data']);
} }
@ -531,6 +537,10 @@ class Analyzer
$codebase->file_reference_provider->addIssue($issue_data['file_path'], $issue_data); $codebase->file_reference_provider->addIssue($issue_data['file_path'], $issue_data);
} }
} }
if ($codebase->track_unused_suppressions) {
IssueBuffer::processUnusedSuppressions($codebase->file_provider);
}
} }
/** /**

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class UnusedPsalmSuppress extends CodeIssue
{
}

View File

@ -13,6 +13,7 @@ use function microtime;
use function number_format; use function number_format;
use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Issue\CodeIssue; use Psalm\Issue\CodeIssue;
use Psalm\Issue\UnusedPsalmSuppress;
use Psalm\Report\CheckstyleReport; use Psalm\Report\CheckstyleReport;
use Psalm\Report\CompactReport; use Psalm\Report\CompactReport;
use Psalm\Report\ConsoleReport; use Psalm\Report\ConsoleReport;
@ -58,6 +59,11 @@ class IssueBuffer
/** @var array<int, array<int, CodeIssue>> */ /** @var array<int, array<int, CodeIssue>> */
protected static $recorded_issues = []; protected static $recorded_issues = [];
/**
* @var array<string, array<int, int>>
*/
protected static $unused_suppressions = [];
/** /**
* @param CodeIssue $e * @param CodeIssue $e
* @param string[] $suppressed_issues * @param string[] $suppressed_issues
@ -73,6 +79,15 @@ class IssueBuffer
return self::add($e); return self::add($e);
} }
public static function addUnusedSuppression(string $file_path, int $offset, string $issue_type) : void
{
if (!isset(self::$unused_suppressions[$file_path])) {
self::$unused_suppressions[$file_path] = [];
}
self::$unused_suppressions[$file_path][$offset] = $offset + \strlen($issue_type) - 1;
}
/** /**
* @param CodeIssue $e * @param CodeIssue $e
* @param string[] $suppressed_issues * @param string[] $suppressed_issues
@ -85,14 +100,17 @@ class IssueBuffer
$fqcn_parts = explode('\\', get_class($e)); $fqcn_parts = explode('\\', get_class($e));
$issue_type = array_pop($fqcn_parts); $issue_type = array_pop($fqcn_parts);
$file_path = $e->getFilePath();
if (!$config->reportIssueInFile($issue_type, $e->getFilePath())) { if (!$config->reportIssueInFile($issue_type, $file_path)) {
return true; return true;
} }
$suppressed_issue_position = array_search($issue_type, $suppressed_issues); $suppressed_issue_position = array_search($issue_type, $suppressed_issues);
if ($suppressed_issue_position !== false) { if ($suppressed_issue_position !== false) {
/** @psalm-suppress MixedArrayTypeCoercion */
unset(self::$unused_suppressions[$file_path][$suppressed_issue_position]);
return true; return true;
} }
@ -102,6 +120,8 @@ class IssueBuffer
$suppressed_issue_position = array_search($parent_issue_type, $suppressed_issues); $suppressed_issue_position = array_search($parent_issue_type, $suppressed_issues);
if ($suppressed_issue_position !== false) { if ($suppressed_issue_position !== false) {
/** @psalm-suppress MixedArrayTypeCoercion */
unset(self::$unused_suppressions[$file_path][$suppressed_issue_position]);
return true; return true;
} }
} }
@ -190,6 +210,47 @@ class IssueBuffer
return self::$issues_data; return self::$issues_data;
} }
/**
* @return array<string, array<int, int>>
*/
public static function getUnusedSuppressions() : array
{
return self::$unused_suppressions;
}
public static function addUnusedSuppressions(array $unused_suppressions) : void
{
self::$unused_suppressions += $unused_suppressions;
}
public static function processUnusedSuppressions(\Psalm\Internal\Provider\FileProvider $file_provider) : void
{
$config = Config::getInstance();
foreach (self::$unused_suppressions as $file_path => $offsets) {
if (!$offsets) {
continue;
}
$file_contents = $file_provider->getContents($file_path);
foreach ($offsets as $start => $end) {
self::add(
new UnusedPsalmSuppress(
'This suppression is never used',
new CodeLocation\Raw(
$file_contents,
$file_path,
$config->shortenFileName($file_path),
$start,
$end
)
)
);
}
}
}
/** /**
* @return int * @return int
*/ */
@ -492,6 +553,7 @@ class IssueBuffer
self::$recording_level = 0; self::$recording_level = 0;
self::$recorded_issues = []; self::$recorded_issues = [];
self::$console_issues = []; self::$console_issues = [];
self::$unused_suppressions = [];
} }
/** /**

View File

@ -321,6 +321,9 @@ Options:
--find-unused-code[=auto] --find-unused-code[=auto]
Look for unused code. Options are 'auto' or 'always'. If no value is specified, default is 'auto' Look for unused code. Options are 'auto' or 'always'. If no value is specified, default is 'auto'
--find-unused-psalm-suppress
Finds all @psalm-suppress annotations that arent used
--find-references-to=[class|method|property] --find-references-to=[class|method|property]
Searches the codebase for references to the given fully-qualified class or method, Searches the codebase for references to the given fully-qualified class or method,
where method is in the format class::methodName where method is in the format class::methodName

View File

@ -62,7 +62,8 @@ $valid_long_options = [
'shepherd::', 'shepherd::',
'no-progress', 'no-progress',
'include-php-versions', // used for baseline 'include-php-versions', // used for baseline
'track-tainted-input' 'track-tainted-input',
'find-unused-psalm-suppress',
]; ];
gc_collect_cycles(); gc_collect_cycles();
@ -511,6 +512,10 @@ if (isset($options['track-tainted-input'])) {
$project_analyzer->trackTaintedInputs(); $project_analyzer->trackTaintedInputs();
} }
if (isset($options['find-unused-psalm-suppress'])) {
$project_analyzer->trackUnusedSuppressions();
}
/** @var string $plugin_path */ /** @var string $plugin_path */
foreach ($plugins as $plugin_path) { foreach ($plugins as $plugin_path) {
$config->addPluginPath($plugin_path); $config->addPluginPath($plugin_path);

View File

@ -143,6 +143,7 @@ class DocumentationTest extends TestCase
if ($check_references) { if ($check_references) {
$this->project_analyzer->getCodebase()->reportUnusedCode(); $this->project_analyzer->getCodebase()->reportUnusedCode();
$this->project_analyzer->trackUnusedSuppressions();
} }
foreach ($error_levels as $error_level) { foreach ($error_levels as $error_level) {

View File

@ -8,6 +8,55 @@ class IssueSuppressionTest extends TestCase
use Traits\ValidCodeAnalysisTestTrait; use Traits\ValidCodeAnalysisTestTrait;
use Traits\InvalidCodeAnalysisTestTrait; use Traits\InvalidCodeAnalysisTestTrait;
/**
* @return void
*/
public function testIssueSuppressedOnFunction()
{
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage('UnusedPsalmSuppress');
$this->project_analyzer->trackUnusedSuppressions();
$this->addFile(
'somefile.php',
'<?php
class A {
/**
* @psalm-suppress UndefinedClass
* @psalm-suppress MixedMethodCall
* @psalm-suppress MissingReturnType
* @psalm-suppress UnusedVariable
*/
public function b() {
B::fooFoo()->barBar()->bat()->baz()->bam()->bas()->bee()->bet()->bes()->bis();
}
}'
);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
/**
* @return void
*/
public function testIssueSuppressedOnStatement()
{
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage('UnusedPsalmSuppress');
$this->project_analyzer->trackUnusedSuppressions();
$this->addFile(
'somefile.php',
'<?php
/** @psalm-suppress InvalidArgument */
echo strpos("hello", "e");'
);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
/** /**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}> * @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/ */

View File

@ -119,6 +119,10 @@ class TestCase extends BaseTestCase
$file_analyzer->analyze($context); $file_analyzer->analyze($context);
} }
} }
if ($codebase->track_unused_suppressions) {
\Psalm\IssueBuffer::processUnusedSuppressions($codebase->file_provider);
}
} }
/** /**