diff --git a/docs/running_psalm/plugins/authoring_plugins.md b/docs/running_psalm/plugins/authoring_plugins.md index 9a54ff91d1a..2e3043de74a 100644 --- a/docs/running_psalm/plugins/authoring_plugins.md +++ b/docs/running_psalm/plugins/authoring_plugins.md @@ -82,6 +82,7 @@ class SomePlugin implements \Psalm\Plugin\EventHandler\AfterStatementAnalysisInt - `AfterMethodCallAnalysisInterface` - called after Psalm analyzes a method call. - `BeforeStatementAnalysisInterface` - called before Psalm evaluates an statement. - `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 d1845f45066..acf8378f7bc 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\BeforeStatementAnalysisInterface; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; @@ -30,6 +31,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\BeforeStatementAnalysisEvent; use Psalm\Plugin\EventHandler\Event\StringInterpreterEvent; @@ -39,6 +41,7 @@ use function array_merge; use function count; +use function is_bool; use function is_subclass_of; /** @@ -131,6 +134,11 @@ class EventDispatcher */ public $after_codebase_populated = []; + /** + * @var list> + */ + private array $before_add_issue = []; + /** * Static methods to be called after codebase has been populated * @@ -222,6 +230,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; } @@ -353,6 +365,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..3942f9e5689 --- /dev/null +++ b/src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php @@ -0,0 +1,25 @@ +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()); + } }