Skip to content

add callback types for array_uintersect etc v2 #3312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
30 changes: 10 additions & 20 deletions resources/functionMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,21 +266,17 @@
'array_diff' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'],
'array_diff_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'],
'array_diff_key' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'],
'array_diff_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'callable(mixed,mixed):int'],
'array_diff_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable'],
'array_diff_ukey' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_comp_func'=>'callable(mixed,mixed):int'],
'array_diff_ukey\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_diff_uassoc' => ['array', 'array'=>'array', '...arrays'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_diff_ukey' => ['array', 'array'=>'array', '...arrays'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_fill' => ['array', 'start_key'=>'int', 'num'=>'int', 'val'=>'mixed'],
'array_fill_keys' => ['array', 'keys'=>'array', 'val'=>'mixed'],
'array_filter' => ['array', 'input'=>'array', 'callback='=>'callable(mixed,mixed):bool|callable(mixed):bool', 'flag='=>'int'],
'array_flip' => ['array', 'input'=>'array<int|string>'],
'array_intersect' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'],
'array_intersect_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'],
'array_intersect_key' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'],
'array_intersect_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_intersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable'],
'array_intersect_ukey' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_intersect_ukey\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable(mixed,mixed):int'],
'array_intersect_uassoc' => ['array', 'array'=>'array', '...arrays'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_intersect_ukey' => ['array', 'array'=>'array', '...arrays'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_key_exists' => ['bool', 'key'=>'string|int', 'search'=>'array'],
'array_key_first' => ['int|string|null', 'array'=>'array'],
'array_key_last' => ['int|string|null', 'array'=>'array'],
Expand All @@ -304,18 +300,12 @@
'array_slice' => ['array', 'input'=>'array', 'offset'=>'int', 'length='=>'?int', 'preserve_keys='=>'bool'],
'array_splice' => ['array', '&rw_input'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'mixed'],
'array_sum' => ['int|float', 'input'=>'array'],
'array_udiff' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'callable(mixed,mixed):int'],
'array_udiff\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_udiff_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_comp_func'=>'callable(mixed,mixed):int'],
'array_udiff_assoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_udiff_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'callable', 'key_comp_func'=>'callable(mixed,mixed):int'],
'array_udiff_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', 'arg5'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_uintersect' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'callable(mixed,mixed):int'],
'array_uintersect\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_uintersect_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'callable(mixed,mixed):int'],
'array_uintersect_assoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_uintersect_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'callable(mixed,mixed):int', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', 'arg5'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_udiff' => ['array', 'array'=>'array', '...arrays'=>'array', 'value_compare_func'=>'callable(mixed,mixed):int'],
'array_udiff_assoc' => ['array', 'array'=>'array', '...arrays'=>'array', 'value_compare_func'=>'callable(mixed,mixed):int'],
'array_udiff_uassoc' => ['array', 'array1'=>'array', '...arrays'=>'array', 'value_compare_func'=>'callable(mixed,mixed):int', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_uintersect' => ['array', 'array'=>'array', '...arrays'=>'array', 'value_compare_func'=>'callable(mixed,mixed):int'],
'array_uintersect_assoc' => ['array', 'array'=>'array', '...arrays'=>'array', 'value_compare_func'=>'callable(mixed,mixed):int'],
'array_uintersect_uassoc' => ['array', 'array'=>'array', '...arrays'=>'array', 'value_compare_func'=>'callable(mixed,mixed):int', 'key_compare_func'=>'callable(mixed,mixed):int'],
'array_unique' => ['array', 'array'=>'array', 'flags='=>'int'],
'array_unshift' => ['positive-int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'],
'array_values' => ['list<mixed>', 'input'=>'array'],
Expand Down
25 changes: 24 additions & 1 deletion src/Reflection/SignatureMap/Php8SignatureMapProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use function array_map;
use function count;
use function explode;
use function in_array;
use function is_string;
use function sprintf;
use function strtolower;
Expand Down Expand Up @@ -185,7 +186,29 @@ public function getMethodSignatures(string $className, string $methodName, ?Refl
public function getFunctionSignatures(string $functionName, ?string $className, ReflectionFunctionAbstract|null $reflectionFunction): array
{
$lowerName = strtolower($functionName);
if (!array_key_exists($lowerName, $this->map->functions)) {
if (
!array_key_exists($lowerName, $this->map->functions)
|| (
$className === null
// These functions have a variadic parameter in the middle. This is not well described by php8-stubs.
&& in_array(
$functionName,
[
'array_diff_uassoc',
'array_diff_ukey',
'array_intersect_uassoc',
'array_intersect_ukey',
'array_udiff',
'array_udiff_assoc',
'array_udiff_uassoc',
'array_uintersect',
'array_uintersect_assoc',
'array_uintersect_uassoc',
],
true,
)
)
Comment on lines +191 to +210
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to complicate it with the php8-stubs. The issue is that the stubs look like this:

/** @param array|callable $rest */
function array_intersect_uassoc(array $array, ...$rest): array
{
}

which then overrides the newly introduced stub. So I needed to skip them somehow. I suppose an alternative would be to skip php8-stubs where the last parameter is ...$rest, but I prefer this more explicit way.

) {
return $this->functionSignatureMapProvider->getFunctionSignatures($functionName, $className, $reflectionFunction);
}

Expand Down
25 changes: 24 additions & 1 deletion src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use PHPStan\Type\VerbosityLevel;
use function array_fill;
use function array_key_exists;
use function array_pop;
use function count;
use function implode;
use function is_string;
Expand Down Expand Up @@ -493,21 +494,43 @@ private function processArguments(
$originalParameters = $parametersAcceptor instanceof ResolvedFunctionVariant
? $parametersAcceptor->getOriginalParametersAcceptor()->getParameters()
: array_fill(0, count($parameters), null);
$expandedParameters = [];
$expandedOriginalParameters = [];
$parametersByName = [];
$originalParametersByName = [];
$unusedParametersByName = [];
$errors = [];
$expandVariadicParam = count($arguments) - count($parameters);
foreach ($parameters as $i => $parameter) {
$parametersByName[$parameter->getName()] = $parameter;
$originalParametersByName[$parameter->getName()] = $originalParameters[$i];
$originalParameter = $originalParameters[$i];
$originalParametersByName[$parameter->getName()] = $originalParameter;
$expandedParameters[] = $parameter;
$expandedOriginalParameters[] = $originalParameter;

if ($parameter->isVariadic()) {
// This handles variadic parameter followed by a another parameter. This cannot happen in user-defined
// code, but it does happen in built-in functions - e.g. array_intersect_uassoc. Currently, this condition
// doesn't handle the case where the following parameter is optional (it may not be needed).
if ($expandVariadicParam < 0 && array_key_exists($i + 1, $parameters)) {
$expandVariadicParam++;
array_pop($expandedParameters);
array_pop($expandedOriginalParameters);
} else {
for (; $expandVariadicParam > 0; --$expandVariadicParam) {
$expandedParameters[] = $parameter;
$expandedOriginalParameters[] = $originalParameter;
}
}

continue;
}

$unusedParametersByName[$parameter->getName()] = $parameter;
}

$parameters = $expandedParameters;
$originalParameters = $expandedOriginalParameters;
$newArguments = [];

$namedArgumentAlreadyOccurred = false;
Expand Down
7 changes: 5 additions & 2 deletions src/Type/TypehintHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,11 @@ public static function decideType(
}

if (
(!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed()))
&& $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes()
($type->isCallable()->yes() && $phpDocType->isCallable()->yes())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, I could broaden the type of the callbacks in function map to callable and then I wouldn't need this. I'm not sure what's better.

|| (
(!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed()))
&& $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes()
)
) {
$resultType = $phpDocType;
} else {
Expand Down
157 changes: 149 additions & 8 deletions stubs/arrayFunctions.stub
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,161 @@ function uksort(array &$array, callable $callback): bool
}

/**
* @template T of mixed
* @template TK of array-key
* @template TV of mixed
*
* @param array<T> $one
* @param array<T> $two
* @param callable(T, T): int $three
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TV, TV): int $value_compare_func
* @return array<TK, TV>
*/
function array_udiff(
array $one,
array $two,
callable $three
): int {}
array $array,
array ...$arrays,
callable $value_compare_func,
): array {}

/**
* @param array<array-key, mixed> $value
* @return ($value is __always-list ? true : false)
*/
function array_is_list(array $value): bool {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TK, TK): int $key_compare_func
* @return array<TK, TV>
*/
function array_diff_uassoc(
array $array,
array ...$arrays,
callable $key_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TK, TK): int $key_compare_func
* @return array<TK, TV>
*/
function array_diff_ukey(
array $array,
array ...$arrays,
callable $key_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TK, TK): int $key_compare_func
* @return array<TK, TV>
*/
function array_intersect_uassoc(
array $array,
array ...$arrays,
callable $key_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TK, TK): int $key_compare_func
* @return array<TK, TV>
*/
function array_intersect_ukey(
array $array,
array ...$arrays,
callable $key_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TV, TV): int $value_compare_func
* @return array<TK, TV>
*/
function array_udiff_assoc(
array $array,
array ...$arrays,
callable $value_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TV, TV): int $value_compare_func
* @return array<TK, TV>
*/
function array_uintersect_assoc(
array $array,
array ...$arrays,
callable $value_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array1
* @param array<TK, TV> ...$arrays
* @param callable(TV, TV): int $value_compare_func
* @param callable(TK, TK): int $key_compare_func
* @return array<TK, TV>
*/
function array_udiff_uassoc(
array $array1,
array ...$arrays,
callable $value_compare_func,
callable $key_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TV, TV): int $value_compare_func
* @param callable(TK, TK): int $key_compare_func
* @return array<TK, TV>
*/
function array_uintersect_uassoc(
array $array,
array ...$arrays,
callable $value_compare_func,
callable $key_compare_func
): array {}

/**
* @template TK of array-key
* @template TV of mixed
*
* @param array<TK, TV> $array
* @param array<TK, TV> ...$arrays
* @param callable(TV, TV): int $value_compare_func
* @return array<TK, TV>
*/
function array_uintersect(
array $array,
array ...$arrays,
callable $value_compare_func,
): array {}
Original file line number Diff line number Diff line change
Expand Up @@ -543,11 +543,12 @@ public function testParseAll(int $phpVersionId): void

foreach ($signatures as $signature) {
self::assertNotInstanceOf(ErrorType::class, $signature->getReturnType(), $functionName);
$optionalOcurred = false;
$optionalOccurred = false;
foreach ($signature->getParameters() as $parameter) {
// Required parameter can follow variadic parameter - e.g. array_intersect_uassoc.
if ($parameter->isOptional()) {
$optionalOcurred = true;
} elseif ($optionalOcurred) {
$optionalOccurred = $optionalOccurred || !$parameter->isVariadic();
} elseif ($optionalOccurred) {
$this->fail(sprintf('%s contains required parameter after optional.', $functionName));
}
self::assertNotInstanceOf(ErrorType::class, $parameter->getType(), sprintf('%s (parameter %s)', $functionName, $parameter->getName()));
Expand Down
Loading
Loading