diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index e05fb8cb4..04ed7e16d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -53,6 +53,7 @@ use Psalm\Issue\UnrecognizedExpression; use Psalm\Issue\UnsupportedReferenceUsage; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent; +use Psalm\Plugin\EventHandler\Event\BeforeExpressionAnalysisEvent; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; use Psalm\Type\TaintKind; @@ -80,6 +81,10 @@ class ExpressionAnalyzer ?TemplateResult $template_result = null, bool $assigned_to_reference = false ): bool { + if (self::dispatchBeforeExpressionAnalysis($stmt, $context, $statements_analyzer) === false) { + return false; + } + $codebase = $statements_analyzer->getCodebase(); if (self::handleExpression( @@ -126,24 +131,10 @@ class ExpressionAnalyzer } } - $event = new AfterExpressionAnalysisEvent( - $stmt, - $context, - $statements_analyzer, - $codebase, - [], - ); - - if ($codebase->config->eventDispatcher->dispatchAfterExpressionAnalysis($event) === false) { + if (self::dispatchAfterExpressionAnalysis($stmt, $context, $statements_analyzer) === false) { return false; } - $file_manipulations = $event->getFileReplacements(); - - if ($file_manipulations) { - FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations); - } - return true; } @@ -554,4 +545,60 @@ class ExpressionAnalyzer return true; } + + private static function dispatchBeforeExpressionAnalysis( + PhpParser\Node\Expr $expr, + Context $context, + StatementsAnalyzer $statements_analyzer + ): ?bool { + $codebase = $statements_analyzer->getCodebase(); + + $event = new BeforeExpressionAnalysisEvent( + $expr, + $context, + $statements_analyzer, + $codebase, + [], + ); + + if ($codebase->config->eventDispatcher->dispatchBeforeExpressionAnalysis($event) === false) { + return false; + } + + $file_manipulations = $event->getFileReplacements(); + + if (!empty($file_manipulations)) { + FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations); + } + + return null; + } + + private static function dispatchAfterExpressionAnalysis( + PhpParser\Node\Expr $expr, + Context $context, + StatementsAnalyzer $statements_analyzer + ): ?bool { + $codebase = $statements_analyzer->getCodebase(); + + $event = new AfterExpressionAnalysisEvent( + $expr, + $context, + $statements_analyzer, + $codebase, + [], + ); + + if ($codebase->config->eventDispatcher->dispatchAfterExpressionAnalysis($event) === false) { + return false; + } + + $file_manipulations = $event->getFileReplacements(); + + if (!empty($file_manipulations)) { + FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations); + } + + return null; + } } diff --git a/src/Psalm/Internal/EventDispatcher.php b/src/Psalm/Internal/EventDispatcher.php index b42c7b3da..fb574949e 100644 --- a/src/Psalm/Internal/EventDispatcher.php +++ b/src/Psalm/Internal/EventDispatcher.php @@ -16,6 +16,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\BeforeExpressionAnalysisInterface; use Psalm\Plugin\EventHandler\BeforeFileAnalysisInterface; use Psalm\Plugin\EventHandler\BeforeStatementAnalysisInterface; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; @@ -32,6 +33,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\BeforeExpressionAnalysisEvent; use Psalm\Plugin\EventHandler\Event\BeforeFileAnalysisEvent; use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent; use Psalm\Plugin\EventHandler\Event\StringInterpreterEvent; @@ -77,6 +79,13 @@ class EventDispatcher */ public array $after_every_function_checks = []; + /** + * Static methods to be called before expression checks are completed + * + * @var list> + */ + public array $before_expression_checks = []; + /** * Static methods to be called after expression checks have completed * @@ -197,6 +206,10 @@ class EventDispatcher $this->after_every_function_checks[] = $class; } + if (is_subclass_of($class, BeforeExpressionAnalysisInterface::class)) { + $this->before_expression_checks[] = $class; + } + if (is_subclass_of($class, AfterExpressionAnalysisInterface::class)) { $this->after_expression_checks[] = $class; } @@ -284,6 +297,17 @@ class EventDispatcher } } + public function dispatchBeforeExpressionAnalysis(BeforeExpressionAnalysisEvent $event): ?bool + { + foreach ($this->before_expression_checks as $handler) { + if ($handler::beforeExpressionAnalysis($event) === false) { + return false; + } + } + + return null; + } + public function dispatchAfterExpressionAnalysis(AfterExpressionAnalysisEvent $event): ?bool { foreach ($this->after_expression_checks as $handler) { diff --git a/src/Psalm/Plugin/EventHandler/BeforeExpressionAnalysisInterface.php b/src/Psalm/Plugin/EventHandler/BeforeExpressionAnalysisInterface.php new file mode 100644 index 000000000..60d9f47ae --- /dev/null +++ b/src/Psalm/Plugin/EventHandler/BeforeExpressionAnalysisInterface.php @@ -0,0 +1,15 @@ + + */ + private array $file_replacements; + + /** + * Called before an expression is checked + * + * @param list $file_replacements + * @internal + */ + public function __construct( + Expr $expr, + Context $context, + StatementsSource $statements_source, + Codebase $codebase, + array $file_replacements = [] + ) { + $this->expr = $expr; + $this->context = $context; + $this->statements_source = $statements_source; + $this->codebase = $codebase; + $this->file_replacements = $file_replacements; + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getStatementsSource(): StatementsSource + { + return $this->statements_source; + } + + public function getCodebase(): Codebase + { + return $this->codebase; + } + + /** + * @return list + */ + public function getFileReplacements(): array + { + return $this->file_replacements; + } + + /** + * @param list $file_replacements + */ + public function setFileReplacements(array $file_replacements): void + { + $this->file_replacements = $file_replacements; + } +}