diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 356d0b30be..fa2a2f2ef9 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -39,6 +39,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\ConditionalTypeForParameter; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -1049,7 +1050,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, $offsetType = new ConstantIntegerType($i); $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true); } - } else { + } elseif ($type->isConstantArray()->yes()) { for ($i = $sizeType->getMin();; $i++) { $offsetType = new ConstantIntegerType($i); $hasOffset = $type->hasOffsetValueType($offsetType); @@ -1060,7 +1061,11 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, } } - return $valueTypesBuilder->getArray(); + + $arrayType = $valueTypesBuilder->getArray(); + if ($arrayType->isIterableAtLeastOnce()->yes()) { + return $arrayType; + } } return null; @@ -1102,54 +1107,6 @@ private function specifyTypesForConstantBinaryExpression( )); } - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) >= 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) - && $constantType instanceof ConstantIntegerType - ) { - if ($constantType->getValue() < 0) { - return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); - } - - $argType = $scope->getType($exprNode->getArgs()[0]->value); - - if ($argType instanceof UnionType) { - $narrowed = $this->narrowUnionByArraySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr); - if ($narrowed !== null) { - return $narrowed; - } - } - - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - - if ($argType->isArray()->yes()) { - if ( - $context->truthy() - && $argType->isConstantArray()->yes() - && $constantType->isSuperTypeOf($argType->getArraySize())->no() - ) { - return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); - } - - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - $constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope); - if ($context->truthy() && $constArray !== null) { - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr); - } else { - $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr); - } - return $funcTypes->unionWith($valueTypes); - } - } - } - if ( !$context->null() && $exprNode instanceof FuncCall @@ -2137,6 +2094,70 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } $rightType = $scope->getType($rightExpr); + if ( + !$context->null() + && $unwrappedLeftExpr instanceof FuncCall + && count($unwrappedLeftExpr->getArgs()) >= 1 + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true) + && $rightType->isInteger()->yes() + ) { + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); + } + + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); + if ($isZero->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr); + + if ($context->truthy() && !$argType->isArray()->yes()) { + $newArgType = new UnionType([ + new ObjectType(Countable::class), + new ConstantArrayType([], []), + ]); + } else { + $newArgType = new ConstantArrayType([], []); + } + + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, $newArgType, $context, false, $scope, $rootExpr), + ); + } + + if ($argType instanceof UnionType) { + $narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $rootExpr); + if ($narrowed !== null) { + return $narrowed; + } + } + + if ($context->truthy()) { + if ($argType->isArray()->yes()) { + if ( + $argType->isConstantArray()->yes() + && $rightType->isSuperTypeOf($argType->getArraySize())->no() + ) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); + } + + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr); + $constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope); + if ($constArray !== null) { + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr), + ); + } elseif (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope, $rootExpr), + ); + } + + return $funcTypes; + } + } + } + if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall diff --git a/tests/PHPStan/Analyser/nsrt/bug-3993.php b/tests/PHPStan/Analyser/nsrt/bug-3993.php index e472a0d68c..38b1884bf5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3993.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3993.php @@ -13,7 +13,7 @@ public function doFoo($arguments) return; } - assertType('mixed~null', $arguments); + assertType('mixed~array{}|null', $arguments); array_shift($arguments); diff --git a/tests/PHPStan/Analyser/nsrt/count-type.php b/tests/PHPStan/Analyser/nsrt/count-type.php index 09114d90f8..54fb89c2c7 100644 --- a/tests/PHPStan/Analyser/nsrt/count-type.php +++ b/tests/PHPStan/Analyser/nsrt/count-type.php @@ -44,12 +44,12 @@ public function doFooBar( if (count($arr) == $maybeZero) { assertType('array', $arr); } else { - assertType('non-empty-array', $arr); + assertType('array', $arr); } if (count($arr) === $maybeZero) { assertType('array', $arr); } else { - assertType('non-empty-array', $arr); + assertType('array', $arr); } if (count($arr) == $negative) { @@ -65,3 +65,24 @@ public function doFooBar( } } + +/** + * @param \ArrayObject $obj + */ +function(\ArrayObject $obj): void { + if (count($obj) === 0) { + assertType('ArrayObject', $obj); + return; + } + + assertType('ArrayObject', $obj); +}; + +function($mixed): void { + if (count($mixed) === 0) { + assertType('array{}|Countable', $mixed); + return; + } + + assertType('mixed~array{}', $mixed); +}; diff --git a/tests/PHPStan/Analyser/nsrt/strlen-int-range.php b/tests/PHPStan/Analyser/nsrt/strlen-int-range.php index 7a4e217287..f66d50c140 100644 --- a/tests/PHPStan/Analyser/nsrt/strlen-int-range.php +++ b/tests/PHPStan/Analyser/nsrt/strlen-int-range.php @@ -113,3 +113,17 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, assertType('string', $s); } } + +/** + * @param int<1, max> $oneOrMore + * @param int<2, max> $twoOrMore + */ +function doFooBar(array $arr, int $oneOrMore, int $twoOrMore): void +{ + if (count($arr) == $oneOrMore) { + assertType('non-empty-array', $arr); + } + if (count($arr) === $twoOrMore) { + assertType('non-empty-array', $arr); + } +}