Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Introduce BeforeStatementAnalysisEvent #7535

Merged
merged 2 commits into from Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/running_psalm/plugins/authoring_plugins.md
Expand Up @@ -80,6 +80,7 @@ class SomePlugin implements \Psalm\Plugin\EventHandler\AfterStatementAnalysisInt
- `AfterFunctionCallAnalysisInterface` - called after Psalm evaluates a function call to any function defined within the project itself. Can alter the return type or perform modifications of the call.
- `AfterFunctionLikeAnalysisInterface` - called after Psalm has completed its analysis of a given function-like.
- `AfterMethodCallAnalysisInterface` - called after Psalm analyzes a method call.
- `BeforeStatementAnalysisInterface` - called before Psalm evaluates an statement.
- `AfterStatementAnalysisInterface` - called after Psalm evaluates an statement.
- `BeforeFileAnalysisInterface` - called before Psalm analyzes a file.
- `FunctionExistenceProviderInterface` - can be used to override Psalm's builtin function existence checks for one or more functions.
Expand Down
74 changes: 58 additions & 16 deletions src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
Expand Up @@ -56,6 +56,7 @@
use Psalm\IssueBuffer;
use Psalm\NodeTypeProvider;
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;
use Psalm\Type;
use UnexpectedValueException;

Expand Down Expand Up @@ -353,6 +354,10 @@ private static function analyzeStatement(
Context $context,
?Context $global_context
): ?bool {
if (self::dispatchBeforeStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}

$ignore_variable_property = false;
$ignore_variable_method = false;

Expand Down Expand Up @@ -619,25 +624,10 @@ private static function analyzeStatement(
}
}

$codebase = $statements_analyzer->getCodebase();

$event = new AfterStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[]
);

if ($codebase->config->eventDispatcher->dispatchAfterStatementAnalysis($event) === false) {
if (self::dispatchAfterStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}

$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}

if ($new_issues) {
$statements_analyzer->removeSuppressedIssues($new_issues);
}
Expand Down Expand Up @@ -673,6 +663,58 @@ private static function analyzeStatement(
return null;
}

private static function dispatchAfterStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();

$event = new AfterStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[]
);

if ($codebase->config->eventDispatcher->dispatchAfterStatementAnalysis($event) === false) {
return false;
}

$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
return null;
}

private static function dispatchBeforeStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();

$event = new BeforeStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[]
);

if ($codebase->config->eventDispatcher->dispatchBeforeStatementAnalysis($event) === false) {
return false;
}

$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
return null;
}

private function parseStatementDocblock(
PhpParser\Comment\Doc $docblock,
PhpParser\Node\Stmt $stmt,
Expand Down
23 changes: 23 additions & 0 deletions src/Psalm/Internal/EventDispatcher.php
Expand Up @@ -16,6 +16,7 @@
use Psalm\Plugin\EventHandler\AfterMethodCallAnalysisInterface;
use Psalm\Plugin\EventHandler\AfterStatementAnalysisInterface;
use Psalm\Plugin\EventHandler\BeforeFileAnalysisInterface;
use Psalm\Plugin\EventHandler\BeforeStatementAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\AfterClassLikeAnalysisEvent;
Expand All @@ -30,6 +31,7 @@
use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeFileAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\StringInterpreterEvent;
use Psalm\Plugin\EventHandler\RemoveTaintsInterface;
use Psalm\Plugin\EventHandler\StringInterpreterInterface;
Expand Down Expand Up @@ -80,6 +82,13 @@ class EventDispatcher
*/
public $after_expression_checks = [];

/**
* Static methods to be called before statement checks are processed
*
* @var list<class-string<BeforeStatementAnalysisInterface>>
*/
public $before_statement_checks = [];

/**
* Static methods to be called after statement checks have completed
*
Expand Down Expand Up @@ -185,6 +194,10 @@ public function registerClass(string $class): void
$this->after_expression_checks[] = $class;
}

if (is_subclass_of($class, BeforeStatementAnalysisInterface::class)) {
$this->before_statement_checks[] = $class;
}

if (is_subclass_of($class, AfterStatementAnalysisInterface::class)) {
$this->after_statement_checks[] = $class;
}
Expand Down Expand Up @@ -271,6 +284,16 @@ public function dispatchAfterExpressionAnalysis(AfterExpressionAnalysisEvent $ev
return null;
}

public function dispatchBeforeStatementAnalysis(BeforeStatementAnalysisEvent $event): ?bool
{
foreach ($this->before_statement_checks as $handler) {
if ($handler::beforeStatementAnalysis($event) === false) {
return false;
}
}
return null;
}

public function dispatchAfterStatementAnalysis(AfterStatementAnalysisEvent $event): ?bool
{
foreach ($this->after_statement_checks as $handler) {
Expand Down
19 changes: 19 additions & 0 deletions src/Psalm/Plugin/EventHandler/BeforeStatementAnalysisInterface.php
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin\EventHandler;

use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;

interface BeforeStatementAnalysisInterface
{
/**
* Called before a statement has been checked
*
* @return null|false Whether to continue
* + `null` continues with next event handler
* + `false` stops analyzing current statement in StatementsAnalyzer
*/
public static function beforeStatementAnalysis(BeforeStatementAnalysisEvent $event): ?bool;
}
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin\EventHandler\Event;

use PhpParser\Node\Stmt;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\FileManipulation;
use Psalm\StatementsSource;

final class BeforeStatementAnalysisEvent
{
private Stmt $stmt;
private Context $context;
private StatementsSource $statements_source;
private Codebase $codebase;
/**
* @var list<FileManipulation>
*/
private array $file_replacements;

/**
* Called after a statement has been checked
*
* @param list<FileManipulation> $file_replacements
*/
public function __construct(
ohader marked this conversation as resolved.
Show resolved Hide resolved
Stmt $stmt,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array $file_replacements = []
) {
$this->stmt = $stmt;
$this->context = $context;
$this->statements_source = $statements_source;
$this->codebase = $codebase;
$this->file_replacements = $file_replacements;
}

public function getStmt(): Stmt
{
return $this->stmt;
}

public function setStmt(Stmt $stmt): void
{
$this->stmt = $stmt;
}

public function getContext(): Context
{
return $this->context;
}

public function getStatementsSource(): StatementsSource
{
return $this->statements_source;
}

public function getCodebase(): Codebase
{
return $this->codebase;
}

/**
* @return list<FileManipulation>
*/
public function getFileReplacements(): array
{
return $this->file_replacements;
}

/**
* @param list<FileManipulation> $file_replacements
*/
public function setFileReplacements(array $file_replacements): void
{
$this->file_replacements = $file_replacements;
}
}