diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index 2aa17a5aea..fe1fb6304a 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -10,8 +10,17 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; class ArrayFilterFunctionReturnTypeReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension { @@ -32,6 +41,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keyType = $arrayArgType->getIterableKeyType(); $itemType = $arrayArgType->getIterableValueType(); + if ($callbackArg === null) { + return $this->removeFalsey($arrayArgType); + } + if ($flagArg === null && $callbackArg instanceof Closure && count($callbackArg->stmts) === 1) { $statement = $callbackArg->stmts[0]; if ($statement instanceof Return_ && $statement->expr !== null && count($callbackArg->params) > 0) { @@ -53,4 +66,42 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ArrayType($keyType, $itemType); } + private function removeFalsey(Type $type): Type + { + $falseyTypes = new UnionType([ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantArrayType([], []), + ]); + + if ($type instanceof ConstantArrayType) { + $keys = $type->getKeyTypes(); + $values = $type->getValueTypes(); + + foreach ($values as $offset => $value) { + if (!$falseyTypes->isSuperTypeOf($value)->yes()) { + continue; + } + + unset($keys[$offset], $values[$offset]); + } + + return new ConstantArrayType(array_values($keys), array_values($values)); + } + + $keyType = $type->getIterableKeyType(); + $valueType = $type->getIterableValueType(); + + $valueType = TypeCombinator::remove($valueType, $falseyTypes); + + if ($valueType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + return new ArrayType($keyType, $valueType); + } + } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 42a7457ad9..c07f669443 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -4116,6 +4116,30 @@ public function dataArrayFunctions(): array 'null', 'array_shift([])', ], + [ + 'array(null, \'\', 1)', + '$constantArrayWithFalseyValues', + ], + [ + 'array(2 => 1)', + '$constantTruthyValues', + ], + [ + 'array', + '$falsey', + ], + [ + 'array()', + 'array_filter($falsey)', + ], + [ + 'array', + '$withFalsey', + ], + [ + 'array', + 'array_filter($withFalsey)', + ], ]; } diff --git a/tests/PHPStan/Analyser/data/array-functions.php b/tests/PHPStan/Analyser/data/array-functions.php index a4cd3d08e9..8bd993d80b 100644 --- a/tests/PHPStan/Analyser/data/array-functions.php +++ b/tests/PHPStan/Analyser/data/array-functions.php @@ -41,6 +41,16 @@ 1 => new \stdClass(), ]; +$constantArrayWithFalseyValues = [null, '', 1]; + +$constantTruthyValues = array_filter($constantArrayWithFalseyValues); + +/** @var array $falsey */ +$falsey = doFoo(); + +/** @var array $withFalsey */ +$withFalsey = doFoo(); + /** @var array $generalStringKeys */ $generalStringKeys = doFoo();