diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index c997199d650..52ad18757f4 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -6,6 +6,7 @@ use Psalm\Internal\Type\TypeExpander; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArrayKey; +use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TIntRange; @@ -21,6 +22,10 @@ use function array_pop; use function array_push; use function array_reverse; +use function count; +use function is_array; + +use const PHP_INT_MAX; /** * @internal @@ -134,6 +139,48 @@ public static function isContainedBy( continue; } + // if params are specified + if ($container_type_part instanceof TCallable + && is_array($container_type_part->params) + && $input_type_part instanceof TCallable + ) { + $container_all_param_count = count($container_type_part->params); + $container_required_param_count = 0; + foreach ($container_type_part->params as $index => $container_param) { + if ($container_param->is_optional === false) { + $container_required_param_count = $index + 1; + } + + if ($container_param->is_variadic === true) { + $container_all_param_count = PHP_INT_MAX; + } + } + + $input_required_param_count = 0; + if (!is_array($input_type_part->params)) { + // it's not declared, there can be an arbitrary number of params + $input_all_param_count = PHP_INT_MAX; + } else { + $input_all_param_count = count($input_type_part->params); + foreach ($input_type_part->params as $index => $input_param) { + if ($input_param->is_optional === false) { + $input_required_param_count = $index + 1; + } + + if ($input_param->is_variadic === true) { + $input_all_param_count = PHP_INT_MAX; + } + } + } + + // too few or too many non-optional params provided in callback + if ($container_required_param_count > $input_all_param_count + || $container_all_param_count < $input_required_param_count + ) { + return false; + } + } + if ($union_comparison_result) { $atomic_comparison_result = new TypeComparisonResult(); } else { diff --git a/tests/ArgTest.php b/tests/ArgTest.php index de57435f0e9..2d16c1934dd 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -312,6 +312,40 @@ public function foo(int ...$values): array } ', ], + 'variadicCallbackArgsCountMatch' => [ + ' [ + ' 'TooFewArguments', ], + 'callbackArgsCountMismatch' => [ + ' 'InvalidScalarArgument', + ], + 'callableArgsCountMismatch' => [ + ' 'InvalidScalarArgument', + ], ]; } } diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index 407e96ebe5c..70b99124934 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -1560,13 +1560,13 @@ function deserialize_object(string $data, string $type) {}' * @template TNewKey of array-key * @template TNewValue * @psalm-param iterable $iterable - * @psalm-param callable(TKey, TValue): iterable $mapper + * @psalm-param callable(TKey): iterable $mapper * @psalm-return \Generator */ function map(iterable $iterable, callable $mapper): Generator { - foreach ($iterable as $key => $value) { - yield from $mapper($key, $value); + foreach ($iterable as $key => $_) { + yield from $mapper($key); } }