From e3602bbfe13d29a2b60ea8b48bd54ac3b742ef0c Mon Sep 17 00:00:00 2001 From: Oliver Hader Date: Mon, 1 Feb 2021 17:00:07 +0100 Subject: [PATCH 1/4] [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 d08197c4b7b..09ff7639b3b 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 29e8067490d..12c1731eea2 100644 --- a/src/Psalm/Internal/EventDispatcher.php +++ b/src/Psalm/Internal/EventDispatcher.php @@ -15,6 +15,7 @@ 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\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 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 @@ public function registerClass(string $class): void $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 @@ public function dispatchAfterCodebasePopulated(AfterCodebasePopulatedEvent $even } } + 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 8377ff11db2..a9fe2423c1a 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -16,6 +16,7 @@ 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 @@ public static function add(CodeIssue $e, bool $is_fixable = false): bool { $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 00000000000..70c941b711e --- /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 015d95af583..4083e836a0e 100644 --- a/tests/CodebaseTest.php +++ b/tests/CodebaseTest.php @@ -7,8 +7,13 @@ 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 @@ public function classExtendsRejectsUnpopulatedClasslikes(): void $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()); + } } From ff07a8d662e4e42344321c1d6b1a283217a497bd Mon Sep 17 00:00:00 2001 From: Oliver Hader Date: Sun, 30 Jan 2022 19:29:12 +0100 Subject: [PATCH 2/4] [TASK] Use final event class declarations --- src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php b/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php index 1452d35d9a7..cee265c4711 100644 --- a/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php @@ -6,7 +6,7 @@ use Psalm\Issue\CodeIssue; -class BeforeAddIssueEvent +final class BeforeAddIssueEvent { /** * @var CodeIssue From 995ecd0964474c04d1e0de59d2590f654f0d1c18 Mon Sep 17 00:00:00 2001 From: Oliver Hader Date: Mon, 31 Jan 2022 07:52:24 +0100 Subject: [PATCH 3/4] Update src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php Co-authored-by: Bruce Weirdan --- src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php b/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php index cee265c4711..74ea1d6bc6f 100644 --- a/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php @@ -18,6 +18,7 @@ final class BeforeAddIssueEvent */ private bool $fixable; + /** @internal */ public function __construct(CodeIssue $issue, bool $fixable) { $this->issue = $issue; From ffafccc2caf3383b58f11ba475a41e4d80020cd7 Mon Sep 17 00:00:00 2001 From: Oliver Hader Date: Mon, 31 Jan 2022 07:52:33 +0100 Subject: [PATCH 4/4] Update src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php Co-authored-by: Bruce Weirdan --- src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php b/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php index 70c941b711e..3942f9e5689 100644 --- a/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php +++ b/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php @@ -11,6 +11,10 @@ interface BeforeAddIssueInterface /** * Called before adding a code issue. * + * Note that event handlers are called in the order they were registered, and thus + * the handler registered earlier may prevent subsequent handlers from running by + * returning a boolean value. + * * @param BeforeAddIssueEvent $event * @return null|bool $event How and whether to continue: * + `null` continues with next event handler