Skip to content

Commit

Permalink
Merge pull request #7530 from ohader/issue-7528
Browse files Browse the repository at this point in the history
  • Loading branch information
weirdan committed Jan 31, 2022
2 parents b51cb75 + ffafccc commit 997592d
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/running_psalm/plugins/authoring_plugins.md
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions src/Psalm/Internal/EventDispatcher.php
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -39,6 +41,7 @@

use function array_merge;
use function count;
use function is_bool;
use function is_subclass_of;

/**
Expand Down Expand Up @@ -131,6 +134,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
*
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/Psalm/IssueBuffer.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
25 changes: 25 additions & 0 deletions src/Psalm/Plugin/EventHandler/BeforeAddIssueInterface.php
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin\EventHandler;

use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;

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
* + `true` stops event handling & keeps issue
* + `false` stops event handling & ignores issue
*/
public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool;
}
37 changes: 37 additions & 0 deletions src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin\EventHandler\Event;

use Psalm\Issue\CodeIssue;

final class BeforeAddIssueEvent
{
/**
* @var CodeIssue
*/
private CodeIssue $issue;

/**
* @var bool
*/
private bool $fixable;

/** @internal */
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;
}
}
46 changes: 46 additions & 0 deletions tests/CodebaseTest.php
Expand Up @@ -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;
Expand Down Expand Up @@ -207,4 +212,45 @@ public function classExtendsRejectsUnpopulatedClasslikes(): void

$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());
}
}

0 comments on commit 997592d

Please sign in to comment.