diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 78ac65db296..630783cca68 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], @@ -237,6 +244,8 @@ 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], 'popen' => ['hasSideEffects' => true], + 'preg_replace_callback' => ['hasSideEffects' => false], + 'preg_replace_callback_array' => ['hasSideEffects' => false], 'readfile' => ['hasSideEffects' => true], 'rename' => ['hasSideEffects' => true], 'rewind' => ['hasSideEffects' => true], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 7a5515b967d..ced46d1808b 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], @@ -1616,6 +1623,8 @@ 'preg_last_error' => ['hasSideEffects' => true], 'preg_last_error_msg' => ['hasSideEffects' => true], 'preg_quote' => ['hasSideEffects' => false], + 'preg_replace_callback' => ['hasSideEffects' => false], + 'preg_replace_callback_array' => ['hasSideEffects' => false], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], 'quoted_printable_decode' => ['hasSideEffects' => false], diff --git a/tests/PHPStan/Analyser/data/discussion-7124.php b/tests/PHPStan/Analyser/data/discussion-7124.php index 29a8d1f5f50..24fe1f28eb6 100644 --- a/tests/PHPStan/Analyser/data/discussion-7124.php +++ b/tests/PHPStan/Analyser/data/discussion-7124.php @@ -33,30 +33,30 @@ function filter(array $array, ?callable $callback = null, int $mode = ARRAY_FILT function () { // This one does fail, as both the value + key is asked and the key + value is used - filter( + echo count(filter( [false, true, false], static fn (int $key, bool $value): bool => 0 === $key % 2 && $value, mode: ARRAY_FILTER_USE_BOTH, - ); + )); // This one should fail, as both the value + key is asked but only the key is used - filter( + echo count(filter( [false, true, false], static fn (int $key): bool => 0 === $key % 2, mode: ARRAY_FILTER_USE_BOTH, - ); + )); // This one should fail, as only the key is asked but the value is used - filter( + echo count(filter( [false, true, false], static fn (bool $value): bool => $value, mode: ARRAY_FILTER_USE_KEY, - ); + )); // This one should fail, as only the value is asked but the key is used - filter( + echo count(filter( [false, true, false], static fn (int $key): bool => 0 === $key % 2, mode: 0, - ); + )); }; diff --git a/tests/PHPStan/Command/data/file-without-errors.php b/tests/PHPStan/Command/data/file-without-errors.php index 08929907d3c..48196273364 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-12224.php'], []); } + public function testBug11101(): void + { + $this->analyse([__DIR__ . '/data/bug-11101.php'], [ + [ + 'Call to function array_filter() on a separate line has no effect.', + 8, + ], + [ + 'Call to function array_map() on a separate line has no effect.', + 9, + ], + [ + 'Call to function array_reduce() on a separate line has no effect.', + 10, + ], + [ + 'Call to function array_filter() on a separate line has no effect.', + 13, + ], + [ + 'Call to function preg_replace_callback() on a separate line has no effect.', + 14, + ], + [ + 'Call to function preg_replace_callback_array() 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_any() on a separate line has no effect.', + 8, + ], + [ + 'Call to function array_all() on a separate line has no effect.', + 9, + ], + [ + 'Call to function array_any() on a separate line has no effect.', + 31, + ], + ]); + } + + public function testBug11101PureClosure(): void + { + $this->analyse([__DIR__ . '/data/bug-11101-pure-closure.php'], [ + [ + 'Call to function array_map() on a separate line has no effect.', + 17, + ], + ]); + } + public function testBug4455(): void { require_once __DIR__ . '/data/bug-4455.php'; 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..9130eb9f666 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11101-php84.php @@ -0,0 +1,33 @@ + $args */ + public function test(array $args): void + { + $pureFx = static function (string $v): void {}; + + assert(isset($args[0])); + + $pureFx($args[0]); + + array_map($pureFx, $args); + } + +} 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..f0d7cf3c753 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11101.php @@ -0,0 +1,38 @@ + 'strtoupper'], 'abc'); +} + +function doBar(array $array, callable $cb): void +{ + // impure callbacks - NOT reported + array_filter($array, function ($v) { + echo $v; + return true; + }); + array_map(function ($v) { + echo $v; + return $v; + }, $array); + array_reduce($array, function ($carry, $item) { + echo $item; + return $carry; + }, 0); + array_map('printf', $array); + + // unknown callable purity - NOT reported + array_map($cb, $array); + array_filter($array, $cb); +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index 8287809407a..8cfc8f37341 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -276,6 +276,12 @@ public function testBug12048(): void $this->analyse([__DIR__ . '/data/bug-12048.php'], []); } + public function testBug11100(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11100.php'], []); + } + public function testBug12224(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Pure/data/bug-11100.php b/tests/PHPStan/Rules/Pure/data/bug-11100.php new file mode 100644 index 00000000000..a0e3f5f3848 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-11100.php @@ -0,0 +1,30 @@ += 8.0 + +namespace Bug11100; + +final class FooClass +{ + /** + * @param array $getParams + * + * @phpstan-pure + */ + public function build(array $getParams, string $trailingSlash, bool $useSlashSeparator): string + { + if ($getParams === []) { + return ''; + } + + if ($useSlashSeparator) { + return '/' . implode('/', array_map( + static function (string $key, string|int $value): string { + return rawurlencode($key) . '/' . rawurlencode((string) $value); + }, + array_keys($getParams), + $getParams + )) . $trailingSlash; + } + + return $trailingSlash . '?' . http_build_query($getParams); + } +}