diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 78ac65db296..f22b1789352 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -20,6 +20,8 @@ 'apcu_key_info' => ['hasSideEffects' => true], 'apcu_sma_info' => ['hasSideEffects' => true], 'apcu_store' => ['hasSideEffects' => true], + 'array_all' => ['hasSideEffects' => false], + 'array_any' => ['hasSideEffects' => false], 'array_change_key_case' => ['hasSideEffects' => false], 'array_chunk' => ['hasSideEffects' => false], 'array_column' => ['hasSideEffects' => false], @@ -32,6 +34,9 @@ 'array_diff_ukey' => ['hasSideEffects' => false], 'array_fill' => ['hasSideEffects' => false], 'array_fill_keys' => ['hasSideEffects' => false], + 'array_filter' => ['hasSideEffects' => false], + 'array_find' => ['hasSideEffects' => false], + 'array_find_key' => ['hasSideEffects' => false], 'array_flip' => ['hasSideEffects' => false], 'array_intersect' => ['hasSideEffects' => false], 'array_intersect_assoc' => ['hasSideEffects' => false], @@ -42,6 +47,7 @@ 'array_key_last' => ['hasSideEffects' => false], 'array_key_exists' => ['hasSideEffects' => false], 'array_keys' => ['hasSideEffects' => false], + 'array_map' => ['hasSideEffects' => false], 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], @@ -49,6 +55,7 @@ 'array_product' => ['hasSideEffects' => false], 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], + 'array_reduce' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 4e50a9f80f3..98133f25ef7 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -725,6 +725,8 @@ 'apcu_key_info' => ['hasSideEffects' => true], 'apcu_sma_info' => ['hasSideEffects' => true], 'apcu_store' => ['hasSideEffects' => true], + 'array_all' => ['hasSideEffects' => false], + 'array_any' => ['hasSideEffects' => false], 'array_change_key_case' => ['hasSideEffects' => false], 'array_chunk' => ['hasSideEffects' => false], 'array_column' => ['hasSideEffects' => false], @@ -737,6 +739,9 @@ 'array_diff_ukey' => ['hasSideEffects' => false], 'array_fill' => ['hasSideEffects' => false], 'array_fill_keys' => ['hasSideEffects' => false], + 'array_filter' => ['hasSideEffects' => false], + 'array_find' => ['hasSideEffects' => false], + 'array_find_key' => ['hasSideEffects' => false], 'array_first' => ['hasSideEffects' => false], 'array_flip' => ['hasSideEffects' => false], 'array_intersect' => ['hasSideEffects' => false], @@ -750,6 +755,7 @@ 'array_key_last' => ['hasSideEffects' => false], 'array_keys' => ['hasSideEffects' => false], 'array_last' => ['hasSideEffects' => false], + 'array_map' => ['hasSideEffects' => false], 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], @@ -757,6 +763,7 @@ 'array_product' => ['hasSideEffects' => false], 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], + 'array_reduce' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], diff --git a/tests/PHPStan/Analyser/nsrt/array-find-key.php b/tests/PHPStan/Analyser/nsrt/array-find-key.php index 5caf828f531..20ea054ab2d 100644 --- a/tests/PHPStan/Analyser/nsrt/array-find-key.php +++ b/tests/PHPStan/Analyser/nsrt/array-find-key.php @@ -1,26 +1,4 @@ - $array - * @param callable(mixed, array-key=): mixed $callback - * @return ?array-key - */ - function array_find_key(array $array, callable $callback) - { - foreach ($array as $key => $value) { - if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean - return $key; - } - } - - return null; - } - } - -} += 8.4 namespace ArrayFindKey { diff --git a/tests/PHPStan/Analyser/nsrt/array-find.php b/tests/PHPStan/Analyser/nsrt/array-find.php index f3b5b0b8222..62d5334f891 100644 --- a/tests/PHPStan/Analyser/nsrt/array-find.php +++ b/tests/PHPStan/Analyser/nsrt/array-find.php @@ -1,26 +1,4 @@ - $array - * @param callable(mixed, array-key=): mixed $callback - * @return mixed - */ - function array_find(array $array, callable $callback) - { - foreach ($array as $key => $value) { - if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean - return $value; - } - } - - return null; - } - } - -} += 8.4 namespace ArrayFind { diff --git a/tests/PHPStan/Command/data/file-without-errors.php b/tests/PHPStan/Command/data/file-without-errors.php index 08929907d3c..c6d0a42c3ac 100644 --- a/tests/PHPStan/Command/data/file-without-errors.php +++ b/tests/PHPStan/Command/data/file-without-errors.php @@ -1,3 +1,3 @@ analyse([__DIR__ . '/data/bug-11101.php'], [ + [ + 'Call to function array_filter() on a separate line has no effect.', + 13, + ], + [ + 'Call to function array_map() on a separate line has no effect.', + 14, + ], + [ + 'Call to function array_reduce() on a separate line has no effect.', + 15, + ], + ]); + } + + #[RequiresPhp('>= 8.4.0')] + public function testBug11101Php84(): void + { + $this->analyse([__DIR__ . '/data/bug-11101-php84.php'], [ + [ + 'Call to function array_find() on a separate line has no effect.', + 13, + ], + [ + 'Call to function array_find_key() on a separate line has no effect.', + 14, + ], + [ + 'Call to function array_any() on a separate line has no effect.', + 15, + ], + [ + 'Call to function array_all() on a separate line has no effect.', + 16, + ], + ]); + } + + public function testBug11101CustomPure(): void + { + $this->analyse([__DIR__ . '/data/bug-11101-custom-pure.php'], [ + [ + 'Call to function Bug11101CustomPure\pureWithImmediateCallback() on a separate line has no effect.', + 38, + ], + [ + 'Call to function Bug11101CustomPure\pureWithLaterCallback() on a separate line has no effect.', + 56, + ], + [ + 'Call to function Bug11101CustomPure\pureWithLaterCallback() on a separate line has no effect.', + 59, + ], + [ + 'Call to function Bug11101CustomPure\pureWithLaterCallback() on a separate line has no effect.', + 65, + ], + ]); + } + #[RequiresPhp('>= 8.5.0')] public function testPipeOperator(): void { diff --git a/tests/PHPStan/Rules/Functions/data/bug-11101-custom-pure.php b/tests/PHPStan/Rules/Functions/data/bug-11101-custom-pure.php new file mode 100644 index 00000000000..4c57f1bccf9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11101-custom-pure.php @@ -0,0 +1,68 @@ + $array + * @param callable(int): bool $callback + * @param-immediately-invoked-callable $callback + * @return array + */ +function pureWithImmediateCallback(array $array, callable $callback): array +{ + return array_filter($array, $callback); +} + +/** + * @phpstan-pure + * @param array $array + * @param callable(int): bool $callback + * @param-later-invoked-callable $callback + * @return array + */ +function pureWithLaterCallback(array $array, callable $callback): array +{ + return $array; +} + +class Foo +{ + + /** + * @param array $array + */ + public function testImmediateCallback(array $array, callable $callback): void + { + // Pure callback - should be reported + pureWithImmediateCallback($array, fn ($v) => $v > 5); + + // Impure callback - should NOT be reported + pureWithImmediateCallback($array, function ($v) { + echo $v; + return $v > 0; + }); + + // Unknown callback - should NOT be reported + pureWithImmediateCallback($array, $callback); + } + + /** + * @param array $array + */ + public function testLaterCallback(array $array, callable $callback): void + { + // Pure callback - should be reported + pureWithLaterCallback($array, fn ($v) => $v > 5); + + // Impure callback - should be reported (later-invoked, callback impurity doesn't matter at call site) + pureWithLaterCallback($array, function ($v) { + echo $v; + return $v > 0; + }); + + // Unknown callback - should be reported (later-invoked, callback impurity doesn't matter at call site) + pureWithLaterCallback($array, $callback); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11101-php84.php b/tests/PHPStan/Rules/Functions/data/bug-11101-php84.php new file mode 100644 index 00000000000..271178a8290 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11101-php84.php @@ -0,0 +1,19 @@ += 8.4 + +namespace Bug11101Php84; + +class Foo +{ + + /** + * @param array $array + */ + public function doFoo(array $array): void + { + array_find($array, fn ($v) => $v > 5); + array_find_key($array, fn ($v) => $v > 5); + array_any($array, fn ($v) => $v > 5); + array_all($array, fn ($v) => $v > 5); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11101.php b/tests/PHPStan/Rules/Functions/data/bug-11101.php new file mode 100644 index 00000000000..fe2a5f5fbb3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11101.php @@ -0,0 +1,37 @@ + $array + */ + public function doFoo(array $array): void + { + array_filter($array, 'is_string'); + array_map('is_string', $array); + array_reduce($array, function ($carry, $item) { + return $carry + $item; + }, 0); + } + + /** + * @param array $array + */ + public function doBar(array $array, callable $callback): void + { + // These should NOT be reported because the callback might be impure + array_filter($array, $callback); + array_map($callback, $array); + array_reduce($array, $callback, 0); + + // Impure closure should not be reported + array_filter($array, function ($v) { + echo $v; + return $v > 0; + }); + } + +}