From e3602bbfe13d29a2b60ea8b48bd54ac3b742ef0c Mon Sep 17 00:00:00 2001 From: Oliver Hader Date: Mon, 1 Feb 2021 17:00:07 +0100 Subject: [PATCH] [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 --- .../plugins/authoring_plugins.md | 1 + src/Psalm/Internal/EventDispatcher.php | 23 ++++++++++ src/Psalm/IssueBuffer.php | 6 +++ .../EventHandler/BeforeAddIssueInterface.php | 21 +++++++++ .../Event/BeforeAddIssueEvent.php | 36 +++++++++++++++ tests/CodebaseTest.php | 46 +++++++++++++++++++ 6 files changed, 133 insertions(+) create mode 100644 src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php create mode 100644 src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php diff --git a/docs/running_psalm/plugins/authoring_plugins.md b/docs/running_psalm/plugins/authoring_plugins.md index d08197c4b..09ff7639b 100644 --- a/docs/running_psalm/plugins/authoring_plugins.md +++ b/docs/running_psalm/plugins/authoring_plugins.md @@ -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. diff --git a/src/Psalm/Internal/EventDispatcher.php b/src/Psalm/Internal/EventDispatcher.php index 29e806749..12c1731ee 100644 --- a/src/Psalm/Internal/EventDispatcher.php +++ b/src/Psalm/Internal/EventDispatcher.php @@ -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> + */ + 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) { diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 8377ff11d..a9fe2423c 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -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); diff --git a/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php b/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php new file mode 100644 index 000000000..70c941b71 --- /dev/null +++ b/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php @@ -0,0 +1,21 @@ +issue = $issue; + $this->fixable = $fixable; + } + + public function getIssue(): CodeIssue + { + return $this->issue; + } + + public function isFixable(): bool + { + return $this->fixable; + } +} diff --git a/tests/CodebaseTest.php b/tests/CodebaseTest.php index 015d95af5..4083e836a 100644 --- a/tests/CodebaseTest.php +++ b/tests/CodebaseTest.php @@ -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', + '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()); + } }