diff --git a/resources/functionMap_bleedingEdge.php b/resources/functionMap_bleedingEdge.php index 11dd4fa773..695f5d862c 100644 --- a/resources/functionMap_bleedingEdge.php +++ b/resources/functionMap_bleedingEdge.php @@ -11,6 +11,18 @@ 'bcpowmod' => ['numeric-string|null', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'modulus'=>'string', 'scale='=>'int'], 'bcsqrt' => ['numeric-string', 'operand'=>'numeric-string', 'scale='=>'int'], 'bcsub' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], + 'array_filter' => ['array', 'input'=>'array', 'callback='=>'pure-callable(mixed,mixed):bool|pure-callable(mixed):bool', 'flag='=>'ARRAY_FILTER_USE_BOTH|ARRAY_FILTER_USE_KEY'], + 'array_reduce' => ['mixed', 'input'=>'array', 'callback'=>'pure-callable(mixed,mixed):mixed', 'initial='=>'mixed'], + 'array_udiff_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_comp_func'=>'pure-callable(mixed,mixed):int'], + 'array_udiff_assoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'], + 'array_udiff_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'pure-callable', 'key_comp_func'=>'pure-callable(mixed,mixed):int'], + 'array_udiff_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', 'arg5'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'], + 'array_uintersect' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'pure-callable(mixed,mixed):int'], + 'array_uintersect\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'], + 'array_uintersect_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'pure-callable(mixed,mixed):int'], + 'array_uintersect_assoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'], + 'array_uintersect_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'pure-callable(mixed,mixed):int', 'key_compare_func'=>'pure-callable(mixed,mixed):int'], + 'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', 'arg5'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'], 'Closure::bind' => ['Closure', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|class-string|\'static\'|null'], 'Closure::bindTo' => ['Closure', 'new'=>'?object', 'newscope='=>'object|class-string|\'static\'|null'], 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|2|3|4', 'destination='=>'string', 'extra_headers='=>'string'], diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index 2df15b78f0..e67fce5728 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -10,6 +10,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use function count; use function in_array; use function sprintf; @@ -21,10 +22,12 @@ final class CallToFunctionStatementWithoutSideEffectsRule implements Rule private const SIDE_EFFECT_FLIP_PARAMETERS = [ // functionName => [name, pos, testName] + 'array_filter' => ['callback', 1, 'isPure'], + 'array_map' => ['callback', 0, 'isPure'], + 'array_reduce' => ['callback', 1, 'isPure'], 'print_r' => ['return', 1, 'isTruthy'], 'var_export' => ['return', 1, 'isTruthy'], 'highlight_string' => ['return', 1, 'isTruthy'], - ]; public const PHPSTAN_TESTING_FUNCTIONS = [ @@ -76,6 +79,22 @@ public function processNode(Node $node, Scope $scope): array $sideEffectFlipped = false; $hasNamedParameter = false; $checker = [ + 'isPure' => static function (Type $type) use ($scope) { + if ($type->isCallable()->no()) { + return false; + } + $callableParametersAcceptors = $type->getCallableParametersAcceptors($scope); + if (count($callableParametersAcceptors) === 0) { + return false; + } + foreach ($callableParametersAcceptors as $callableParametersAcceptor) { + if (!$callableParametersAcceptor->isPure()->yes()) { + return false; + } + } + + return true; + }, 'isNotNull' => static fn (Type $type) => $type->isNull()->no(), 'isTruthy' => static fn (Type $type) => $type->toBoolean()->isTrue()->yes(), ][$testName]; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php index 85c1f400ee..8b300eccd7 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -32,6 +32,22 @@ public function testRule(): void 'Call to function print_r() on a separate line has no effect.', 26, ], + [ + 'Call to function array_filter() on a separate line has no effect.', + 29, + ], + [ + 'Call to function array_map() on a separate line has no effect.', + 33, + ], + [ + 'Call to function array_reduce() on a separate line has no effect.', + 37, + ], + [ + 'Call to function array_reduce() on a separate line has no effect.', + 40, + ], ]); if (PHP_VERSION_ID < 80000) { @@ -51,6 +67,26 @@ public function testRule(): void 'Call to function highlight_string() on a separate line has no effect.', 21, ], + [ + 'Call to function array_filter() on a separate line has no effect.', + 22, + ], + [ + 'Call to function array_filter() on a separate line has no effect.', + 23, + ], + [ + 'Call to function array_map() on a separate line has no effect.', + 24, + ], + [ + 'Call to function array_map() on a separate line has no effect.', + 25, + ], + [ + 'Call to function array_reduce() on a separate line has no effect.', + 26, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php index 497de17310..667c46c076 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php @@ -19,6 +19,11 @@ public function noEffect(string $url, $resourceOrNull) var_export([], return: true); print_r([], return: true); highlight_string($url, return: true); + array_filter([], callback: 'is_string'); + array_filter([], is_string(...)); + array_map(array: [], callback: 'is_string'); + array_map(is_string(...), []); + array_reduce([], callback: fn($carry, $item) => $carry + $item); } /** @@ -31,6 +36,14 @@ public function hasSideEffect(string $url, $resource) var_export(value: []); print_r(value: []); highlight_string($url); + $callback = rand() === 0 ? is_string(...) : var_dump(...); + array_filter([], callback: $callback); + array_filter([], callback: 'var_dump'); + array_filter([], var_dump(...)); + array_map(array: [], callback: $callback); + array_map(array: [], callback: 'var_dump'); + array_map(var_dump(...), array: []); + array_reduce([], callback: $callback); } } diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects.php index d5c31a3af4..4deb250410 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects.php @@ -7,7 +7,7 @@ class Foo { - public function doFoo(string $url) + public function doFoo(string $url, $mixed) { printf('%s', 'test'); sprintf('%s', 'test'); @@ -24,6 +24,22 @@ public function doFoo(string $url) var_export([], true); print_r([]); print_r([], true); + $callback = rand() === 0 ? 'is_string' : 'var_dump'; + array_filter([], 'var_dump'); + array_filter([], 'is_string'); + array_filter([], $mixed); + array_filter([], $callback); + array_map('var_dump', []); + array_map('is_string', []); + array_map($mixed, []); + array_map($callback, []); + array_reduce([], 'var_dump'); + array_reduce([], 'is_string'); + array_reduce([], $mixed); + array_reduce([], $callback); + array_reduce([], function ($carry, $item) { + return $carry + $item; + }); } public function doBar(string $s)