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

Dynamic function storage provider #7471

Merged
merged 18 commits into from Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -92,6 +92,7 @@ class SomePlugin implements \Psalm\Plugin\EventHandler\AfterStatementAnalysisInt
- `PropertyExistenceProviderInterface` - can be used to override Psalm's builtin property existence checks for one or more classes.
- `PropertyTypeProviderInterface` - can be used to override Psalm's builtin property type lookup for one or more classes.
- `PropertyVisibilityProviderInterface` - can be used to override Psalm's builtin property visibility checks for one or more classes.
- `DynamicFunctionStorageProviderInterface` - can be used to override signature for one or more functions. It means you can define variadic param list. Infer return type by input args. Define function templates. Also check out `Psalm\Plugin\DynamicFunctionStorage` to find out what api it brings for you to change function's definition.

Here are a couple of example plugins:
- [StringChecker](https://github.com/vimeo/psalm/blob/master/examples/plugins/StringChecker.php) - checks class references in strings
Expand Down
Expand Up @@ -346,13 +346,25 @@ private static function getHighOrderFuncStorage(
$codebase = $statements_analyzer->getCodebase();

try {
if ($function_like_call instanceof PhpParser\Node\Expr\FuncCall) {
if ($function_like_call instanceof PhpParser\Node\Expr\FuncCall &&
!$function_like_call->isFirstClassCallable()
) {
$function_id = strtolower((string) $function_like_call->name->getAttribute('resolvedName'));

if (empty($function_id)) {
return null;
}

if ($codebase->functions->dynamic_storage_provider->has($function_id)) {
return $codebase->functions->dynamic_storage_provider->getFunctionStorage(
$function_like_call,
$statements_analyzer,
$function_id,
$context,
new CodeLocation($statements_analyzer, $function_like_call),
);
}

return $codebase->functions->getStorage($statements_analyzer, $function_id);
}

Expand Down
Expand Up @@ -526,9 +526,26 @@ private static function handleNamedFunction(
$function_call_info->defined_constants = [];
$function_call_info->global_variables = [];
$args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs();
$dynamic_function_storage = null;

if ($codebase->functions->dynamic_storage_provider->has($function_call_info->function_id)) {
$dynamic_function_storage = $codebase->functions->dynamic_storage_provider->getFunctionStorage(
$stmt,
$statements_analyzer,
$function_call_info->function_id,
$context,
$code_location
);
}

if ($function_call_info->function_exists) {
if (!$function_call_info->in_call_map || $function_call_info->is_stubbed) {
if ($dynamic_function_storage) {
$function_call_info->function_storage = $dynamic_function_storage;
$function_call_info->function_params = $dynamic_function_storage->params;
$function_call_info->allow_named_args = $dynamic_function_storage->allow_named_arg_calls;
$function_call_info->defined_constants = $dynamic_function_storage->defined_constants;
$function_call_info->global_variables = $dynamic_function_storage->global_variables;
} elseif (!$function_call_info->in_call_map || $function_call_info->is_stubbed) {
try {
$function_call_info->function_storage = $function_storage = $codebase_functions->getStorage(
$statements_analyzer,
Expand Down Expand Up @@ -562,15 +579,6 @@ private static function handleNamedFunction(
}

if ($codebase->functions->params_provider->has($function_call_info->function_id)) {
ArgumentsAnalyzer::analyze(
weirdan marked this conversation as resolved.
Show resolved Hide resolved
$statements_analyzer,
$stmt->getArgs(),
$function_call_info->function_params,
$function_call_info->function_id,
$function_call_info->allow_named_args,
$context
);

$function_call_info->function_params = $codebase->functions->params_provider->getFunctionParams(
$statements_analyzer,
$function_call_info->function_id,
Expand Down
5 changes: 5 additions & 0 deletions src/Psalm/Internal/Codebase/Functions.php
Expand Up @@ -8,6 +8,7 @@
use Psalm\Codebase;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Provider\DynamicFunctionStorageProvider;
use Psalm\Internal\Provider\FileStorageProvider;
use Psalm\Internal\Provider\FunctionExistenceProvider;
use Psalm\Internal\Provider\FunctionParamsProvider;
Expand Down Expand Up @@ -55,6 +56,9 @@ class Functions
/** @var FunctionParamsProvider */
public $params_provider;

/** @var DynamicFunctionStorageProvider */
public $dynamic_storage_provider;

/**
* @var Reflection
*/
Expand All @@ -67,6 +71,7 @@ public function __construct(FileStorageProvider $storage_provider, Reflection $r
$this->return_type_provider = new FunctionReturnTypeProvider();
$this->existence_provider = new FunctionExistenceProvider();
$this->params_provider = new FunctionParamsProvider();
$this->dynamic_storage_provider = new DynamicFunctionStorageProvider();

self::$stubbed_functions = [];
}
Expand Down
101 changes: 101 additions & 0 deletions src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Psalm\Internal\Provider;

use Closure;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\ArgTypeInferer;
use Psalm\Plugin\DynamicFunctionStorage;
use Psalm\Plugin\DynamicTemplateProvider;
use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface;
use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent;
use Psalm\Storage\FunctionStorage;

use function strtolower;

/**
* For each function call analysis will be created individual FunctionStorage in plugin hook.
* If it is created be aware, it shadows the FunctionStorage Psalm may generate during the scanning phase.
*
* @internal
*/
final class DynamicFunctionStorageProvider
{
/** @var array<lowercase-string, array<Closure(DynamicFunctionStorageProviderEvent): ?DynamicFunctionStorage>> */
private static $handlers = [];

/** @var array<lowercase-string, ?FunctionStorage> */
private static $dynamic_storages = [];

/**
* @param class-string<DynamicFunctionStorageProviderInterface> $class
*/
public function registerClass(string $class): void
{
$callable = Closure::fromCallable([$class, 'getFunctionStorage']);

foreach ($class::getFunctionIds() as $function_id) {
$this->registerClosure($function_id, $callable);
}
}

/**
* @param Closure(DynamicFunctionStorageProviderEvent): ?DynamicFunctionStorage $c
*/
public function registerClosure(string $fq_function_name, Closure $c): void
{
self::$handlers[strtolower($fq_function_name)][] = $c;
}

public function has(string $fq_function_name): bool
{
return isset(self::$handlers[strtolower($fq_function_name)]);
}

public function getFunctionStorage(
PhpParser\Node\Expr\FuncCall $stmt,
StatementsAnalyzer $statements_analyzer,
string $function_id,
Context $context,
CodeLocation $code_location
): ?FunctionStorage {
if ($stmt->isFirstClassCallable()) {
return null;
}

$dynamic_storage_id = strtolower($statements_analyzer->getFilePath())
. ':' . $stmt->getLine()
. ':' . (int)$stmt->getAttribute('startFilePos')
. ':dynamic-storage'
. ':-:' . strtolower($function_id);

if (isset(self::$dynamic_storages[$dynamic_storage_id])) {
return self::$dynamic_storages[$dynamic_storage_id];
}

foreach (self::$handlers[strtolower($function_id)] ?? [] as $class_handler) {
$event = new DynamicFunctionStorageProviderEvent(
new ArgTypeInferer($context, $statements_analyzer),
new DynamicTemplateProvider('fn-' . strtolower($function_id)),
$statements_analyzer,
$function_id,
$stmt,
$context,
$code_location,
);

$result = $class_handler($event);

return self::$dynamic_storages[$dynamic_storage_id] = $result
? $result->toFunctionStorage($function_id)
: null;
}

return null;
}
}
45 changes: 45 additions & 0 deletions src/Psalm/Plugin/ArgTypeInferer.php
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin;

use PhpParser;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Type;
use Psalm\Type\Union;

final class ArgTypeInferer
{
private Context $context;
private StatementsAnalyzer $statements_analyzer;

/**
* @internal
*/
public function __construct(Context $context, StatementsAnalyzer $statements_analyzer)
{
$this->context = $context;
$this->statements_analyzer = $statements_analyzer;
}

/**
* @return false|Union
*/
public function infer(PhpParser\Node\Arg $arg)
{
$already_inferred_type = $this->statements_analyzer->node_data->getType($arg->value);

if ($already_inferred_type) {
return $already_inferred_type;
}

if (ExpressionAnalyzer::analyze($this->statements_analyzer, $arg->value, $this->context) === false) {
klimick marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

return $this->statements_analyzer->node_data->getType($arg->value) ?? Type::getMixed();
}
}
76 changes: 76 additions & 0 deletions src/Psalm/Plugin/DynamicFunctionStorage.php
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin;

use Psalm\Storage\FunctionLikeParameter;
use Psalm\Storage\FunctionStorage;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;

final class DynamicFunctionStorage
{
/**
* Required param list for a function.
*
* @var list<FunctionLikeParameter>
*/
public array $params = [];

/**
* A function return type. Maybe null.
* That means we can infer it in {@see FunctionReturnTypeProviderInterface} hook.
*/
public ?Union $return_type = null;

/**
* A function can have template args or return type.
* Plugin hook must fill all used templates here.
*
* @var list<TTemplateParam>
*/
public array $templates = [];

/**
* Determines if a function can be called with named arguments.
*/
public bool $allow_named_arg_calls = true;

/**
* Function purity.
* If function is pure then plugin hook should set it to true.
*/
public bool $pure = false;

/**
* Determines if a function can be called with a various number of arguments.
*/
public bool $variadic = false;

/**
* @internal
*/
public function toFunctionStorage(string $function_cased_name): FunctionStorage
{
$storage = new FunctionStorage();
$storage->cased_name = $function_cased_name;
$storage->setParams($this->params);
$storage->return_type = $this->return_type;
$storage->allow_named_arg_calls = $this->allow_named_arg_calls;
$storage->pure = $this->pure;
$storage->variadic = $this->variadic;

if (!empty($this->templates)) {
$storage->template_types = [];

foreach ($this->templates as $template) {
$storage->template_types[$template->param_name] = [
$template->defining_class => $template->as
];
}
}

return $storage;
}
}
30 changes: 30 additions & 0 deletions src/Psalm/Plugin/DynamicTemplateProvider.php
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin;

use Psalm\Type;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;

final class DynamicTemplateProvider
{
private string $defining_class;

/**
* @internal
*/
public function __construct(string $defining_class)
{
$this->defining_class = $defining_class;
}

/**
* If {@see DynamicFunctionStorage} requires template params this method can create it.
*/
public function createTemplate(string $param_name, Union $as = null): TTemplateParam
{
return new TTemplateParam($param_name, $as ?? Type::getMixed(), $this->defining_class);
}
}
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Psalm\Plugin\EventHandler;

use Psalm\Plugin\DynamicFunctionStorage;
use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent;

interface DynamicFunctionStorageProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array;

public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage;
}