From d5bc652a4d59d0c0493ae83de129e7869cde9ae3 Mon Sep 17 00:00:00 2001 From: klimick Date: Tue, 15 Aug 2023 09:11:08 +0300 Subject: [PATCH] Revert #9700, #9623, #9570, #7417 --- .../Expression/Call/ArgumentsAnalyzer.php | 34 +- .../Expression/Call/FunctionCallAnalyzer.php | 18 +- .../Call/HighOrderFunctionArgHandler.php | 325 -------- .../Call/HighOrderFunctionArgInfo.php | 90 -- .../Call/Method/AtomicMethodCallAnalyzer.php | 5 +- .../ExistingAtomicMethodCallAnalyzer.php | 10 +- .../Expression/Call/MethodCallAnalyzer.php | 4 +- .../Expression/Call/NewAnalyzer.php | 9 +- .../Expression/Call/StaticCallAnalyzer.php | 4 +- .../StaticMethod/AtomicStaticCallAnalyzer.php | 8 +- .../ExistingAtomicStaticCallAnalyzer.php | 7 +- .../Statements/ExpressionAnalyzer.php | 10 +- .../Comparator/CallableTypeComparator.php | 45 +- .../Type/TemplateStandinTypeReplacer.php | 25 +- tests/CallableTest.php | 773 ------------------ tests/Template/ClassTemplateTest.php | 9 +- 16 files changed, 26 insertions(+), 1350 deletions(-) delete mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php delete mode 100644 src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 653ffedc9ac..0dcf99fcbb9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -195,19 +195,7 @@ public static function analyze( $toggled_class_exists = true; } - $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( - $statements_analyzer, - $template_result ?? new TemplateResult([], []), - $high_order_callable_info, - $param->type ?? Type::getMixed(), - ); - } elseif (($arg->value instanceof PhpParser\Node\Expr\Closure + if (($arg->value instanceof PhpParser\Node\Expr\Closure || $arg->value instanceof PhpParser\Node\Expr\ArrowFunction) && $param && !$arg->value->getDocComment() @@ -227,15 +215,7 @@ public static function analyze( $was_inside_call = $context->inside_call; $context->inside_call = true; - if (ExpressionAnalyzer::analyze( - $statements_analyzer, - $arg->value, - $context, - false, - null, - false, - $high_order_template_result, - ) === false) { + if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) { $context->inside_call = $was_inside_call; return false; @@ -243,16 +223,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) ) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 21bdf3f9ea7..7c2c54c4d3a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -83,8 +83,7 @@ class FunctionCallAnalyzer extends CallAnalyzer public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\FuncCall $stmt, - Context $context, - ?TemplateResult $template_result = null + Context $context ): bool { $function_name = $stmt->name; @@ -167,13 +166,11 @@ public static function analyze( $set_inside_conditional = true; } - if (!$template_result) { - $template_result = new TemplateResult([], []); - } - if (!$is_first_class_callable) { + $template_result = null; + if (isset($function_call_info->function_storage->template_types)) { - $template_result->template_types += $function_call_info->function_storage->template_types ?: []; + $template_result = new TemplateResult($function_call_info->function_storage->template_types ?: [], []); } ArgumentsAnalyzer::analyze( @@ -209,8 +206,6 @@ public static function analyze( } } - $already_inferred_lower_bounds = $template_result->lower_bounds; - $template_result = new TemplateResult([], []); // do this here to allow closure param checks @@ -235,11 +230,6 @@ public static function analyze( $function_call_info->function_id, ); - $template_result->lower_bounds = array_merge( - $template_result->lower_bounds, - $already_inferred_lower_bounds, - ); - if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { $stmt_type = FunctionCallReturnTypeFetcher::fetch( $statements_analyzer, 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/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 4e6d3188211..12c5197f3e8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -78,8 +78,7 @@ public static function analyze( ?Atomic $static_type, bool $is_intersection, ?string $lhs_var_id, - AtomicMethodCallAnalysisResult $result, - ?TemplateResult $inferred_template_result = null + AtomicMethodCallAnalysisResult $result ): void { if ($lhs_type_part instanceof TTemplateParam && !$lhs_type_part->as->isMixed() @@ -113,7 +112,6 @@ public static function analyze( $context, $lhs_type_part->callable, $result, - $inferred_template_result, ); return; } @@ -491,7 +489,6 @@ public static function analyze( $lhs_var_id, $method_id, $result, - $inferred_template_result, ); $statements_analyzer->node_data = $old_node_data; 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..5b3b8345ba6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -71,8 +71,7 @@ public static function analyze( ?Atomic $static_type, ?string $lhs_var_id, MethodIdentifier $method_id, - AtomicMethodCallAnalysisResult $result, - ?TemplateResult $inferred_template_result = null + AtomicMethodCallAnalysisResult $result ): Union { $config = $codebase->config; @@ -225,13 +224,6 @@ public static function analyze( $template_result = new TemplateResult([], $class_template_params ?: []); $template_result->lower_bounds += $method_template_params; - 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() && !$context->collect_initializations diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 437f65d7510..59908857f4b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -47,8 +47,7 @@ public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\MethodCall $stmt, Context $context, - bool $real_method_call = true, - ?TemplateResult $template_result = null + bool $real_method_call = true ): bool { $was_inside_call = $context->inside_call; @@ -208,7 +207,6 @@ public static function analyze( false, $lhs_var_id, $result, - $template_result, ); if (isset($context->vars_in_scope[$lhs_var_id]) && ($possible_new_class_type = $context->vars_in_scope[$lhs_var_id]) instanceof Union 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/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index fa7cc498184..9f675543611 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -41,8 +41,7 @@ class StaticCallAnalyzer extends CallAnalyzer public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\StaticCall $stmt, - Context $context, - ?TemplateResult $template_result = null + Context $context ): bool { $method_id = null; @@ -213,7 +212,6 @@ public static function analyze( $moved_call, $has_mock, $has_existing_method, - $template_result, ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index b0839345418..e7f5b64d77e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -76,8 +76,7 @@ public static function analyze( bool $ignore_nullable_issues, bool &$moved_call, bool &$has_mock, - bool &$has_existing_method, - ?TemplateResult $inferred_template_result = null + bool &$has_existing_method ): void { $intersection_types = []; @@ -213,7 +212,6 @@ public static function analyze( $fq_class_name, $moved_call, $has_existing_method, - $inferred_template_result, ); } else { if ($stmt->name instanceof PhpParser\Node\Expr) { @@ -311,8 +309,7 @@ private static function handleNamedCall( array $intersection_types, string $fq_class_name, bool &$moved_call, - bool &$has_existing_method, - ?TemplateResult $inferred_template_result = null + bool &$has_existing_method ): bool { $codebase = $statements_analyzer->getCodebase(); @@ -889,7 +886,6 @@ private static function handleNamedCall( $cased_method_id, $class_storage, $moved_call, - $inferred_template_result, ); return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 607f521881a..dcd03eb45b5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -64,8 +64,7 @@ public static function analyze( MethodIdentifier $method_id, string $cased_method_id, ClassLikeStorage $class_storage, - bool &$moved_call, - ?TemplateResult $inferred_template_result = null + bool &$moved_call ): void { $fq_class_name = $method_id->fq_class_name; $method_name_lc = $method_id->method_name; @@ -186,10 +185,6 @@ public static function analyze( $template_result = new TemplateResult([], $found_generic_params ?: []); - if ($inferred_template_result) { - $template_result->lower_bounds += $inferred_template_result->lower_bounds; - } - if (CallAnalyzer::checkMethodArgs( $method_id, $args, diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 7cdb1b54a05..d1dc7876468 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -78,7 +78,6 @@ public static function analyze( bool $array_assignment = false, ?Context $global_context = null, bool $from_stmt = false, - ?TemplateResult $template_result = null, bool $assigned_to_reference = false ): bool { if (self::dispatchBeforeExpressionAnalysis($stmt, $context, $statements_analyzer) === false) { @@ -94,7 +93,6 @@ public static function analyze( $array_assignment, $global_context, $from_stmt, - $template_result, $assigned_to_reference, ) === false) { return false; @@ -149,7 +147,6 @@ private static function handleExpression( bool $array_assignment, ?Context $global_context, bool $from_stmt, - ?TemplateResult $template_result = null, bool $assigned_to_reference = false ): bool { if ($stmt instanceof PhpParser\Node\Expr\Variable) { @@ -174,11 +171,11 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\MethodCall) { - return MethodCallAnalyzer::analyze($statements_analyzer, $stmt, $context, true, $template_result); + return MethodCallAnalyzer::analyze($statements_analyzer, $stmt, $context); } if ($stmt instanceof PhpParser\Node\Expr\StaticCall) { - return StaticCallAnalyzer::analyze($statements_analyzer, $stmt, $context, $template_result); + return StaticCallAnalyzer::analyze($statements_analyzer, $stmt, $context); } if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { @@ -271,7 +268,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_) { @@ -287,7 +284,6 @@ private static function handleExpression( $statements_analyzer, $stmt, $context, - $template_result, ); } diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 2999480dd36..b3898842d85 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -10,9 +10,6 @@ use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Provider\NodeDataProvider; -use Psalm\Internal\Type\TemplateInferredTypeReplacer; -use Psalm\Internal\Type\TemplateResult; -use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeExpander; use Psalm\Type; use Psalm\Type\Atomic; @@ -26,7 +23,6 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Union; use UnexpectedValueException; use function array_slice; @@ -401,35 +397,6 @@ public static function getCallableFromAtomic( if ($codebase->methods->methodExists($invoke_id)) { $declaring_method_id = $codebase->methods->getDeclaringMethodId($invoke_id); - $template_result = null; - - if ($input_type_part instanceof Atomic\TGenericObject) { - $invokable_storage = $codebase->methods->getClassLikeStorageForMethod( - $declaring_method_id ?? $invoke_id, - ); - $type_params = []; - - foreach ($invokable_storage->template_types ?? [] as $template => $for_class) { - foreach ($for_class as $type) { - $type_params[] = new Type\Union([ - new TTemplateParam($template, $type, $input_type_part->value), - ]); - } - } - - if (!empty($type_params)) { - $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); - $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); - - TemplateStandinTypeReplacer::fillTemplateResult( - new Type\Union([$input_with_templates]), - $template_result, - $codebase, - null, - new Type\Union([$input_type_part]), - ); - } - } if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); @@ -445,22 +412,12 @@ public static function getCallableFromAtomic( ); } - $callable = new TCallable( + return new TCallable( 'callable', $method_storage->params, $converted_return_type, $method_storage->pure, ); - - if ($template_result) { - $callable = TemplateInferredTypeReplacer::replace( - new Union([$callable]), - $template_result, - $codebase, - )->getSingleAtomic(); - } - - return $callable; } } } 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..85c8266c49c 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', ], ], - 'inferTemplateOfHighOrderFunctionArgByPreviousArg' => [ - 'code' => ' - */ - function getList() { throw new RuntimeException("???"); } - - /** - * @template T - * @return Closure(T): T - */ - function id() { throw new RuntimeException("???"); } - - /** - * @template A - * @template B - * - * @param list $_items - * @param callable(A): B $_ab - * @return list - */ - function map(array $_items, callable $_ab) { throw new RuntimeException("???"); } - - $result = map(getList(), id()); - ', - 'assertions' => [ - '$result' => 'list', - ], - ], - 'inferTemplateOfHighOrderFunctionArgByPreviousArgInClassContext' => [ - 'code' => ' - */ - public function map(callable $ab) { throw new RuntimeException("???"); } - } - - /** - * @return ArrayList - */ - function getList() { throw new RuntimeException("???"); } - - /** - * @template T - * @return Closure(T): T - */ - function id() { throw new RuntimeException("???"); } - - $result = getList()->map(id()); - ', - 'assertions' => [ - '$result' => 'ArrayList', - ], - ], - 'inferTemplateOfHighOrderFunctionFromMethodArgByPreviousArg' => [ - 'code' => '): T - */ - public function flatten() { throw new RuntimeException("???"); } - } - /** - * @return list> - */ - function getList() { throw new RuntimeException("???"); } - /** - * @template T - * @return Closure(list): T - */ - function flatten() { throw new RuntimeException("???"); } - /** - * @template A - * @template B - * - * @param list $_a - * @param callable(A): B $_ab - * @return list - */ - function map(array $_a, callable $_ab) { throw new RuntimeException("???"); } - - $ops = new Ops; - $result = map(getList(), $ops->flatten()); - ', - 'assertions' => [ - '$result' => 'list', - ], - ], - 'inferTemplateOfHighOrderFunctionFromStaticMethodArgByPreviousArg' => [ - 'code' => '): T - */ - public static function flatten() { throw new RuntimeException("???"); } - } - /** - * @return list> - */ - function getList() { throw new RuntimeException("???"); } - /** - * @template T - * @return Closure(list): T - */ - function flatten() { throw new RuntimeException("???"); } - /** - * @template A - * @template B - * - * @param list $_a - * @param callable(A): B $_ab - * @return list - */ - function map(array $_a, callable $_ab) { throw new RuntimeException("???"); } - - $result = map(getList(), StaticOps::flatten()); - ', - 'assertions' => [ - '$result' => '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' => '): list - */ - function filter(callable $_predicate): Closure { throw new RuntimeException("???"); } - /** - * @template A - * @template B - * - * @param callable(A): B $_ab - * @return Closure(list): list - */ - function map(callable $_ab): Closure { throw new RuntimeException("???"); } - /** - * @template T - * @return (Closure(list): (non-empty-list | null)) - */ - function asNonEmptyList(): Closure { throw new RuntimeException("???"); } - /** - * @template T - * @return Closure(T): T - */ - function id(): Closure { throw new RuntimeException("???"); } - - /** - * @template A - * @template B - * @template C - * @template D - * @template E - * @template F - * - * @param A $arg - * @param callable(A): B $ab - * @param callable(B): C $bc - * @param callable(C): D $cd - * @param callable(D): E $de - * @param callable(E): F $ef - * @return F - */ - function pipe4(mixed $arg, callable $ab, callable $bc, callable $cd, callable $de, callable $ef): mixed - { - return $ef($de($cd($bc($ab($arg))))); - } - - /** - * @template TFoo of string - * @template TBar of bool - */ - final class Item - { - /** - * @param TFoo $foo - * @param TBar $bar - */ - public function __construct( - public string $foo, - public bool $bar, - ) { } - } - - /** - * @return list - */ - function getList(): array { return []; } - - $result = pipe4( - getList(), - filter(fn($i) => $i->bar), - filter(fn(Item $i) => $i->foo !== "bar"), - map(fn($i) => new Item("test: " . $i->foo, $i->bar)), - asNonEmptyList(), - id(), - );', - 'assertions' => [ - '$result' => 'non-empty-list>|null', - ], - 'ignored_issues' => [], - '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