From 25b01ce3a3e0602e5dd60432f480c4e829bf79a7 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:36:08 +0200 Subject: [PATCH 1/4] ensure callbacks have the required number of params Fix https://github.com/vimeo/psalm/issues/8593 --- .../Type/Comparator/UnionTypeComparator.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index c997199d650..1906fc938f1 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,39 @@ 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_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; + } + } + + $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; + } + } + } + + // too few or too many non-optional params provided in callback + if ($container_required_param_count > $input_all_param_count + || count($container_type_part->params) < $input_required_param_count + ) { + return false; + } + } + if ($union_comparison_result) { $atomic_comparison_result = new TypeComparisonResult(); } else { From 2c47d06968090a4682454a7522aa5c8543394714 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 17 Oct 2022 16:25:43 +0200 Subject: [PATCH 2/4] fix tests --- tests/Template/FunctionTemplateTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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); } } From edeff148b41592bf1731de9e916ea02190773578 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 22 Oct 2022 08:28:56 +0200 Subject: [PATCH 3/4] handle variadic --- .../Internal/Type/Comparator/UnionTypeComparator.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index 1906fc938f1..52ad18757f4 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -144,11 +144,16 @@ public static function isContainedBy( && 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; @@ -161,12 +166,16 @@ public static function isContainedBy( 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 - || count($container_type_part->params) < $input_required_param_count + || $container_all_param_count < $input_required_param_count ) { return false; } From 7d953dfb90d3508039b2ce355744b38710de9d86 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 22 Oct 2022 08:48:18 +0200 Subject: [PATCH 4/4] add tests --- tests/ArgTest.php | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) 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', + ], ]; } }