diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 276d219dce..08e0c65f0c 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -244,12 +244,25 @@ public function specifyTypesInCondition( && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) && $leftType->isInteger()->yes() ) { + $argType = $scope->getType($expr->right->getArgs()[0]->value); + + if ($argType instanceof UnionType && $leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + + $narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr); + if ($narrowed !== null) { + return $narrowed; + } + } + if ( $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($context->truthy() && $argType->isArray()->maybe()) { $countables = []; if ($argType instanceof UnionType) { @@ -936,6 +949,43 @@ public function specifyTypesInCondition( return new SpecifiedTypes([], [], false, [], $rootExpr); } + private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes + { + if (count($countFuncCall->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); + } + + if ( + $isNormalCount->yes() + && $argType->isConstantArray()->yes() + ) { + $result = []; + foreach ($argType->getTypes() as $innerType) { + $arraySize = $innerType->getArraySize(); + $isSize = $sizeType->isSuperTypeOf($arraySize); + if ($context->truthy()) { + if ($isSize->no()) { + continue; + } + } + if ($context->falsey()) { + if (!$isSize->yes()) { + continue; + } + } + + $result[] = $innerType; + } + + return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr); + } + + return null; + } + private function specifyTypesForConstantBinaryExpression( Expr $exprNode, ConstantScalarType $constantType, @@ -986,36 +1036,11 @@ private function specifyTypesForConstantBinaryExpression( ) { $argType = $scope->getType($exprNode->getArgs()[0]->value); - if (count($exprNode->getArgs()) === 1) { - $isNormalCount = TrinaryLogic::createYes(); - } else { - $mode = $scope->getType($exprNode->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); - } - - if ( - $isNormalCount->yes() - && $argType instanceof UnionType - ) { - $result = []; - foreach ($argType->getTypes() as $innerType) { - $arraySize = $innerType->getArraySize(); - $isSize = $constantType->isSuperTypeOf($arraySize); - if ($context->truthy()) { - if ($isSize->no()) { - continue; - } - } - if ($context->falsey()) { - if (!$isSize->yes()) { - continue; - } - } - - $result[] = $innerType; + if ($argType instanceof UnionType) { + $narrowed = $this->narrowUnionByArraySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr); + if ($narrowed !== null) { + return $narrowed; } - - return $this->create($exprNode->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr); } if ($context->truthy() || $constantType->getValue() === 0) { @@ -1025,6 +1050,13 @@ private function specifyTypesForConstantBinaryExpression( } if ($argType->isArray()->yes()) { + if (count($exprNode->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($exprNode->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); + } + $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); if ($isNormalCount->yes() && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-3558.php b/tests/PHPStan/Analyser/nsrt/bug-3558.php index 8c2e198013..2f71cf07d2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3558.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3558.php @@ -28,6 +28,6 @@ function (): void { } if(count($idGroups) > 1){ - assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}|array{1}', $idGroups); + assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}', $idGroups); } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php new file mode 100644 index 0000000000..08a220a624 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -0,0 +1,113 @@ + 0) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) > 1) { + assertType("array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) >= 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + public function arraySmallerThan(): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + if (count($x) < 1) { + assertType("array{}", $x); + } else { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) <= 1) { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + public function intUnionCount(): void + { + $count = 1; + if (rand(0, 1)) { + $count++; + } + + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType('1|2', $count); + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + if (count($x) >= $count) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + /** + * @param int<1,2> $count + */ + public function intRangeCount($count): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + if (count($x) >= $count) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +}