Skip to content

Commit

Permalink
Merge pull request #8594 from kkmuffme/callable-invalidargument-requi…
Browse files Browse the repository at this point in the history
…red-param-mismatch-missing-error

ensure callbacks have the required number of params
  • Loading branch information
orklah committed Oct 22, 2022
2 parents e8e8777 + 7d953df commit b739b67
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 3 deletions.
47 changes: 47 additions & 0 deletions src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions tests/ArgTest.php
Expand Up @@ -312,6 +312,40 @@ public function foo(int ...$values): array
}
',
],
'variadicCallbackArgsCountMatch' => [
'<?php
/**
* @param callable(string, string):void $callback
* @return void
*/
function caller($callback) {}
/**
* @param string ...$bar
* @return void
*/
function foo(...$bar) {}
caller("foo");',
],
'variadicCallableArgsCountMatch' => [
'<?php
/**
* @param callable(string, ...int):void $callback
* @return void
*/
function var_caller($callback) {}
/**
* @param string $a
* @param int $b
* @param int $c
* @return void
*/
function foo($a, $b, $c) {}
var_caller("foo");',
],
];
}

Expand Down Expand Up @@ -733,6 +767,41 @@ public function __construct(
',
'error_message' => 'TooFewArguments',
],
'callbackArgsCountMismatch' => [
'<?php
/**
* @param callable(string, string):void $callback
* @return void
*/
function caller($callback) {}
/**
* @param string $a
* @return void
*/
function foo($a) {}
caller("foo");',
'error_message' => 'InvalidScalarArgument',
],
'callableArgsCountMismatch' => [
'<?php
/**
* @param callable(string):void $callback
* @return void
*/
function caller($callback) {}
/**
* @param string $a
* @param string $b
* @return void
*/
function foo($a, $b) {}
caller("foo");',
'error_message' => 'InvalidScalarArgument',
],
];
}
}
6 changes: 3 additions & 3 deletions tests/Template/FunctionTemplateTest.php
Expand Up @@ -1560,13 +1560,13 @@ function deserialize_object(string $data, string $type) {}'
* @template TNewKey of array-key
* @template TNewValue
* @psalm-param iterable<TKey, TValue> $iterable
* @psalm-param callable(TKey, TValue): iterable<TNewKey, TNewValue> $mapper
* @psalm-param callable(TKey): iterable<TNewKey, TNewValue> $mapper
* @psalm-return \Generator<TNewKey, TNewValue>
*/
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);
}
}
Expand Down

0 comments on commit b739b67

Please sign in to comment.