diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index a8fa5864c7..bf0eb42757 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; @@ -9,6 +10,8 @@ use PhpParser\Node\Expr\Error; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; @@ -74,6 +77,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { [$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $callbackArg->expr); + } elseif ($callbackArg instanceof String_) { + $itemVar = new Variable('item'); + $expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar)]); + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, null, $keyType, $expr); } } @@ -85,6 +92,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { [$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $callbackArg->expr); + } elseif ($callbackArg instanceof String_) { + $keyVar = new Variable('key'); + $expr = new FuncCall(new Name($callbackArg->value), [new Arg($keyVar)]); + [$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $keyVar, $keyType, $expr); } } @@ -96,6 +107,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { [$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, $callbackArg->params[1]->var ?? null, $keyType, $callbackArg->expr); + } elseif ($callbackArg instanceof String_) { + $itemVar = new Variable('item'); + $keyVar = new Variable('key'); + $expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]); + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, $keyVar, $keyType, $expr); } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 37b28051e8..e9a95f0246 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -281,6 +281,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-callables.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-string-callables.php'); if (PHP_VERSION_ID >= 70400) { yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-arrow-functions.php'); } diff --git a/tests/PHPStan/Analyser/data/array-filter-string-callables.php b/tests/PHPStan/Analyser/data/array-filter-string-callables.php new file mode 100644 index 0000000000..e143e3bc91 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-filter-string-callables.php @@ -0,0 +1,79 @@ + $list1 + * @param array $list2 + * @param array $list3 + */ +function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void +{ + $filtered1 = array_filter($list1, 'is_string'); + assertType('array{}', $filtered1); + + $filtered2 = array_filter($list2, 'is_string', ARRAY_FILTER_USE_KEY); + assertType('array{}', $filtered2); + + $filtered3 = array_filter($list3, 'is_string', ARRAY_FILTER_USE_BOTH); + assertType('array{}', $filtered3); +} + +/** + * @param array $map1 + * @param array $map2 + * @param array $map3 + */ +function filtersString(array $map1, array $map2, array $map3): void +{ + $filtered1 = array_filter($map1, 'is_string'); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, 'is_string', ARRAY_FILTER_USE_KEY); + assertType('array', $filtered2); + + $filtered3 = array_filter($map3, 'is_string', ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered3); +} + +/** + * @param array $list1 + */ +function nonCallableStringIsIgnored(array $list1): void +{ + $filtered1 = array_filter($list1, 'foo'); + assertType('array', $filtered1); +} + +/** + * @param array $map1 + * @param array $map2 + */ +function nonBuiltInFunctionsAreNotSupportedYetAndThereforeIgnored(array $map1, array $map2): void +{ + $filtered1 = array_filter($map1, '\ArrayFilter\isString'); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, '\ArrayFilter\Filters::isString'); + assertType('array', $filtered2); +} + +/** + * @param mixed $value + */ +function isString($value): bool +{ + return is_string($value); +} + +class Filters { + /** + * @param mixed $value + */ + public static function isString($value): bool + { + return is_string($value); + } +}