diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 9a4068228e0..6158937302d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -938,18 +938,21 @@ protected static function checkFunctionLikeArgumentsMatch( $calling_class_storage = $class_storage; + $static_fq_class_name = $fq_class_name; + $self_fq_class_name = $fq_class_name; + if ($method_id && strpos($method_id, '::')) { $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); if ($declaring_method_id && $declaring_method_id !== $method_id) { - list($fq_class_name) = explode('::', $declaring_method_id); - $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); + list($self_fq_class_name) = explode('::', $declaring_method_id); + $class_storage = $codebase->classlike_storage_provider->get($self_fq_class_name); } $appearing_method_id = $codebase->methods->getAppearingMethodId($method_id); if ($appearing_method_id && $declaring_method_id !== $appearing_method_id) { - list($fq_class_name) = explode('::', $appearing_method_id); + list($self_fq_class_name) = explode('::', $appearing_method_id); } } @@ -1044,7 +1047,8 @@ protected static function checkFunctionLikeArgumentsMatch( if (self::checkFunctionLikeArgumentMatches( $statements_analyzer, $cased_method_id, - $fq_class_name, + $self_fq_class_name, + $static_fq_class_name, $function_param, $argument_offset, $arg, @@ -1158,7 +1162,8 @@ protected static function checkFunctionLikeArgumentsMatch( /** * @param string|null $cased_method_id - * @param string|null $fq_class_name + * @param string|null $self_fq_class_name + * @param string|null $static_fq_class_name * @param FunctionLikeParameter|null $function_param * @param array> $existing_generic_params * @param array> $generic_params @@ -1168,7 +1173,8 @@ protected static function checkFunctionLikeArgumentsMatch( private static function checkFunctionLikeArgumentMatches( StatementsAnalyzer $statements_analyzer, $cased_method_id, - $fq_class_name, + $self_fq_class_name, + $static_fq_class_name, $function_param, int $argument_offset, PhpParser\Node\Arg $arg, @@ -1224,7 +1230,8 @@ private static function checkFunctionLikeArgumentMatches( $statements_analyzer, $codebase, $cased_method_id, - $fq_class_name, + $self_fq_class_name, + $static_fq_class_name, $function_param, $arg->value->inferredType, $argument_offset, @@ -1375,7 +1382,8 @@ private static function handlePossiblyMatchingByRefParam( /** * @param string|null $cased_method_id - * @param string|null $fq_class_name + * @param string|null $self_fq_class_name + * @param string|null $static_fq_class_name * @param array> $existing_generic_params * @param array> $generic_params * @param array> $template_types @@ -1386,7 +1394,8 @@ private static function checkFunctionLikeTypeMatches( StatementsAnalyzer $statements_analyzer, Codebase $codebase, $cased_method_id, - $fq_class_name, + $self_fq_class_name, + $static_fq_class_name, $function_param, Type\Union $arg_type, int $argument_offset, @@ -1452,8 +1461,8 @@ private static function checkFunctionLikeTypeMatches( $fleshed_out_type = ExpressionAnalyzer::fleshOutType( $codebase, $param_type, - $fq_class_name ?: $context->self, - $fq_class_name ?: $context->self + $self_fq_class_name, + $static_fq_class_name ); if ($arg->unpack) { diff --git a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php index 8d8b173c379..7aab5bea1e4 100644 --- a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php @@ -1712,58 +1712,66 @@ private static function compareCallable( return false; } - if ($container_type_part->params !== null) { - foreach ($container_type_part->params as $i => $container_param) { - if (!isset($input_type_part->params[$i])) { - if ($container_param->is_optional) { - break; - } + if ($input_type_part->params !== null && $container_type_part->params !== null) { + foreach ($input_type_part->params as $i => $input_param) { + $container_param = null; - $type_coerced = true; - $type_coerced_from_mixed = true; + if (isset($container_type_part->params[$i])) { + $container_param = $container_type_part->params[$i]; + } elseif ($container_type_part->params) { + $last_param = end($container_type_part->params); - $all_types_contain = false; - break; + if ($last_param->is_variadic) { + $container_param = $last_param; + } } - $input_param = $input_type_part->params[$i]; + if (!$container_param) { + if ($input_param->is_optional) { + break; + } - 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 - ) + return false; + } + + if ($container_param->type + && !$container_param->type->hasMixed() + && !self::isContainedBy( + $codebase, + $container_param->type, + $input_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; + 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 (!$container_type_part->return_type->isVoid() + && !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; - } else { - if (!$container_type_part->return_type->isVoid() - && !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; - } } } } diff --git a/src/Psalm/Internal/CallMap.php b/src/Psalm/Internal/CallMap.php index fcccb358345..307f18a272a 100644 --- a/src/Psalm/Internal/CallMap.php +++ b/src/Psalm/Internal/CallMap.php @@ -11303,7 +11303,7 @@ 'RegexIterator::setPregFlags' => ['void', 'new_flags'=>'int'], 'RegexIterator::valid' => ['bool'], 'register_event_handler' => ['bool', 'event_handler_func'=>'string', 'handler_register_name'=>'string', 'event_type_mask'=>'int'], -'register_shutdown_function' => ['void', 'function'=>'callable():void', '...parameter='=>'mixed'], +'register_shutdown_function' => ['void', 'function'=>'callable(mixed...):void', '...parameter='=>'mixed'], 'register_tick_function' => ['bool', 'function'=>'callable():void', '...args='=>'mixed'], 'rename' => ['bool', 'old_name'=>'string', 'new_name'=>'string', 'context='=>'resource'], 'rename_function' => ['bool', 'original_name'=>'string', 'new_name'=>'string'], diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index a2943ee99bb..407ee4867ec 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -90,7 +90,7 @@ public static function handleOverride(array $args, Codebase $codebase) * @return ?Type\Union */ function ( - StatementsAnalyzer $_statements_analyzer, + \Psalm\StatementsSource $_statements_analyzer, string $fq_classlike_name, string $method_name, array $call_args, @@ -143,7 +143,7 @@ function ( * @return ?Type\Union */ function ( - StatementsAnalyzer $_statements_analyzer, + \Psalm\StatementsSource $_statements_analyzer, string $fq_classlike_name, string $method_name, array $call_args, @@ -176,7 +176,7 @@ function ( * @return ?Type\Union */ function ( - StatementsAnalyzer $_statements_analyzer, + \Psalm\StatementsSource $_statements_analyzer, string $fq_classlike_name, string $method_name, array $call_args, @@ -229,7 +229,7 @@ function ( * @param array $call_args */ function ( - StatementsAnalyzer $statements_analyzer, + \Psalm\StatementsSource $statements_analyzer, string $function_id, array $call_args, Context $_context, @@ -262,6 +262,10 @@ function ( } } + if (!$statements_analyzer instanceof StatementsAnalyzer) { + throw new \UnexpectedValueException('This is bad'); + } + $storage = $statements_analyzer->getCodebase()->functions->getStorage( $statements_analyzer, $function_id @@ -277,7 +281,7 @@ function ( * @param array $call_args */ function ( - StatementsAnalyzer $statements_analyzer, + \Psalm\StatementsSource $statements_analyzer, string $function_id, array $call_args, Context $_context, @@ -290,6 +294,10 @@ function ( return clone $call_arg_type; } + if (!$statements_analyzer instanceof StatementsAnalyzer) { + throw new \UnexpectedValueException('This is bad'); + } + $storage = $statements_analyzer->getCodebase()->functions->getStorage( $statements_analyzer, $function_id @@ -305,7 +313,7 @@ function ( * @param array $call_args */ function ( - StatementsAnalyzer $statements_analyzer, + \Psalm\StatementsSource $statements_analyzer, string $function_id, array $call_args, Context $_context, @@ -327,6 +335,10 @@ function ( } } + if (!$statements_analyzer instanceof StatementsAnalyzer) { + throw new \UnexpectedValueException('This is bad'); + } + $storage = $statements_analyzer->getCodebase()->functions->getStorage( $statements_analyzer, $function_id diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 1b79e6792f7..93f33975cdf 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -364,10 +364,10 @@ class C2 extends C {} * @param Closure(B):A $f * @param Closure(C):B $g * - * @return Closure(C):A + * @return Closure(C2):A */ function foo(Closure $f, Closure $g) : Closure { - return function (C2 $x) use ($f, $g) : A { + return function (C $x) use ($f, $g) : A { return $f($g($x)); }; } @@ -693,31 +693,48 @@ class one { public function two(string $_p): void {} } ], 'callableSelfArg' => [ 'func2(function(B $x): void {}); + $c->func2(function(B $x): void {}); + class A {} class B extends A { - /** - * @param callable(static) $f - */ - function func1(callable $f): void { - $f($this); - } - /** * @param callable(self) $f */ function func2(callable $f): void { $f($this); } + }', + ], + 'callableParentArg' => [ + 'func3(function(A $x): void {}); + $c->func3(function(A $x): void {}); + + class A {} + + class B extends A { /** * @param callable(parent) $f */ function func3(callable $f): void { $f($this); } - } - + }', + ], + 'callableStaticArg' => [ + 'func1(function(B $x): void {}); $c->func1(function(C $x): void {}); - $b->func2(function(B $x): void {}); - $c->func2(function(B $x): void {});', + + class A {} + + class B extends A { + /** + * @param callable(static) $f + */ + function func1(callable $f): void { + $f($this); + } + }', ], 'callableSelfReturn' => [ ' [ + ' [ + ' [ + ' 'InvalidArgument', ], + 'prohibitCallableWithRequiredArg' => [ + ' 'InvalidArgument' + ], ]; } }