diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 653ffedc9ac..f13912aa8d5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -20,6 +20,7 @@ use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Stubs\Generator\StubsGenerator; +use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; @@ -196,16 +197,18 @@ public static function analyze( } $high_order_template_result = null; - $high_order_callable_info = $param - ? HighOrderFunctionArgHandler::getCallableArgInfo($context, $arg->value, $statements_analyzer, $param) - : null; - if ($param && $high_order_callable_info) { - $high_order_template_result = HighOrderFunctionArgHandler::remapLowerBounds( + if (($arg->value instanceof PhpParser\Node\Expr\FuncCall + || $arg->value instanceof PhpParser\Node\Expr\MethodCall + || $arg->value instanceof PhpParser\Node\Expr\StaticCall) + && $param + && $function_storage = self::getHighOrderFuncStorage($context, $statements_analyzer, $arg->value) + ) { + $high_order_template_result = self::handleHighOrderFuncCallArg( $statements_analyzer, $template_result ?? new TemplateResult([], []), - $high_order_callable_info, - $param->type ?? Type::getMixed(), + $function_storage, + $param, ); } elseif (($arg->value instanceof PhpParser\Node\Expr\Closure || $arg->value instanceof PhpParser\Node\Expr\ArrowFunction) @@ -243,16 +246,6 @@ public static function analyze( $context->inside_call = $was_inside_call; - if ($high_order_callable_info && $high_order_template_result) { - HighOrderFunctionArgHandler::enhanceCallableArgType( - $context, - $arg->value, - $statements_analyzer, - $high_order_callable_info, - $high_order_template_result, - ); - } - if (($argument_offset === 0 && $method_id === 'array_filter' && count($args) === 2) || ($argument_offset > 0 && $method_id === 'array_map' && count($args) >= 2) ) { @@ -306,6 +299,184 @@ public static function analyze( return null; } + private static function getHighOrderFuncStorage( + Context $context, + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Expr\CallLike $function_like_call + ): ?FunctionLikeStorage { + $codebase = $statements_analyzer->getCodebase(); + + try { + 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); + } + + if ($function_like_call instanceof PhpParser\Node\Expr\MethodCall && + $function_like_call->var instanceof PhpParser\Node\Expr\Variable && + $function_like_call->name instanceof PhpParser\Node\Identifier && + is_string($function_like_call->var->name) && + isset($context->vars_in_scope['$' . $function_like_call->var->name]) + ) { + $lhs_type = $context->vars_in_scope['$' . $function_like_call->var->name]->getSingleAtomic(); + + if (!$lhs_type instanceof Type\Atomic\TNamedObject) { + return null; + } + + $method_id = new MethodIdentifier( + $lhs_type->value, + strtolower((string)$function_like_call->name), + ); + + return $codebase->methods->getStorage($method_id); + } + + if ($function_like_call instanceof PhpParser\Node\Expr\StaticCall && + $function_like_call->name instanceof PhpParser\Node\Identifier + ) { + $method_id = new MethodIdentifier( + (string)$function_like_call->class->getAttribute('resolvedName'), + strtolower($function_like_call->name->name), + ); + + return $codebase->methods->getStorage($method_id); + } + } catch (UnexpectedValueException $e) { + return null; + } + + return null; + } + + /** + * Compiles TemplateResult for high-order functions ($func_call) + * by previous template args ($inferred_template_result). + * + * It's need for proper template replacement: + * + * ``` + * * template T + * * return Closure(T): T + * function id(): Closure { ... } + * + * * template A + * * template B + * * + * * param list $_items + * * param callable(A): B $_ab + * * return list + * function map(array $items, callable $ab): array { ... } + * + * // list + * $numbers = [1, 2, 3]; + * + * $result = map($numbers, id()); + * // $result is list because template T of id() was inferred by previous arg. + * ``` + */ + private static function handleHighOrderFuncCallArg( + StatementsAnalyzer $statements_analyzer, + TemplateResult $inferred_template_result, + FunctionLikeStorage $storage, + FunctionLikeParameter $actual_func_param + ): ?TemplateResult { + $codebase = $statements_analyzer->getCodebase(); + + $input_hof_atomic = $storage->return_type && $storage->return_type->isSingle() + ? $storage->return_type->getSingleAtomic() + : null; + + // Try upcast invokable to callable type. + if ($input_hof_atomic instanceof Type\Atomic\TNamedObject && + $input_hof_atomic->value !== 'Closure' && + $codebase->classExists($input_hof_atomic->value) + ) { + $callable_from_invokable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $input_hof_atomic, + ); + + if ($callable_from_invokable) { + $invoke_id = new MethodIdentifier($input_hof_atomic->value, '__invoke'); + $declaring_invoke_id = $codebase->methods->getDeclaringMethodId($invoke_id); + + $storage = $codebase->methods->getStorage($declaring_invoke_id ?? $invoke_id); + $input_hof_atomic = $callable_from_invokable; + } + } + + if (!$input_hof_atomic instanceof TClosure && !$input_hof_atomic instanceof TCallable) { + return null; + } + + $container_hof_atomic = $actual_func_param->type && $actual_func_param->type->isSingle() + ? $actual_func_param->type->getSingleAtomic() + : null; + + if (!$container_hof_atomic instanceof TClosure && !$container_hof_atomic instanceof TCallable) { + return null; + } + + $replaced_container_hof_atomic = new Union([$container_hof_atomic]); + + // Replaces all input args in container function. + // + // For example: + // The map function expects callable(A):B as second param + // We know that previous arg type is list where the int is the A template. + // Then we can replace callable(A): B to callable(int):B using $inferred_template_result. + $replaced_container_hof_atomic = TemplateInferredTypeReplacer::replace( + $replaced_container_hof_atomic, + $inferred_template_result, + $codebase, + ); + + /** @var TClosure|TCallable $container_hof_atomic */ + $container_hof_atomic = $replaced_container_hof_atomic->getSingleAtomic(); + $high_order_template_result = new TemplateResult($storage->template_types ?: [], []); + + // We can replace each templated param for the input function. + // Example: + // map($numbers, id()); + // We know that map expects callable(int):B because the $numbers is list. + // We know that id() returns callable(T):T. + // Then we can replace templated params sequentially using the expected callable(int):B. + foreach ($input_hof_atomic->params ?? [] as $offset => $actual_func_param) { + if ($actual_func_param->type && + $actual_func_param->type->getTemplateTypes() && + isset($container_hof_atomic->params[$offset]) + ) { + TemplateStandinTypeReplacer::fillTemplateResult( + $actual_func_param->type, + $high_order_template_result, + $codebase, + null, + $container_hof_atomic->params[$offset]->type, + ); + } + } + + return $high_order_template_result; + } + private static function handleArrayMapFilterArrayArg( StatementsAnalyzer $statements_analyzer, string $method_id, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php deleted file mode 100644 index 3d1a51c4e67..00000000000 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php +++ /dev/null @@ -1,325 +0,0 @@ - $_items - * * param callable(A): B $_ab - * * return list - * function map(array $items, callable $ab): array { ... } - * - * // list - * $numbers = [1, 2, 3]; - * - * $result = map($numbers, id()); - * // $result is list because template T of id() was inferred by previous arg. - * ``` - */ - public static function remapLowerBounds( - StatementsAnalyzer $statements_analyzer, - TemplateResult $inferred_template_result, - HighOrderFunctionArgInfo $input_function, - Union $container_function_type - ): TemplateResult { - // Try to infer container callable by $inferred_template_result - $container_type = TemplateInferredTypeReplacer::replace( - $container_function_type, - $inferred_template_result, - $statements_analyzer->getCodebase(), - ); - - $input_function_type = $input_function->getFunctionType(); - $input_function_template_result = $input_function->getTemplates(); - - // Traverse side by side 'container' params and 'input' params. - // This maps 'input' templates to 'container' templates. - // - // Example: - // 'input' => Closure(C:Bar, D:Bar): array{C:Bar, D:Bar} - // 'container' => Closure(int, string): array{int, string} - // - // $remapped_lower_bounds will be: [ - // 'C' => ['Bar' => [int]], - // 'D' => ['Bar' => [string]] - // ]. - foreach ($input_function_type->getAtomicTypes() as $input_atomic) { - if (!$input_atomic instanceof TClosure && !$input_atomic instanceof TCallable) { - continue; - } - - foreach ($container_type->getAtomicTypes() as $container_atomic) { - if (!$container_atomic instanceof TClosure && !$container_atomic instanceof TCallable) { - continue; - } - - foreach ($input_atomic->params ?? [] as $offset => $input_param) { - if (!isset($container_atomic->params[$offset])) { - continue; - } - - TemplateStandinTypeReplacer::fillTemplateResult( - $input_param->type ?? Type::getMixed(), - $input_function_template_result, - $statements_analyzer->getCodebase(), - $statements_analyzer, - $container_atomic->params[$offset]->type, - ); - } - } - } - - return $input_function_template_result; - } - - public static function enhanceCallableArgType( - Context $context, - PhpParser\Node\Expr $arg_expr, - StatementsAnalyzer $statements_analyzer, - HighOrderFunctionArgInfo $high_order_callable_info, - TemplateResult $high_order_template_result - ): void { - // Psalm can infer simple callable/closure. - // But can't infer first-class-callable or high-order function. - if ($high_order_callable_info->getType() === HighOrderFunctionArgInfo::TYPE_CALLABLE) { - return; - } - - $fully_inferred_callable_type = TemplateInferredTypeReplacer::replace( - $high_order_callable_info->getFunctionType(), - $high_order_template_result, - $statements_analyzer->getCodebase(), - ); - - // Some templates may not have been replaced. - // They expansion makes error message better. - $expanded = TypeExpander::expandUnion( - $statements_analyzer->getCodebase(), - $fully_inferred_callable_type, - $context->self, - $context->self, - $context->parent, - true, - true, - false, - false, - true, - ); - - $statements_analyzer->node_data->setType($arg_expr, $expanded); - } - - public static function getCallableArgInfo( - Context $context, - PhpParser\Node\Expr $input_arg_expr, - StatementsAnalyzer $statements_analyzer, - FunctionLikeParameter $container_param - ): ?HighOrderFunctionArgInfo { - if (!self::isSupported($container_param)) { - return null; - } - - $codebase = $statements_analyzer->getCodebase(); - - try { - if ($input_arg_expr instanceof PhpParser\Node\Expr\FuncCall) { - $function_id = strtolower((string) $input_arg_expr->name->getAttribute('resolvedName')); - - if (empty($function_id)) { - return null; - } - - $dynamic_storage = !$input_arg_expr->isFirstClassCallable() - ? $codebase->functions->dynamic_storage_provider->getFunctionStorage( - $input_arg_expr, - $statements_analyzer, - $function_id, - $context, - new CodeLocation($statements_analyzer, $input_arg_expr), - ) - : null; - - return new HighOrderFunctionArgInfo( - $input_arg_expr->isFirstClassCallable() - ? HighOrderFunctionArgInfo::TYPE_FIRST_CLASS_CALLABLE - : HighOrderFunctionArgInfo::TYPE_CALLABLE, - $dynamic_storage ?? $codebase->functions->getStorage($statements_analyzer, $function_id), - ); - } - - if ($input_arg_expr instanceof PhpParser\Node\Expr\MethodCall && - $input_arg_expr->var instanceof PhpParser\Node\Expr\Variable && - $input_arg_expr->name instanceof PhpParser\Node\Identifier && - is_string($input_arg_expr->var->name) && - isset($context->vars_in_scope['$' . $input_arg_expr->var->name]) - ) { - $lhs_type = $context->vars_in_scope['$' . $input_arg_expr->var->name]->getSingleAtomic(); - - if (!$lhs_type instanceof Type\Atomic\TNamedObject) { - return null; - } - - $method_id = new MethodIdentifier( - $lhs_type->value, - strtolower((string)$input_arg_expr->name), - ); - - return new HighOrderFunctionArgInfo( - $input_arg_expr->isFirstClassCallable() - ? HighOrderFunctionArgInfo::TYPE_FIRST_CLASS_CALLABLE - : HighOrderFunctionArgInfo::TYPE_CALLABLE, - $codebase->methods->getStorage($method_id), - ); - } - - if ($input_arg_expr instanceof PhpParser\Node\Expr\StaticCall && - $input_arg_expr->name instanceof PhpParser\Node\Identifier - ) { - $method_id = new MethodIdentifier( - (string)$input_arg_expr->class->getAttribute('resolvedName'), - strtolower($input_arg_expr->name->toString()), - ); - - return new HighOrderFunctionArgInfo( - $input_arg_expr->isFirstClassCallable() - ? HighOrderFunctionArgInfo::TYPE_FIRST_CLASS_CALLABLE - : HighOrderFunctionArgInfo::TYPE_CALLABLE, - $codebase->methods->getStorage($method_id), - ); - } - - if ($input_arg_expr instanceof PhpParser\Node\Scalar\String_) { - return self::fromLiteralString(Type::getString($input_arg_expr->value), $statements_analyzer); - } - - if ($input_arg_expr instanceof PhpParser\Node\Expr\ConstFetch) { - $constant = $context->constants[$input_arg_expr->name->toString()] ?? null; - - return null !== $constant - ? self::fromLiteralString($constant, $statements_analyzer) - : null; - } - - if ($input_arg_expr instanceof PhpParser\Node\Expr\ClassConstFetch && - $input_arg_expr->name instanceof PhpParser\Node\Identifier - ) { - $storage = $codebase->classlikes - ->getStorageFor((string)$input_arg_expr->class->getAttribute('resolvedName')); - - $constant = null !== $storage - ? $storage->constants[$input_arg_expr->name->toString()] ?? null - : null; - - return null !== $constant && null !== $constant->type - ? self::fromLiteralString($constant->type, $statements_analyzer) - : null; - } - - if ($input_arg_expr instanceof PhpParser\Node\Expr\New_ && - $input_arg_expr->class instanceof PhpParser\Node\Name - ) { - $class_storage = $codebase->classlikes - ->getStorageFor((string) $input_arg_expr->class->getAttribute('resolvedName')); - - $invoke_storage = $class_storage && isset($class_storage->methods['__invoke']) - ? $class_storage->methods['__invoke'] - : null; - - if (!$invoke_storage) { - return null; - } - - return new HighOrderFunctionArgInfo( - HighOrderFunctionArgInfo::TYPE_CLASS_CALLABLE, - $invoke_storage, - $class_storage, - ); - } - } catch (UnexpectedValueException $e) { - return null; - } - - return null; - } - - private static function isSupported(FunctionLikeParameter $container_param): bool - { - if (!$container_param->type || !$container_param->type->hasCallableType()) { - return false; - } - - foreach ($container_param->type->getAtomicTypes() as $a) { - if (($a instanceof TClosure || $a instanceof TCallable) && !$a->params) { - return false; - } - - if ($a instanceof Type\Atomic\TCallableArray || - $a instanceof Type\Atomic\TCallableString || - $a instanceof Type\Atomic\TCallableKeyedArray - ) { - return false; - } - } - - return true; - } - - private static function fromLiteralString( - Union $constant, - StatementsAnalyzer $statements_analyzer - ): ?HighOrderFunctionArgInfo { - $literal = $constant->isSingle() ? $constant->getSingleAtomic() : null; - - if (!$literal instanceof Type\Atomic\TLiteralString || empty($literal->value)) { - return null; - } - - $codebase = $statements_analyzer->getCodebase(); - - return new HighOrderFunctionArgInfo( - HighOrderFunctionArgInfo::TYPE_STRING_CALLABLE, - strpos($literal->value, '::') !== false - ? $codebase->methods->getStorage(MethodIdentifier::wrap($literal->value)) - : $codebase->functions->getStorage($statements_analyzer, strtolower($literal->value)), - ); - } -} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php deleted file mode 100644 index 526e6ee1141..00000000000 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php +++ /dev/null @@ -1,90 +0,0 @@ -type = $type; - $this->function_storage = $function_storage; - $this->class_storage = $class_storage; - } - - public function getTemplates(): TemplateResult - { - $templates = $this->class_storage - ? array_merge( - $this->function_storage->template_types ?? [], - $this->class_storage->template_types ?? [], - ) - : $this->function_storage->template_types ?? []; - - return new TemplateResult($templates, []); - } - - public function getType(): string - { - return $this->type; - } - - public function getFunctionType(): Union - { - switch ($this->type) { - case self::TYPE_FIRST_CLASS_CALLABLE: - return new Union([ - new TClosure( - 'Closure', - $this->function_storage->params, - $this->function_storage->return_type, - $this->function_storage->pure, - ), - ]); - - case self::TYPE_STRING_CALLABLE: - case self::TYPE_CLASS_CALLABLE: - return new Union([ - new TCallable( - 'callable', - $this->function_storage->params, - $this->function_storage->return_type, - $this->function_storage->pure, - ), - ]); - - default: - return $this->function_storage->return_type ?? Type::getMixed(); - } - } -} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index 980043f1ce7..0a23879a337 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -228,9 +228,6 @@ public static function analyze( if ($inferred_template_result) { $template_result->lower_bounds += $inferred_template_result->lower_bounds; } - if ($method_storage && $method_storage->template_types) { - $template_result->template_types += $method_storage->template_types; - } if ($codebase->store_node_types && !$stmt->isFirstClassCallable() diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 45476ca22b6..ceffa4fd60e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -72,8 +72,7 @@ class NewAnalyzer extends CallAnalyzer public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\New_ $stmt, - Context $context, - TemplateResult $template_result = null + Context $context ): bool { $fq_class_name = null; @@ -255,7 +254,6 @@ public static function analyze( $fq_class_name, $from_static, $can_extend, - $template_result, ); } else { ArgumentsAnalyzer::analyze( @@ -291,8 +289,7 @@ private static function analyzeNamedConstructor( Context $context, string $fq_class_name, bool $from_static, - bool $can_extend, - TemplateResult $template_result = null + bool $can_extend ): void { $storage = $codebase->classlike_storage_provider->get($fq_class_name); @@ -393,7 +390,7 @@ private static function analyzeNamedConstructor( ); } - $template_result ??= new TemplateResult([], []); + $template_result = new TemplateResult([], []); if (self::checkMethodArgs( $method_id, diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 7cdb1b54a05..b49ea1b4a14 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -271,7 +271,7 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\New_) { - return NewAnalyzer::analyze($statements_analyzer, $stmt, $context, $template_result); + return NewAnalyzer::analyze($statements_analyzer, $stmt, $context); } if ($stmt instanceof PhpParser\Node\Expr\Array_) { diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 9fa51c420cd..65482e45692 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -232,17 +232,6 @@ private static function handleAtomicStandin( ); } - if ($atomic_type instanceof TTemplateParam - && isset($template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) - ) { - $most_specific_type = self::getMostSpecificTypeFromBounds( - $template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], - $codebase, - ); - - return array_values($most_specific_type->getAtomicTypes()); - } - if ($atomic_type instanceof TTemplateParamClass && isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class]) ) { @@ -271,14 +260,11 @@ private static function handleAtomicStandin( $include_first = true; - if (isset($template_result->lower_bounds[$atomic_type->array_param_name][$atomic_type->defining_class]) + if (isset($template_result->template_types[$atomic_type->array_param_name][$atomic_type->defining_class]) && !empty($template_result->lower_bounds[$atomic_type->offset_param_name]) ) { $array_template_type - = self::getMostSpecificTypeFromBounds( - $template_result->lower_bounds[$atomic_type->array_param_name][$atomic_type->defining_class], - $codebase, - ); + = $template_result->template_types[$atomic_type->array_param_name][$atomic_type->defining_class]; $offset_template_type = self::getMostSpecificTypeFromBounds( array_values($template_result->lower_bounds[$atomic_type->offset_param_name])[0], @@ -334,12 +320,7 @@ private static function handleAtomicStandin( $include_first = true; $template_type = null; - if (isset($template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class])) { - $template_type = self::getMostSpecificTypeFromBounds( - $template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], - $codebase, - ); - } elseif (isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])) { + if (isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])) { $template_type = $template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class]; } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 5815222b1ad..3428471a9e8 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -145,29 +145,6 @@ function asTupled(ArrayList $list): ArrayList '$b' => 'ArrayList', ], ], - 'inferArgByPreviousMethodArg' => [ - 'code' => ' $list - * @param callable(A): B $first - * @param callable(B): C $second - * @return list - */ - public function map(array $list, callable $first, callable $second): array - { - throw new RuntimeException("never"); - } - } - $result = (new ArrayList())->map([1, 2, 3], fn($i) => ["num" => $i], fn($i) => ["object" => $i]);', - 'assertions' => [ - '$result' => 'list', - ], - ], 'inferArgByPreviousFunctionArg' => [ 'code' => ' 'list', ], ], - 'inferInvokableClassCallable' => [ - 'code' => 'ab = Closure::fromCallable($ab); - } - - /** - * @template K - * @param array $a - * @return array - */ - public function __invoke(array $a): array - { - $b = []; - - foreach ($a as $k => $v) { - $b[$k] = ($this->ab)($v); - } - - return $b; - } - } - /** - * @template A - * @template B - * @param A $a - * @param callable(A): B $ab - * @return B - */ - function pipe(mixed $a, callable $ab): mixed - { - return $ab($a); - } - /** - * @return array - */ - function getDict(): array - { - return ["fst" => 1, "snd" => 2, "thr" => 3]; - } - $result = pipe(getDict(), new MapOperator(fn($i) => ["num" => $i])); - ', - 'assertions' => [ - '$result' => 'array', - ], - 'ignored_issues' => [], - 'php_version' => '8.0', - ], - 'inferConstCallableLikeFirstClassCallable' => [ - 'code' => '): list - */ - function map(callable $callback): Closure - { - return fn(array $list) => array_map($callback, $list); - } - /** - * @template A - * @template B - * @param A $a - * @param callable(A): B $ab - * @return B - */ - function pipe1(mixed $a, callable $ab): mixed - { - return $ab($a); - } - /** - * @template A - * @template B - * @template C - * @param A $a - * @param callable(A): B $ab - * @param callable(B): C $bc - * @return C - */ - function pipe2(mixed $a, callable $ab, callable $bc): mixed - { - return $bc($ab($a)); - } - } - - namespace App { - use Functions\Module; - use function Functions\map; - use function Functions\pipe1; - use function Functions\pipe2; - use const Functions\classId; - use const Functions\id; - - $class_const_id = pipe1([42], Module::id); - $class_const_composition = pipe1([42], map(Module::id)); - $class_const_sequential = pipe2([42], map(fn($i) => ["num" => $i]), Module::id); - - $class_const_alias_id = pipe1([42], classId); - $class_const_alias_composition = pipe1([42], map(classId)); - $class_const_alias_sequential = pipe2([42], map(fn($i) => ["num" => $i]), classId); - - $const_id = pipe1([42], id); - $const_composition = pipe1([42], map(id)); - $const_sequential = pipe2([42], map(fn($i) => ["num" => $i]), id); - - $string_id = pipe1([42], "Functions\id"); - $string_composition = pipe1([42], map("Functions\id")); - $string_sequential = pipe2([42], map(fn($i) => ["num" => $i]), "Functions\id"); - - $class_string_id = pipe1([42], "Functions\Module::id"); - $class_string_composition = pipe1([42], map("Functions\Module::id")); - $class_string_sequential = pipe2([42], map(fn($i) => ["num" => $i]), "Functions\Module::id"); - } - ', - 'assertions' => [ - '$class_const_id===' => 'list{42}', - '$class_const_composition===' => 'list<42>', - '$class_const_sequential===' => 'list', - '$class_const_alias_id===' => 'list{42}', - '$class_const_alias_composition===' => 'list<42>', - '$class_const_alias_sequential===' => 'list', - '$const_id===' => 'list{42}', - '$const_composition===' => 'list<42>', - '$const_sequential===' => 'list', - '$string_id===' => 'list{42}', - '$string_composition===' => 'list<42>', - '$string_sequential===' => 'list', - '$class_string_id===' => 'list{42}', - '$class_string_composition===' => 'list<42>', - '$class_string_sequential===' => 'list', - ], - 'ignored_issues' => [], - 'php_version' => '8.0', - ], 'inferPipelineWithPartiallyAppliedFunctions' => [ 'code' => ' [], 'php_version' => '8.0', ], - 'inferPipelineWithPartiallyAppliedFunctionsAndFirstClassCallable' => [ - 'code' => '): list - */ - function map(callable $callback): Closure - { - return fn($array) => array_map($callback, $array); - } - - /** - * @return list - */ - function getNums(): array - { - return []; - } - - /** - * @template T of float|int - */ - final class ObjectNum - { - /** - * @psalm-param T $value - */ - public function __construct( - public readonly float|int $value, - ) {} - } - - /** - * @return list> - */ - function getObjectNums(): array - { - return []; - } - - $id = pipe(getNums(), id(...)); - $wrapped_id = pipe(getNums(), map(id(...))); - $id_nested = pipe(getObjectNums(), map(id(...))); - $id_nested_simple = pipe(getObjectNums(), id(...)); - ', - 'assertions' => [ - '$id' => 'list', - '$wrapped_id' => 'list', - '$id_nested' => 'list>', - '$id_nested_simple' => 'list>', - ], - 'ignored_issues' => [], - 'php_version' => '8.1', - ], - 'inferFirstClassCallableWithGenericObject' => [ - 'code' => ' $container - * @return A - */ - function unwrap(Container $container) - { - return $container->value; - } - $result = pipe( - new Container(42), - unwrap(...), - ); - ', - 'assertions' => [ - '$result===' => '42', - ], - 'ignored_issues' => [], - 'php_version' => '8.1', - ], - 'inferFirstClassCallableOnMethodCall' => [ - 'code' => 'a), $processB($this->b)]; - } - } - - /** - * @template A - * @param A $value - * @return A - */ - function id(mixed $value): mixed - { - return $value; - } - - function intToString(int $value): string - { - return (string) $value; - } - - /** - * @template A - * @param A $value - * @return list{A} - */ - function singleToList(mixed $value): array - { - return [$value]; - } - - $processor = new Processor(a: 1, b: 2); - - $test_id = $processor->process(id(...), id(...)); - $test_complex = $processor->process(intToString(...), singleToList(...)); - ', - 'assertions' => [ - '$test_id' => 'list{int, int}', - '$test_complex' => 'list{string, list{int}}', - ], - 'ignored_issues' => [], - 'php_version' => '8.1', - ], - 'inferFirstClassCallableOnMethodCallWithMultipleParams' => [ - 'code' => 'a, $this->b, $this->c); - } - } - - /** - * @template A - * @template B - * @template C - * @param A $value1 - * @param B $value2 - * @param C $value3 - * @return list{A, B, C} - */ - function tripleId(mixed $value1, mixed $value2, mixed $value3): array - { - return [$value1, $value2, $value3]; - } - - $processor = new Processor(a: 1, b: 2, c: 3); - - $test = $processor->process(tripleId(...)); - ', - 'assertions' => [ - '$test' => 'list{int, int, int}', - ], - 'ignored_issues' => [], - 'php_version' => '8.1', - ], - 'inferFirstClassCallableOnMethodCallWithTemplatedAndNonTemplatedParams' => [ - 'code' => 'param1, $this->param2); - } - } - - /** - * @template T of int|float - * @param T $param2 - * @return array{param1: int, param2: T} - */ - function appHandler1(int $param1, int|float $param2): array - { - return ["param1" => $param1, "param2" => $param2]; - } - - /** - * @template T of int|float - * @param T $param1 - * @return array{param1: T, param2: int} - */ - function appHandler2(int|float $param1, int $param2): array - { - return ["param1" => $param1, "param2" => $param2]; - } - - /** - * @return array{param1: int, param2: int} - */ - function appHandler3(int $param1, int $param2): array - { - return ["param1" => $param1, "param2" => $param2]; - } - - $app = new App(param1: 42, param2: 42); - - $result1 = $app->run(appHandler1(...)); - $result2 = $app->run(appHandler2(...)); - $result3 = $app->run(appHandler3(...)); - ', - 'assertions' => [ - '$result1===' => 'array{param1: int, param2: 42}', - '$result2===' => 'array{param1: 42, param2: int}', - '$result3===' => 'array{param1: int, param2: int}', - ], - 'ignored_issues' => [], - 'php_version' => '8.1', - ], 'inferTypeWhenClosureParamIsOmitted' => [ 'code' => ' 'InvalidArgument', ], - 'invalidFirstClassCallableCannotBeInferred' => [ - 'code' => 'param1); - } - } - - /** - * @template P1 of int|float - * @param P1 $param1 - * @return array{param1: P1} - */ - function appHandler(mixed $param1): array - { - return ["param1" => $param1]; - } - - $result = (new App(param1: [42]))->run(appHandler(...)); - ', - 'error_message' => 'InvalidArgument', - 'ignored_issues' => [], - 'php_version' => '8.1', - ], 'variadicClosureAssignability' => [ 'code' => '> $ints */ + /** @param ArrayCollection $ints */ function takesInts(ArrayCollection $ints) :void {} /** @param ArrayCollection $ints */ function takesIntsOrStrings(ArrayCollection $ints) :void {} - /** @return list */ - function getList() :array {return [];} - - takesInts((new ArrayCollection(getList()))->map("strlen")); + takesInts((new ArrayCollection(["a", "b"]))->map("strlen")); /** @return ($s is "string" ? string : int) */ function foo(string $s) { @@ -2376,7 +2373,7 @@ function foo(string $s) { return 5; } - takesIntsOrStrings((new ArrayCollection(getList()))->map("foo")); + takesIntsOrStrings((new ArrayCollection(["a", "b"]))->map("foo")); /** * @template T