From 5a0e9d09652656bec72ec5403b193b9ef2499b3a Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Tue, 26 Jan 2021 22:43:42 -0500 Subject: [PATCH] Fix #5107 - treat function-bound templated parameters the same Previously they were treated differently depending on whether or not they were inside a method --- .../Analyzer/FunctionLikeAnalyzer.php | 12 ------- .../Expression/Call/FunctionCallAnalyzer.php | 13 +++++-- .../Expression/Call/NewAnalyzer.php | 15 +++++--- .../Type/SimpleAssertionReconciler.php | 18 ++++++++-- tests/Template/ConditionalReturnTypeTest.php | 34 +++++++++++++++++++ 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 61e03b550..47836c765 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -978,18 +978,6 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $check_stmts = false; } } - - if (!$storage instanceof MethodStorage - && $param_type->hasTemplate() - && $param_type->isSingle() - ) { - /** @var Type\Atomic\TTemplateParam */ - $template_type = \array_values($param_type->getAtomicTypes())[0]; - - if ($template_type->as->getTemplateTypes()) { - $param_type = $template_type->as; - } - } } else { $param_type = Type::getMixed(); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 805a80d76..263cb8876 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -554,7 +554,16 @@ class FunctionCallAnalyzer extends CallAnalyzer $invalid_function_call_types = []; $has_valid_function_call_type = false; - foreach ($stmt_name_type->getAtomicTypes() as $var_type_part) { + $var_atomic_types = $stmt_name_type->getAtomicTypes(); + + while ($var_atomic_types) { + $var_type_part = \array_shift($var_atomic_types); + + if ($var_type_part instanceof TTemplateParam) { + $var_atomic_types = \array_merge($var_atomic_types, $var_type_part->as->getAtomicTypes()); + continue; + } + if ($var_type_part instanceof Type\Atomic\TClosure || $var_type_part instanceof TCallable) { if (!$var_type_part->is_pure && $context->pure) { if (IssueBuffer::accepts( @@ -593,8 +602,6 @@ class FunctionCallAnalyzer extends CallAnalyzer $function_call_info->function_exists = true; $has_valid_function_call_type = true; - } elseif ($var_type_part instanceof TTemplateParam && $var_type_part->as->hasCallableType()) { - $has_valid_function_call_type = true; } elseif ($var_type_part instanceof TMixed || $var_type_part instanceof TTemplateParam) { $has_valid_function_call_type = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index de3dd1a97..2e85c59fc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -569,7 +569,16 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna $new_type = null; - foreach ($stmt_class_type->getAtomicTypes() as $lhs_type_part) { + $stmt_class_types = $stmt_class_type->getAtomicTypes(); + + while ($stmt_class_types) { + $lhs_type_part = \array_shift($stmt_class_types); + + if ($lhs_type_part instanceof Type\Atomic\TTemplateParam) { + $stmt_class_types = \array_merge($stmt_class_types, $lhs_type_part->as->getAtomicTypes()); + continue; + } + if ($lhs_type_part instanceof Type\Atomic\TTemplateParamClass) { if (!$statements_analyzer->node_data->getType($stmt)) { $new_type_part = new Type\Atomic\TTemplateParam( @@ -735,9 +744,7 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna )) { // fall through } - } elseif ($lhs_type_part instanceof Type\Atomic\TMixed - || $lhs_type_part instanceof Type\Atomic\TTemplateParam - ) { + } elseif ($lhs_type_part instanceof Type\Atomic\TMixed) { if (IssueBuffer::accepts( new MixedMethodCall( 'Cannot call constructor on an unknown class', diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 7db56ceae..c012dad30 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -1995,12 +1995,24 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler $callable_types[] = $type; $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { - if ($type->as->isMixed()) { + if ($type->as->hasCallableType() || $type->as->hasMixed()) { $type = clone $type; - $type->as = new Type\Union([new Type\Atomic\TCallable]); + + $type->as = self::reconcileCallable( + $codebase, + $type->as, + null, + $negated, + null, + $suppressed_issues, + $failed_reconciliation, + $is_equality + ); } - $callable_types[] = $type; + $did_remove_type = true; + + $callable_types[] = $type; } else { $did_remove_type = true; } diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index 98f451a59..ef07b4f83 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -711,6 +711,40 @@ class ConditionalReturnTypeTest extends TestCase return new NullObject(); }' ], + 'reconcileCallableFunctionTemplateParam' => [ + ' [ + '