diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index 8f9f5616f3..55e4843f36 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -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()) + || ( + (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) + && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + ) ) { $resultType = $phpDocType; } else { diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub index 25c73249ec..a638e47380 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -50,20 +50,163 @@ function uksort(array &$array, callable $callback): bool } /** - * @template T of mixed + * @template TV of mixed + * @template TK of mixed * - * @param array $one - * @param array $two - * @param callable(T, T): int $three + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array */ function array_udiff( array $one, array $two, callable $three -): int {} +): array {} /** * @param array $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 $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_diff_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * @return array + */ +function array_intersect_uassoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TK, TK): int $three + * + * @return array + */ +function array_intersect_ukey( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * + * @return array + */ +function array_udiff_assoc( + array $one, + array $two, + callable $three +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array + */ +function array_udiff_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_uintersect_assoc( + array $one, + array $two, + callable $three, +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @param callable(TK, TK): int $four + * @return array + */ +function array_uintersect_uassoc( + array $one, + array $two, + callable $three, + callable $four +): array {} + +/** + * @template TK of array-key + * @template TV of mixed + * + * @param array $one + * @param array $two + * @param callable(TV, TV): int $three + * @return array + */ +function array_uintersect( + array $one, + array $two, + callable $three, +): array {} diff --git a/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php b/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php new file mode 100644 index 0000000000..5dc721664e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_diff_intersect_callbacks.php @@ -0,0 +1,127 @@ +, null given.', + 'Parameter #1 $arr1 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.', 20, ], [ - 'Parameter #2 $arr2 of function array_udiff expects array, null given.', + 'Parameter #2 $arr2 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.', 21, ], [ @@ -1716,6 +1716,164 @@ public function testCountArrayShift(): void $this->analyse([__DIR__ . '/data/count-array-shift.php'], $errors); } + public function testArrayDiffUassoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_diff_uassoc.php'], [ + [ + 'Parameter #3 $data_comp_func of function array_diff_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $data_comp_func of function array_diff_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayDiffUkey(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_diff_ukey.php'], [ + [ + 'Parameter #3 $key_comp_func of function array_diff_ukey expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_comp_func of function array_diff_ukey expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayIntersectUassoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_intersect_uassoc.php'], [ + [ + 'Parameter #3 $key_compare_func of function array_intersect_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_compare_func of function array_intersect_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayIntersectUkey(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_intersect_ukey.php'], [ + [ + 'Parameter #3 $key_compare_func of function array_intersect_ukey expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_compare_func of function array_intersect_ukey expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayUdiffAssoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_udiff_assoc.php'], [ + [ + 'Parameter #3 $key_comp_func of function array_udiff_assoc expects callable(1|2, 1|2): int, Closure(string, string): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $key_comp_func of function array_udiff_assoc expects callable(1|2|3|4|5, 1|2|3|4|5): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayUdiffUasssoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_udiff_uassoc.php'], [ + [ + 'Parameter #3 $data_comp_func of function array_udiff_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 28, + ], + [ + 'Parameter #4 $key_comp_func of function array_udiff_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 31, + ], + [ + 'Parameter #3 $data_comp_func of function array_udiff_uassoc expects callable(1|2|3|4|5, 1|2|3|4|5): int, Closure(string, string): int<-1, 1> given.', + 39, + ], + [ + 'Parameter #4 $key_comp_func of function array_udiff_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 42, + ], + ]); + } + + public function testArrayUintersectAssoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_uintersect_assoc.php'], [ + [ + 'Parameter #3 $data_compare_func of function array_uintersect_assoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $data_compare_func of function array_uintersect_assoc expects callable(1|2|3|4, 1|2|3|4): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testArrayUintersectUassoc(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_uintersect_uassoc.php'], [ + [ + 'Parameter #3 $data_compare_func of function array_uintersect_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 28, + ], + [ + 'Parameter #4 $key_compare_func of function array_uintersect_uassoc expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 31, + ], + [ + 'Parameter #3 $data_compare_func of function array_uintersect_uassoc expects callable(1|2|3|4|5, 1|2|3|4|5): int, Closure(string, string): int<-1, 1> given.', + 39, + ], + [ + 'Parameter #4 $key_compare_func of function array_uintersect_uassoc expects callable(0|1|2|3, 0|1|2|3): int, Closure(string, string): int<-1, 1> given.', + 42, + ], + ]); + } + + public function testArrayUintersect(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/array_uintersect.php'], [ + [ + 'Parameter #3 $data_compare_func of function array_uintersect expects callable(\'a\'|\'b\'|\'c\'|\'d\', \'a\'|\'b\'|\'c\'|\'d\'): int, Closure(int, int): int<-1, 1> given.', + 22, + ], + [ + 'Parameter #3 $data_compare_func of function array_uintersect expects callable(1|2|3|4, 1|2|3|4): int, Closure(string, string): int<-1, 1> given.', + 30, + ], + ]); + } + + public function testBug7707(): void + { + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-7707.php'], []); + } + public function testNoNamedArguments(): void { if (PHP_VERSION_ID < 80000) { diff --git a/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php new file mode 100644 index 0000000000..257fc17c85 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_diff_uassoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php b/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php new file mode 100644 index 0000000000..8a98630a88 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_diff_ukey.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_diff_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php new file mode 100644 index 0000000000..9f8ba1bfa3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_uassoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php new file mode 100644 index 0000000000..4d81086d24 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_intersect_ukey.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_intersect_ukey( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php b/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php new file mode 100644 index 0000000000..7827734e59 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_udiff_assoc.php @@ -0,0 +1,33 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_udiff_assoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php new file mode 100644 index 0000000000..f4767621c2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_udiff_uassoc.php @@ -0,0 +1,45 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_udiff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + }, +); + +array_udiff_uassoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_udiff_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect.php b/tests/PHPStan/Rules/Functions/data/array_uintersect.php new file mode 100644 index 0000000000..dbe5307a82 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect.php @@ -0,0 +1,33 @@ + $b; + } +); + +array_uintersect( + [1, 2, 3], + [1, 2, 3, 4], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect( + ['a', 'b'], + ['c', 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect( + [1, 2, 3], + [1, 2, 3, 4], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php b/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php new file mode 100644 index 0000000000..e410f3827e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect_assoc.php @@ -0,0 +1,33 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + [1, 2, 3], + [1, 2, 3, 4], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_assoc( + [1, 2, 3], + [1, 2, 3, 4], + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php b/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php new file mode 100644 index 0000000000..f079aec189 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_uintersect_uassoc.php @@ -0,0 +1,45 @@ + 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); + +array_uintersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + }, +); + +array_uintersect_uassoc( + ['a' => 'a', 'b' => 'b'], + ['c' => 'c', 'd' => 'd'], + static function (int $a, int $b): int { + return $a <=> $b; + }, + static function (int $a, int $b): int { + return $a <=> $b; + } +); + +array_uintersect_uassoc( + [1, 2, 3], + [1, 2, 4, 5], + static function (string $a, string $b): int { + return $a <=> $b; + }, + static function (string $a, string $b): int { + return $a <=> $b; + } +); diff --git a/tests/PHPStan/Rules/Functions/data/bug-7707.php b/tests/PHPStan/Rules/Functions/data/bug-7707.php new file mode 100644 index 0000000000..1fc7bbab6c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7707.php @@ -0,0 +1,98 @@ + 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_diff_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_intersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_intersect_ukey( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_udiff_assoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + } + )); + + var_dump(array_udiff_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + }, + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_uintersect_assoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + } + )); + + var_dump(array_uintersect_uassoc( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + }, + // keys comparison + static function (string $a, string $b): int { + return $a <=> $b; + } + )); + + var_dump(array_uintersect( + ['a' => 1, 'b' => 2], + ['c' => 1, 'd' => 2], + // values comparison + static function (int $a, int $b): int { + return $a <=> $b; + } + )); + } +}