diff --git a/src/Psalm/Checker/TypeChecker.php b/src/Psalm/Checker/TypeChecker.php index 735875136..ffbf6e18f 100644 --- a/src/Psalm/Checker/TypeChecker.php +++ b/src/Psalm/Checker/TypeChecker.php @@ -344,6 +344,27 @@ class TypeChecker } } + if ($container_type_part instanceof Type\Atomic\Fn) { + if (!$input_type_part instanceof Type\Atomic\Fn) { + $type_coerced = true; + $type_coerced_from_mixed = true; + + return false; + } + + if (self::compareCallable( + $codebase, + $input_type_part, + $container_type_part, + $type_coerced, + $type_coerced_from_mixed, + $all_types_contain + ) === false + ) { + return false; + } + } + if (($input_type_part instanceof TArray || $input_type_part instanceof ObjectLike) && ($container_type_part instanceof TArray || $container_type_part instanceof ObjectLike) ) { @@ -445,6 +466,26 @@ class TypeChecker return true; } + if ($container_type_part instanceof TCallable && $input_type_part instanceof Type\Atomic\Fn) { + $all_types_contain = true; + + if (self::compareCallable( + $codebase, + $input_type_part, + $container_type_part, + $type_coerced, + $type_coerced_from_mixed, + $all_types_contain + ) === false + ) { + return false; + } + + if (!$all_types_contain) { + return false; + } + } + if ($input_type_part instanceof TNamedObject && $input_type_part->value === 'Closure' && $container_type_part instanceof TCallable @@ -571,6 +612,12 @@ class TypeChecker } } + if ($container_type_part instanceof Type\Atomic\Fn && $input_type_part instanceof TCallable) { + $type_coerced = true; + + return false; + } + if ($container_type_part instanceof TCallable && ( $input_type_part instanceof TString || @@ -631,6 +678,84 @@ class TypeChecker return false; } + /** + * @param TCallable|Type\Atomic\Fn $input_type_part + * @param TCallable|Type\Atomic\Fn $container_type_part + * @param bool &$type_coerced + * @param bool &$type_coerced_from_mixed + * @param bool &$all_types_contain + * + * @return null|false + * + * @psalm-suppress ConflictingReferenceConstraint + */ + private static function compareCallable( + Codebase $codebase, + $input_type_part, + $container_type_part, + &$type_coerced, + &$type_coerced_from_mixed, + &$all_types_contain + ) { + if ($container_type_part->params !== null && $input_type_part->params === null) { + $type_coerced = true; + $type_coerced_from_mixed = true; + + return false; + } + + if ($container_type_part->params !== null) { + foreach ($input_type_part->params as $i => $input_param) { + if (!isset($container_type_part->params[$i])) { + $type_coerced = true; + $type_coerced_from_mixed = true; + + $all_types_contain = false; + break; + } + + $container_param = $container_type_part->params[$i]; + + if (!self::isContainedBy( + $codebase, + $input_param->type ?: Type::getMixed(), + $container_param->type ?: Type::getMixed(), + false, + false, + $has_scalar_match, + $type_coerced, + $type_coerced_from_mixed + ) + ) { + $all_types_contain = false; + } + } + + if (isset($container_type_part->return_type)) { + if (!isset($input_type_part->return_type)) { + $type_coerced = true; + $type_coerced_from_mixed = true; + + $all_types_contain = false; + } else { + if (!self::isContainedBy( + $codebase, + $input_type_part->return_type, + $container_type_part->return_type, + false, + false, + $has_scalar_match, + $type_coerced, + $type_coerced_from_mixed + ) + ) { + $all_types_contain = false; + } + } + } + } + } + /** * Takes two arrays of types and merges them * diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 79c1cfb4c..2df6cd98c 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -298,6 +298,7 @@ class CallableTest extends TestCase /** * @param Closure(int):int $f * @param Closure(int):int $g + * * @return Closure(int):int */ function foo(Closure $f, Closure $g) : Closure { @@ -306,6 +307,112 @@ class CallableTest extends TestCase } }' ], + 'returnsTypedClosureWithClasses' => [ + ' [ + '$a' => 'A', + ], + ], + 'returnsTypedClosureWithSubclassParam' => [ + ' [ + '$a' => 'A', + ], + ], + 'returnsTypedClosureWithParentReturn' => [ + ' [ + '$a' => 'A', + ], + ], + 'returnsTypedCallableFromClosure' => [ + ' [ + '$a' => 'A', + ], + ], ]; } @@ -447,6 +554,155 @@ class CallableTest extends TestCase bar($add_one);', 'error_message' => 'InvalidReturnStatement', ], + 'returnsTypedClosureWithBadReturnType' => [ + ' 'InvalidReturnStatement', + ], + 'returnsTypedCallableWithBadReturnType' => [ + ' 'InvalidReturnStatement', + ], + 'returnsTypedClosureWithBadParamType' => [ + ' 'InvalidReturnStatement', + ], + 'returnsTypedCallableWithBadParamType' => [ + ' 'InvalidReturnStatement', + ], + 'returnsTypedClosureWithBadCall' => [ + ' 'InvalidArgument', + ], + 'returnsTypedClosureWithSubclassParam' => [ + ' 'LessSpecificReturnStatement', + ], + 'returnsTypedClosureWithSubclassReturn' => [ + ' 'LessSpecificReturnStatement', + ], + 'returnsTypedClosureFromCallable' => [ + ' 'LessSpecificReturnStatement', + ], ]; } }