1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

[FEATURE] Allow to intercept adding issue in IssueBuffer

This change introduces new `BeforeAddIssueEvent` which is invoked
from `IssueBuffer::add`, which allows to collect and intercept code
issue in a generic way.

Resolves: #7528
This commit is contained in:
Oliver Hader 2021-02-01 17:00:07 +01:00
parent f93bd10c61
commit e3602bbfe1
No known key found for this signature in database
GPG Key ID: C19FAFD699012A5A
6 changed files with 133 additions and 0 deletions

View File

@ -81,6 +81,7 @@ class SomePlugin implements \Psalm\Plugin\EventHandler\AfterStatementAnalysisInt
- `AfterFunctionLikeAnalysisInterface` - called after Psalm has completed its analysis of a given function-like.
- `AfterMethodCallAnalysisInterface` - called after Psalm analyzes a method call.
- `AfterStatementAnalysisInterface` - called after Psalm evaluates an statement.
- `BeforeAddIssueInterface` - called before Psalm adds an item to it's internal `IssueBuffer`, allows handling code issues individually
- `BeforeFileAnalysisInterface` - called before Psalm analyzes a file.
- `FunctionExistenceProviderInterface` - can be used to override Psalm's builtin function existence checks for one or more functions.
- `FunctionParamsProviderInterface.php` - can be used to override Psalm's builtin function parameter lookup for one or more functions.

View File

@ -15,6 +15,7 @@ use Psalm\Plugin\EventHandler\AfterFunctionCallAnalysisInterface;
use Psalm\Plugin\EventHandler\AfterFunctionLikeAnalysisInterface;
use Psalm\Plugin\EventHandler\AfterMethodCallAnalysisInterface;
use Psalm\Plugin\EventHandler\AfterStatementAnalysisInterface;
use Psalm\Plugin\EventHandler\BeforeAddIssueInterface;
use Psalm\Plugin\EventHandler\BeforeFileAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent;
@ -29,6 +30,7 @@ use Psalm\Plugin\EventHandler\Event\AfterFunctionCallAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;
use Psalm\Plugin\EventHandler\Event\BeforeFileAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\StringInterpreterEvent;
use Psalm\Plugin\EventHandler\RemoveTaintsInterface;
@ -37,6 +39,7 @@ use Psalm\Type\Atomic\TLiteralString;
use function array_merge;
use function count;
use function is_bool;
use function is_subclass_of;
/**
@ -122,6 +125,11 @@ class EventDispatcher
*/
public $after_codebase_populated = [];
/**
* @var list<class-string<BeforeAddIssueInterface>>
*/
private array $before_add_issue = [];
/**
* Static methods to be called after codebase has been populated
*
@ -209,6 +217,10 @@ class EventDispatcher
$this->after_codebase_populated[] = $class;
}
if (is_subclass_of($class, BeforeAddIssueInterface::class)) {
$this->before_add_issue[] = $class;
}
if (is_subclass_of($class, AfterAnalysisInterface::class)) {
$this->after_analysis[] = $class;
}
@ -330,6 +342,17 @@ class EventDispatcher
}
}
public function dispatchBeforeAddIssue(BeforeAddIssueEvent $event): ?bool
{
foreach ($this->before_add_issue as $handler) {
$result = $handler::beforeAddIssue($event);
if (is_bool($result)) {
return $result;
}
}
return null;
}
public function dispatchAfterAnalysis(AfterAnalysisEvent $event): void
{
foreach ($this->after_analysis as $handler) {

View File

@ -16,6 +16,7 @@ use Psalm\Issue\MixedIssue;
use Psalm\Issue\TaintedInput;
use Psalm\Issue\UnusedPsalmSuppress;
use Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;
use Psalm\Report\CheckstyleReport;
use Psalm\Report\CodeClimateReport;
use Psalm\Report\CompactReport;
@ -250,6 +251,11 @@ class IssueBuffer
{
$config = Config::getInstance();
$event = new BeforeAddIssueEvent($e, $is_fixable);
if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) {
return false;
};
$fqcn_parts = explode('\\', get_class($e));
$issue_type = array_pop($fqcn_parts);

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Psalm\Plugin\EventHandler;
use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;
interface BeforeAddIssueInterface
{
/**
* Called before adding a code issue.
*
* @param BeforeAddIssueEvent $event
* @return null|bool $event How and whether to continue:
* + `null` continues with next event handler
* + `true` stops event handling & keeps issue
* + `false` stops event handling & ignores issue
*/
public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool;
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Psalm\Plugin\EventHandler\Event;
use Psalm\Issue\CodeIssue;
class BeforeAddIssueEvent
{
/**
* @var CodeIssue
*/
private CodeIssue $issue;
/**
* @var bool
*/
private bool $fixable;
public function __construct(CodeIssue $issue, bool $fixable)
{
$this->issue = $issue;
$this->fixable = $fixable;
}
public function getIssue(): CodeIssue
{
return $this->issue;
}
public function isFixable(): bool
{
return $this->fixable;
}
}

View File

@ -7,8 +7,13 @@ use PhpParser\Node\Stmt\Class_;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Exception\UnpopulatedClasslikeException;
use Psalm\Issue\InvalidReturnStatement;
use Psalm\Issue\InvalidReturnType;
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
use Psalm\Plugin\EventHandler\BeforeAddIssueInterface;
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;
use Psalm\PluginRegistrationSocket;
use Psalm\Tests\Internal\Provider\ClassLikeStorageInstanceCacheProvider;
use Psalm\Type;
@ -207,4 +212,45 @@ class CodebaseTest extends TestCase
$this->codebase->classExtends('A', 'B');
}
/**
* @test
*/
public function addingCodeIssueIsIntercepted(): void
{
$this->addFile(
'somefile.php',
'<?php
namespace Psalm\CurrentTest;
function invalidReturnType(int $value): string
{
return $value;
}
echo invalidReturnType(123);
'
);
$eventHandler = new class implements BeforeAddIssueInterface
{
public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool
{
$issue = $event->getIssue();
if ($issue->code_location->file_path !== 'somefile.php') {
return null;
}
if ($issue instanceof InvalidReturnStatement && $event->isFixable() === false) {
return false;
} elseif ($issue instanceof InvalidReturnType && $event->isFixable() === true) {
return false;
}
return null;
}
};
(new PluginRegistrationSocket($this->codebase->config, $this->codebase))
->registerHooksFromClass(get_class($eventHandler));
$this->analyzeFile('somefile.php', new Context);
self::assertSame(0, IssueBuffer::getErrorCount());
}
}