From 718c807b3301622b35e92874a0ec2299dc02d10d Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Fri, 28 Oct 2022 10:34:08 +0200 Subject: [PATCH] Better pipe support --- composer.json | 2 +- .../Fun/Pipe/PipeArgumentsProvider.php | 305 ++++-------------- 2 files changed, 63 insertions(+), 244 deletions(-) diff --git a/composer.json b/composer.json index f60727c..be89fe4 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "php": "^8.1", - "vimeo/psalm": "^4.20 || ^5.0" + "vimeo/psalm": "^5.0" }, "conflict": { "azjezz/psl": "<2.0" diff --git a/src/EventHandler/Fun/Pipe/PipeArgumentsProvider.php b/src/EventHandler/Fun/Pipe/PipeArgumentsProvider.php index 53b70b0..3856852 100644 --- a/src/EventHandler/Fun/Pipe/PipeArgumentsProvider.php +++ b/src/EventHandler/Fun/Pipe/PipeArgumentsProvider.php @@ -2,277 +2,96 @@ declare(strict_types=1); -namespace Psl\Psalm\EventHandler\Fun\Pipe; +namespace Psalm\Tests\Config\Plugin\Hook; -use Closure; -use PhpParser\Node\Arg; -use PhpParser\Node\ComplexType; -use PhpParser\Node\Expr; -use PhpParser\Node\FunctionLike; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt\Return_; -use PhpParser\NodeAbstract; -use Psalm\CodeLocation; -use Psalm\Issue\TooFewArguments; -use Psalm\Issue\TooManyArguments; -use Psalm\IssueBuffer; -use Psalm\Plugin\EventHandler\Event\FunctionParamsProviderEvent; -use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent; -use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface; -use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; -use Psalm\StatementsSource; +use Psalm\Plugin\DynamicFunctionStorage; +use Psalm\Plugin\DynamicTemplateProvider; +use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface; +use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; use Psalm\Storage\FunctionLikeParameter; -use Psalm\Type; +use Psalm\Type\Atomic\TCallable; +use Psalm\Type\Union; -/** - * @psalm-type Stage = array{0: Type\Union, 1: Type\Union, 2: string} - * @psalm-type StagesOrEmpty = list - * @psalm-type Stages = non-empty-list - */ -class PipeArgumentsProvider implements FunctionParamsProviderInterface, FunctionReturnTypeProviderInterface +use function array_map; +use function count; +use function range; + +final class PipeFunctionPlugin implements DynamicFunctionStorageProviderInterface { /** * @return array */ public static function getFunctionIds(): array { - return [ - 'psl\fun\pipe' - ]; + return ['psl\fun\pipe']; } - /** - * @return list|null - */ - public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array + public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage { - $stages = self::parseStages($event->getStatementsSource(), $event->getCallArgs()); - if (!$stages) { - return []; - } - - $params = []; - $previousOut = self::pipeInputType($stages); - - foreach ($stages as $stage) { - [$_, $currentOut, $paramName] = $stage; - - $params[] = self::createFunctionParameter( - 'stages', - self::createClosureStage($previousOut, $currentOut, $paramName) - ); - - $previousOut = $currentOut; - } - - return $params; - } - - public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union - { - $stages = self::parseStages($event->getStatementsSource(), $event->getCallArgs()); - if (!$stages) { - // - // @see https://github.com/vimeo/psalm/issues/7244 - // Currently, templated arguments are not being resolved in closures / callables - // For now, we fall back to the built-in types. - - // $templated = self::createTemplatedType('T', Type::getMixed(), 'fn-'.$event->getFunctionId()); - // return self::createClosureStage($templated, $templated, 'input'); + $template_provider = $event->getTemplateProvider(); + $callable_args_count = count($event->getArgs()) - 1; + if ($callable_args_count < 1) { return null; } - $in = self::pipeInputType($stages); - $out = self::pipeOutputType($stages); - - return self::createClosureStage($in, $out, 'input'); - } - - /** - * @param array $args - * - * @return StagesOrEmpty - */ - private static function parseStages(StatementsSource $source, array $args): array - { - $stages = []; - foreach ($args as $arg) { - $stage = $arg->value; - - if (!$stage instanceof FunctionLike) { - // The stage could also be an expression instead of a function-like. - // This plugin currently only supports function-like statements. - // All other input is considered to result in a mixed -> mixed stage - // This way we can still recover if types are known in later stages. - - // Expressions currently not covered: - - // New_ expression for invokables - // Variable for variables that can point to either FunctionLike or New_ - // Assignments during a pipe level: $x = fn () => 123 - // `x(...)` results in FuncCall(args: {0: VariadicPlaceholder}) - // ... - - // Haven't found a way to get the resulting type of an expression in psalm yet. - - $stages[] = [Type::getMixed(), Type::getMixed(), 'input']; - continue; - } - - $params = $stage->getParams(); - $paramName = self::parseNameFromParam($params[0] ?? null); - - $in = self::determineValidatedStageInputParam($source, $stage); - $out = self::parseTypeFromASTNode($source, $stage->getReturnType()); - - $stages[] = [$in, $out, $paramName]; - } - - return $stages; - } - - /** - * This function first validates the parameters of the stage. - * A stage should have exactly one required input parameter. - * - * - If there are no parameters, the input parameter is ignored. - * - If there are too many required parameters, this will result in a runtime exception. - * - * In both situations, we can continue building up the stages - * so that the user has as much analyzer info as possible. - */ - private static function determineValidatedStageInputParam(StatementsSource $source, FunctionLike $stage): Type\Union - { - $params = $stage->getParams(); - - if (count($params) === 0) { - IssueBuffer::maybeAdd( - new TooFewArguments( - 'Pipe stage functions require exactly one input parameter, none given. ' . - 'This will ignore the input value.', - new CodeLocation($source, $stage) - ), - $source->getSuppressedIssues() - ); - } + // All expected callables for pipe + $pipe_callables = array_map( + static fn($callable_offset) => self::createABCallable($callable_offset, $template_provider), + range(1, $callable_args_count) + ); - // The pipe function will crash during runtime when there are more than 1 function parameters required. - // We can still determine the stages Input / Output types at this point. - if (count($params) > 1 && !($params[1] ?? null)?->default) { - IssueBuffer::maybeAdd( - new TooManyArguments( - 'Pipe stage functions can only deal with one input parameter.', - new CodeLocation($source, $params[1]) + $pipe_storage = new DynamicFunctionStorage(); + + $pipe_storage->params = [ + // First pipe param + self::createParam( + 'pipe_input', + new Union([$template_provider->createTemplate('T1')]) + ), + // Rest pipe callable params + ...array_map( + static fn(TCallable $callable, int $offset) => self::createParam( + 'fn_' . $offset, + new Union([$callable]), ), - $source->getSuppressedIssues() - ); - } - - $type = $params ? $params[0]->type : null; - - return self::parseTypeFromASTNode($source, $type); - } - - /** - * This function tries parsing the node type based on psalm's NodeTypeProvider. - * If that one is not able to determine the type, this function will fall back on parsing the AST's node type. - * In case we are not able to determine the type, this function falls back to the $default type. - */ - private static function parseTypeFromASTNode( - StatementsSource $source, - ?NodeAbstract $node, - string $default = 'mixed' - ): Type\Union { - if (!$node || $node instanceof ComplexType) { - return self::createSimpleType($default); - } - - $nodeType = null; - if ($node instanceof Expr || $node instanceof Name || $node instanceof Return_) { - $nodeTypeProvider = $source->getNodeTypeProvider(); - $nodeType = $nodeTypeProvider->getType($node); - } - - if (!$nodeType && ($node instanceof Name || $node instanceof Identifier)) { - $nodeType = self::createSimpleType($node->toString() ?: $default); - } - - return $nodeType ?? self::createSimpleType($default); - } - - private static function parseNameFromParam(?Param $param, string $default = 'input'): string - { - if (!$param) { - return $default; - } - - $var = $param->var; - if (!$var instanceof Expr\Variable) { - return $default; - } + $pipe_callables, + array_keys($pipe_callables) + ) + ]; - return is_string($var->name) ? $var->name : $default; - } + // Pipe return type from last callable + $pipe_storage->return_type = $pipe_callables[array_key_last($pipe_callables)]->return_type; - /** - * @param Stages $stages - */ - private static function pipeInputType(array $stages): Type\Union - { - $firstStage = array_shift($stages); - [$in, $_, $_] = $firstStage; + // Pipe template list for each callable + $pipe_storage->templates = array_map( + static fn($offset) => $template_provider->createTemplate('T' . $offset), + range(1, $callable_args_count + 1), + ); - return $in; + return $pipe_storage; } - /** - * @param Stages $stages - */ - private static function pipeOutputType(array $stages): Type\Union - { - $lastStage = array_pop($stages); - [$_, $out, $_] = $lastStage; - - return $out; - } + private static function createABCallable( + int $callable_offset, + DynamicTemplateProvider $template_provider + ): TCallable { + $a = self::createParam( + 'input', + new Union([ + $template_provider->createTemplate('T' . $callable_offset), + ]) + ); - private static function createClosureStage(Type\Union $in, Type\Union $out, string $paramName): Type\Union - { - return new Type\Union([ - new Type\Atomic\TClosure( - value: Closure::class, - params: [ - self::createFunctionParameter($paramName, $in), - ], - return_type: $out, - ) + $b = new Union([ + $template_provider->createTemplate('T' . ($callable_offset + 1)), ]); - } - - private static function createFunctionParameter(string $name, Type\Union $type): FunctionLikeParameter - { - return new FunctionLikeParameter( - $name, - false, - $type, - is_optional: false, - is_nullable: false, - is_variadic: false, - ); - } - private static function createSimpleType(string $type): Type\Union - { - return new Type\Union([Type\Atomic::create($type)]); + return new TCallable('callable', [$a], $b); } - private static function createTemplatedType(string $name, Type\Union $baseType, string $definingClass): Type\Union + private static function createParam(string $name, Union $type): FunctionLikeParameter { - return new Type\Union([ - new Type\Atomic\TTemplateParam($name, $baseType, $definingClass) - ]); + return new FunctionLikeParameter($name, false, $type); } }