1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

add plugin hook to be called after every function call

compared to AfterFunctionCallAnalysisInterface which gets only called
after a call to a function declared within the project, a plugin
implementing AfterEveryFunctionCallAnalysisInterface will get called for
every function call, including calls of PHP builtins.

On the other hand, this interface doesn't allow modification of the code
nor tweaking the return type, but it's still useful for accounting
purposes and for depreacting calls to PHP builtins

this fixes #2804
This commit is contained in:
Philip Hofstetter 2020-02-13 13:04:02 +01:00 committed by Matthew Brown
parent ae0b1a6acb
commit 395cf587d3
6 changed files with 117 additions and 2 deletions

View File

@ -16,8 +16,9 @@ class SomePlugin implements \Psalm\Plugin\Hook\AfterStatementAnalysisInterface
- `AfterClassLikeExistenceCheckInterface` - called after Psalm analyzes a reference to a class, interface or trait.
- `AfterClassLikeVisitInterface` - called after Psalm crawls the parsed Abstract Syntax Tree for a class-like (class, interface, trait). Due to caching the AST is crawled the first time Psalm sees the file, and is only re-crawled if the file changes, the cache is cleared, or you're disabling cache with `--no-cache`/`--no-reflection-cache`. Use this if you want to collect or modify information about a class before Psalm begins its analysis.
- `AfterCodebasePopulatedInterface` - called after Psalm has scanned necessary files and populated codebase data.
- `AfterEveryFunctionCallAnalysisInterface` - called after Psalm evaluates any function call. Cannot influence the call further.
- `AfterExpressionAnalysisInterface` - called after Psalm evaluates an expression.
- `AfterFunctionCallAnalysisInterface` - called after Psalm evaluates an function call.
- `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.
- `AfterMethodCallAnalysisInterface` - called after Psalm analyzes a method call.
- `AfterStatementAnalysisInterface` - called after Psalm evaluates an statement.
- `FunctionExistenceProviderInterface` - can be used to override Psalm's builtin function existence checks for one or more functions.

View File

@ -398,12 +398,28 @@ class Config
public $after_method_checks = [];
/**
* Static methods to be called after function checks have completed
* Static methods to be called after project function checks have completed
*
* Called after function calls to functions defined in the project.
*
* Allows influencing the return type and adding of modifications.
*
* @var class-string<Hook\AfterFunctionCallAnalysisInterface>[]
*/
public $after_function_checks = [];
/**
* Static methods to be called after every function call
*
* Called after each function call, including php internal functions.
*
* Cannot change the call or influence its return type
*
* @var class-string<Hook\AfterEveryFunctionCallAnalysisInterface>[]
*/
public $after_every_function_checks = [];
/**
* Static methods to be called after expression checks have completed
*

View File

@ -635,6 +635,18 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
if ($stmt_type) {
$statements_analyzer->node_data->setType($real_stmt, $stmt_type);
}
if ($config->after_every_function_checks) {
foreach ($config->after_every_function_checks as $plugin_fq_class_name) {
$plugin_fq_class_name::afterEveryFunctionCallAnalysis(
$stmt,
$function_id,
$context,
$statements_analyzer->getSource(),
$codebase
);
}
}
}
foreach ($defined_constants as $const_name => $const_type) {

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Plugin\Hook;
use PhpParser\Node\Expr\FuncCall;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\StatementsSource;
interface AfterEveryFunctionCallAnalysisInterface
{
public static function afterEveryFunctionCallAnalysis(
FuncCall $expr,
string $function_id,
Context $context,
StatementsSource $statements_source,
Codebase $codebase
): void;
}

View File

@ -46,6 +46,10 @@ class PluginRegistrationSocket implements RegistrationInterface
$this->config->after_function_checks[$handler] = $handler;
}
if (is_subclass_of($handler, Hook\AfterEveryFunctionCallAnalysisInterface::class)) {
$this->config->after_every_function_checks[$handler] = $handler;
}
if (is_subclass_of($handler, Hook\AfterExpressionAnalysisInterface::class)) {
$this->config->after_expression_checks[$handler] = $handler;
}

View File

@ -1,6 +1,10 @@
<?php
namespace Psalm\Tests\Config;
use PhpParser\Node\Expr\FuncCall;
use PHPUnit\Framework\MockObject\MockObject;
use Psalm\Plugin\Hook\AfterEveryFunctionCallAnalysisInterface;
use Psalm\StatementsSource;
use function define;
use function defined;
use const DIRECTORY_SEPARATOR;
@ -865,4 +869,64 @@ class PluginTest extends \Psalm\Tests\TestCase
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
}
public function testAfterEveryFunctionPluginIsCalledInAllCases(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm></psalm>'
)
);
$mock = $this->getMockBuilder(\stdClass::class)->setMethods(['check'])->getMock();
$mock->expects($this->exactly(3))
->method('check')
->withConsecutive(
[$this->equalTo('array_map')],
[$this->equalTo('fopen')],
[$this->equalTo('a')]
);
$plugin = new class($mock) implements AfterEveryFunctionCallAnalysisInterface {
/** @var MockObject */
private static $m;
public function __construct(MockObject $m)
{
self::$m = $m;
}
public static function afterEveryFunctionCallAnalysis(
FuncCall $expr,
string $function_id,
Context $context,
StatementsSource $statements_source,
Codebase $codebase
): void {
/** @psalm-suppress UndefinedInterfaceMethod */
self::$m->check($function_id);
}
};
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$this->project_analyzer->getCodebase()->config->after_every_function_checks[] = get_class($plugin);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
function a(): void {}
function b(int $e): int { return $e; }
array_map("b", [1,3,3]);
fopen("/tmp/foo.dat", "r");
a();
'
);
$this->analyzeFile($file_path, new Context());
}
}