diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index e5bd2bbd57..f3707eeef8 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; @@ -1037,7 +1038,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); @@ -1048,7 +1049,11 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, } } - return $valueTypesBuilder->getArray(); + + $arrayType = $valueTypesBuilder->getArray(); + if ($arrayType->isIterableAtLeastOnce()->yes()) { + return $arrayType; + } } return null; @@ -1056,17 +1061,13 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, private function specifyTypesForConstantBinaryExpression( Expr $exprNode, - Type $constantType, + ConstantScalarType $constantType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr, ): ?SpecifiedTypes { - $scalarValues = $constantType->getConstantScalarValues(); - if (count($scalarValues) !== 1) { - return null; - } - $constValue = $scalarValues[0]; + $constValue = $constantType->getValue(); if (!$context->null() && $constValue === false) { $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); @@ -1096,103 +1097,18 @@ 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 - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['strlen', 'mb_strlen'], true) - && $constantType instanceof ConstantIntegerType - ) { - if ($constantType->getValue() < 0) { - return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); - } - - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType->isString()->yes()) { - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - - $accessory = new AccessoryNonEmptyStringType(); - if ($constantType->getValue() >= 2) { - $accessory = new AccessoryNonFalsyStringType(); - } - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $accessory, $newContext, false, $scope, $rootExpr); - - return $funcTypes->unionWith($valueTypes); - } - } - - } - return null; } private function specifyTypesForConstantStringBinaryExpression( Expr $exprNode, - Type $constantType, + ConstantStringType $constantType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr, ): ?SpecifiedTypes { - $scalarValues = $constantType->getConstantScalarValues(); - if (count($scalarValues) !== 1 || !is_string($scalarValues[0])) { - return null; - } - $constantStringValue = $scalarValues[0]; + $constantStringValue = $constantType->getValue(); if ( $context->truthy() @@ -2165,14 +2081,111 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } - if ($rightType->isInteger()->yes() || $rightType->isString()->yes()) { - $types = null; - foreach ($rightType->getFiniteTypes() as $finiteType) { - if ($finiteType->isString()->yes()) { - $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $finiteType, $context, $scope, $rootExpr); + 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 { - $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedLeftExpr, $finiteType, $context, $scope, $rootExpr); + $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->null() + && $unwrappedLeftExpr instanceof FuncCall + && count($unwrappedLeftExpr->getArgs()) === 1 + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], 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); + } + + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); + if ($isZero->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr); + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), $context, false, $scope, $rootExpr), + ); + } + + if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr); + + $accessory = new AccessoryNonEmptyStringType(); + if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($rightType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + $valueTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, $accessory, $context, false, $scope, $rootExpr); + + return $funcTypes->unionWith($valueTypes); } + } + } + + if ($rightType->isString()->yes()) { + $types = null; + foreach ($rightType->getConstantStrings() as $constantString) { + $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $rootExpr); + if ($specifiedType === null) { continue; } diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index c2b21e92c4..37fbd12ccf 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -1283,7 +1283,7 @@ public function dataCondition(): iterable ), ), [ - '$foo' => 'non-empty-string', + '$foo' => "string & ~''", 'strlen($foo)' => '~0', ], [ 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..b978421b0f 100644 --- a/tests/PHPStan/Analyser/nsrt/strlen-int-range.php +++ b/tests/PHPStan/Analyser/nsrt/strlen-int-range.php @@ -69,10 +69,10 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, } if (strlen($s) == $oneOrMore) { - assertType('string', $s); // could be non-empty-string + assertType('non-empty-string', $s); } if (strlen($s) === $oneOrMore) { - assertType('string', $s); // could be non-empty-string + assertType('non-empty-string', $s); } if (strlen($s) == $tenOrEleven) { @@ -113,3 +113,31 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, assertType('string', $s); } } + +/** + * @param int<1, max> $oneOrMore + * @param int<2, max> $twoOrMore + */ +function doFooBar(string $s, array $arr, int $oneOrMore, int $twoOrMore): void +{ + if (strlen($s) == $oneOrMore) { + assertType('non-empty-string', $s); + } + if (strlen($s) === $oneOrMore) { + assertType('non-empty-string', $s); + } + + if (strlen($s) == $twoOrMore) { + assertType('non-falsy-string', $s); + } + if (strlen($s) === $twoOrMore) { + assertType('non-falsy-string', $s); + } + + if (count($arr) == $oneOrMore) { + assertType('non-empty-array', $arr); + } + if (count($arr) === $oneOrMore) { + assertType('non-empty-array', $arr); + } +}