From 9534154372dd59bc5b6bc6a22be72f83e1865980 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 4 Dec 2024 23:26:10 +0100 Subject: [PATCH 1/7] Improve `count()` narrowing of constant arrays --- src/Analyser/TypeSpecifier.php | 102 ++++++++++----------- tests/PHPStan/Analyser/nsrt/bug-4700.php | 2 +- tests/PHPStan/Analyser/nsrt/count-type.php | 23 +++++ 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3c1dc42d5f..1cf3c0022e 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -272,22 +272,20 @@ public function specifyTypesInCondition( ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType instanceof UnionType) { - $sizeType = null; - if ($leftType instanceof ConstantIntegerType) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); - } - } elseif ($leftType instanceof IntegerRangeType) { - $sizeType = $leftType; + $sizeType = null; + if ($leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); } + } elseif ($leftType instanceof IntegerRangeType) { + $sizeType = $leftType; + } - $narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $expr); - if ($narrowed !== null) { - return $narrowed; - } + $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); } if ( @@ -1046,66 +1044,52 @@ public function specifyTypesInCondition( return (new SpecifiedTypes([], []))->setRootExpr($expr); } - private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes + private function specifyTypesForCountFuncCall(FuncCall $countFuncCall, Type $type, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes { if ($sizeType === null) { return null; } - if (count($countFuncCall->getArgs()) === 1) { - $isNormalCount = TrinaryLogic::createYes(); - } else { - $mode = $scope->getType($countFuncCall->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate()); - } - if ( - $isNormalCount->yes() - && $argType->isConstantArray()->yes() + $this->isFuncCallWithNormalCount($countFuncCall, $scope)->yes() + && $type->isConstantArray()->yes() ) { - $result = []; - foreach ($argType->getTypes() as $innerType) { - $arraySize = $innerType->getArraySize(); + $resultType = TypeTraverser::map($type, function (Type $type, callable $traverse) use ($sizeType, $context) { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $arraySize = $type->getArraySize(); $isSize = $sizeType->isSuperTypeOf($arraySize); if ($context->truthy()) { if ($isSize->no()) { - continue; + return new NeverType(); } - $constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope); + $constArray = $this->turnListIntoConstantArray($type, $sizeType); if ($constArray !== null) { - $innerType = $constArray; + $type = $constArray; } } if ($context->falsey()) { if (!$isSize->yes()) { - continue; + return new NeverType(); } } - $result[] = $innerType; - } + return $type; + }); - return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, $scope)->setRootExpr($rootExpr); + return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr); } return null; } - private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type + private function turnListIntoConstantArray(Type $type, Type $sizeType): ?Type { - $argType = $scope->getType($countFuncCall->getArgs()[0]->value); - - if (count($countFuncCall->getArgs()) === 1) { - $isNormalCount = TrinaryLogic::createYes(); - } else { - $mode = $scope->getType($countFuncCall->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate()); - } - if ( - $isNormalCount->yes() - && $type->isList()->yes() + $type->isList()->yes() && $sizeType instanceof ConstantIntegerType && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT ) { @@ -1119,8 +1103,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, } if ( - $isNormalCount->yes() - && $type->isList()->yes() + $type->isList()->yes() && $sizeType instanceof IntegerRangeType && $sizeType->getMin() !== null ) { @@ -1157,6 +1140,18 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, return null; } + private function isFuncCallWithNormalCount(FuncCall $countFuncCall, Scope $scope): TrinaryLogic + { + $argType = $scope->getType($countFuncCall->getArgs()[0]->value); + + if (count($countFuncCall->getArgs()) === 1) { + return TrinaryLogic::createYes(); + } + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + + return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate()); + } + private function specifyTypesForConstantBinaryExpression( Expr $exprNode, Type $constantType, @@ -2186,11 +2181,9 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty ); } - if ($argType instanceof UnionType) { - $narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr); - if ($narrowed !== null) { - return $narrowed; - } + $specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + return $specifiedTypes; } if ($context->truthy()) { @@ -2203,7 +2196,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); - $constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope); + $isNormalCount = $this->isFuncCallWithNormalCount($unwrappedLeftExpr, $scope); + $constArray = $isNormalCount->yes() ? $this->turnListIntoConstantArray($argType, $rightType) : null; if ($constArray !== null) { return $funcTypes->unionWith( $this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, $scope)->setRootExpr($expr), diff --git a/tests/PHPStan/Analyser/nsrt/bug-4700.php b/tests/PHPStan/Analyser/nsrt/bug-4700.php index 078ea41b12..202aca765c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4700.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4700.php @@ -40,7 +40,7 @@ function(array $array, int $count): void { if (isset($array['d'])) $a[] = $array['d']; if (isset($array['e'])) $a[] = $array['e']; if (count($a) > $count) { - assertType('int<1, 5>', count($a)); + assertType('int<2, 5>', count($a)); assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { assertType('0', count($a)); diff --git a/tests/PHPStan/Analyser/nsrt/count-type.php b/tests/PHPStan/Analyser/nsrt/count-type.php index 54fb89c2c7..859718b615 100644 --- a/tests/PHPStan/Analyser/nsrt/count-type.php +++ b/tests/PHPStan/Analyser/nsrt/count-type.php @@ -64,6 +64,29 @@ public function doFooBar( } } + /** @param array{0: string, 1?: string} $arr */ + public function doBar(array $arr): void + { + if (count($arr) <= 1) { + assertType('1', count($arr)); + return; + } + + assertType('2', count($arr)); + assertType('array{string, string}', $arr); + } + + /** @param array{0: string, 1?: string} $arr */ + public function doBaz(array $arr): void + { + if (count($arr) > 1) { + assertType('2', count($arr)); + assertType('array{string, string}', $arr); + } + + assertType('1|2', count($arr)); + } + } /** From a12aaaced3dccb9784e29d2a2b2b806c12ed5eff Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 5 Dec 2024 15:51:50 +0100 Subject: [PATCH 2/7] Consider int range offset in truthy context --- src/Analyser/TypeSpecifier.php | 21 ++++++--------------- tests/PHPStan/Analyser/nsrt/bug-4700.php | 6 +++--- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 1cf3c0022e..d8e6712e4d 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -280,7 +280,7 @@ public function specifyTypesInCondition( $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); } } elseif ($leftType instanceof IntegerRangeType) { - $sizeType = $leftType; + $sizeType = $leftType->shift($offset); } $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); @@ -1061,23 +1061,14 @@ private function specifyTypesForCountFuncCall(FuncCall $countFuncCall, Type $typ $arraySize = $type->getArraySize(); $isSize = $sizeType->isSuperTypeOf($arraySize); - if ($context->truthy()) { - if ($isSize->no()) { - return new NeverType(); - } - - $constArray = $this->turnListIntoConstantArray($type, $sizeType); - if ($constArray !== null) { - $type = $constArray; - } + if ($context->truthy() && $isSize->no()) { + return new NeverType(); } - if ($context->falsey()) { - if (!$isSize->yes()) { - return new NeverType(); - } + if ($context->falsey() && !$isSize->yes()) { + return new NeverType(); } - return $type; + return $this->turnListIntoConstantArray($type, $sizeType) ?? $type; }); return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4700.php b/tests/PHPStan/Analyser/nsrt/bug-4700.php index 202aca765c..9d386b0c50 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4700.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4700.php @@ -41,9 +41,9 @@ function(array $array, int $count): void { if (isset($array['e'])) $a[] = $array['e']; if (count($a) > $count) { assertType('int<2, 5>', count($a)); - assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + assertType('list{0: mixed~null, 1: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { - assertType('0', count($a)); - assertType('array{}', $a); + assertType('int<0, 5>', count($a)); // Could be int<0, 1> + assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); // Could be array{}|array{0: mixed~null} } }; From 74894a33abbc9b3a32dbb47006432879c4336b0d Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Tue, 24 Dec 2024 20:34:29 +0100 Subject: [PATCH 3/7] Add another regression test --- .../CallToFunctionParametersRuleTest.php | 5 ++++ .../PHPStan/Rules/Functions/data/bug-3631.php | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-3631.php diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index ab04c623f4..bbd10c4ec7 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -545,6 +545,11 @@ public function testBug3608(): void $this->analyse([__DIR__ . '/data/bug-3608.php'], []); } + public function testBug3631(): void + { + $this->analyse([__DIR__ . '/data/bug-3631.php'], []); + } + public function testBug3920(): void { $this->analyse([__DIR__ . '/data/bug-3920.php'], []); diff --git a/tests/PHPStan/Rules/Functions/data/bug-3631.php b/tests/PHPStan/Rules/Functions/data/bug-3631.php new file mode 100644 index 0000000000..e3cb0d28f6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3631.php @@ -0,0 +1,27 @@ + + */ +function someFunc(bool $flag): array +{ + $ids = [ + ['fa', 'foo', 'baz'] + ]; + + if ($flag) { + $ids[] = ['foo', 'bar', 'baz']; + + } + + if (count($ids) > 1) { + return array_intersect(...$ids); + } + + return $ids[0]; +} + +var_dump(someFunc(true)); +var_dump(someFunc(false)); From a8b9b55fe488551ce5529a3e6d5498f70dcd7020 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 25 Dec 2024 22:21:41 +0100 Subject: [PATCH 4/7] Clean up a bit more aggressively --- src/Analyser/TypeSpecifier.php | 186 ++++++++++------------- tests/PHPStan/Analyser/nsrt/bug11480.php | 2 +- 2 files changed, 81 insertions(+), 107 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index d8e6712e4d..0067c4d8ca 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -272,7 +272,6 @@ public function specifyTypesInCondition( ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); - $sizeType = null; if ($leftType instanceof ConstantIntegerType) { if ($orEqual) { $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); @@ -281,6 +280,8 @@ public function specifyTypesInCondition( } } elseif ($leftType instanceof IntegerRangeType) { $sizeType = $leftType->shift($offset); + } else { + $sizeType = $leftType; } $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); @@ -1044,103 +1045,91 @@ public function specifyTypesInCondition( return (new SpecifiedTypes([], []))->setRootExpr($expr); } - private function specifyTypesForCountFuncCall(FuncCall $countFuncCall, Type $type, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes + private function specifyTypesForCountFuncCall( + FuncCall $countFuncCall, + Type $type, + Type $sizeType, + TypeSpecifierContext $context, + Scope $scope, + Expr $rootExpr, + ): ?SpecifiedTypes { - if ($sizeType === null) { - return null; + if (count($countFuncCall->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate()); } - if ( - $this->isFuncCallWithNormalCount($countFuncCall, $scope)->yes() - && $type->isConstantArray()->yes() - ) { - $resultType = TypeTraverser::map($type, function (Type $type, callable $traverse) use ($sizeType, $context) { - if ($type instanceof UnionType) { - return $traverse($type); - } - - $arraySize = $type->getArraySize(); - $isSize = $sizeType->isSuperTypeOf($arraySize); - if ($context->truthy() && $isSize->no()) { - return new NeverType(); - } - if ($context->falsey() && !$isSize->yes()) { - return new NeverType(); - } - - return $this->turnListIntoConstantArray($type, $sizeType) ?? $type; - }); - - return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr); + if (!$isNormalCount->yes() || (!$type->isConstantArray()->yes() && !$type->isList()->yes())) { + return null; } - return null; - } + $resultType = TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($sizeType, $context) { + if ($type instanceof UnionType) { + return $traverse($type); + } - private function turnListIntoConstantArray(Type $type, Type $sizeType): ?Type - { - if ( - $type->isList()->yes() - && $sizeType instanceof ConstantIntegerType - && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT - ) { - // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - for ($i = 0; $i < $sizeType->getValue(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); + $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($type->getArraySize()); + if ($context->truthy() && $isSizeSuperTypeOfArraySize->no()) { + return new NeverType(); + } + if ($context->falsey() && !$isSizeSuperTypeOfArraySize->yes()) { + return new NeverType(); } - return $valueTypesBuilder->getArray(); - } - if ( - $type->isList()->yes() - && $sizeType instanceof IntegerRangeType - && $sizeType->getMin() !== null - ) { - // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - for ($i = 0; $i < $sizeType->getMin(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); - } - if ($sizeType->getMax() !== null) { - for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true); - } - } elseif ($type->isConstantArray()->yes()) { - for ($i = $sizeType->getMin();; $i++) { - $offsetType = new ConstantIntegerType($i); - $hasOffset = $type->hasOffsetValueType($offsetType); - if ($hasOffset->no()) { - break; + if ($type->isList()->yes()) { + if ( + $sizeType instanceof ConstantIntegerType + && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + ) { + // turn optional offsets non-optional + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $sizeType->getValue(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); } - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes()); + return $valueTypesBuilder->getArray(); } - } else { - return null; - } - $arrayType = $valueTypesBuilder->getArray(); - if ($arrayType->isIterableAtLeastOnce()->yes()) { - return $arrayType; - } - } + if ( + $sizeType instanceof IntegerRangeType + && $sizeType->getMin() !== null + ) { + // turn optional offsets non-optional + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $sizeType->getMin(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); + } + if ($sizeType->getMax() !== null) { + for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true); + } + } elseif ($type->isConstantArray()->yes()) { + for ($i = $sizeType->getMin();; $i++) { + $offsetType = new ConstantIntegerType($i); + $hasOffset = $type->hasOffsetValueType($offsetType); + if ($hasOffset->no()) { + break; + } + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes()); + } + } else { + return TypeCombinator::intersect($type, new NonEmptyArrayType()); + } - return null; - } + return $valueTypesBuilder->getArray(); + } - private function isFuncCallWithNormalCount(FuncCall $countFuncCall, Scope $scope): TrinaryLogic - { - $argType = $scope->getType($countFuncCall->getArgs()[0]->value); + return $type; + } - if (count($countFuncCall->getArgs()) === 1) { - return TrinaryLogic::createYes(); - } - $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + return TypeCombinator::intersect($type, new NonEmptyArrayType()); + }); - return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate()); + return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr); } private function specifyTypesForConstantBinaryExpression( @@ -2177,30 +2166,15 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty return $specifiedTypes; } - 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, $scope)->setRootExpr($expr); - } - - $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); - $isNormalCount = $this->isFuncCallWithNormalCount($unwrappedLeftExpr, $scope); - $constArray = $isNormalCount->yes() ? $this->turnListIntoConstantArray($argType, $rightType) : null; - if ($constArray !== null) { - return $funcTypes->unionWith( - $this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, $scope)->setRootExpr($expr), - ); - } elseif (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { - return $funcTypes->unionWith( - $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr), - ); - } - - return $funcTypes; + if ($context->truthy() && $argType->isArray()->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr); + if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr), + ); } + + return $funcTypes; } } diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php index f4d3898790..17077d7bfc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11480.php +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -84,7 +84,7 @@ public function intUnionCount(): void 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{}", $x); } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } From 6866fd4c8c6929a13e9ac78cac844759d6a6f63a Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 26 Dec 2024 00:48:01 +0100 Subject: [PATCH 5/7] Avoid specifying incorrect falsey context --- src/Analyser/TypeSpecifier.php | 8 ++++---- tests/PHPStan/Analyser/nsrt/bug11480.php | 2 +- tests/PHPStan/Analyser/nsrt/list-count.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 0067c4d8ca..88671fff04 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1071,10 +1071,10 @@ private function specifyTypesForCountFuncCall( } $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($type->getArraySize()); - if ($context->truthy() && $isSizeSuperTypeOfArraySize->no()) { + if ($isSizeSuperTypeOfArraySize->no()) { return new NeverType(); } - if ($context->falsey() && !$isSizeSuperTypeOfArraySize->yes()) { + if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) { return new NeverType(); } @@ -1123,10 +1123,10 @@ private function specifyTypesForCountFuncCall( return $valueTypesBuilder->getArray(); } - return $type; + return $context->truthy() ? $type : new NeverType(); } - return TypeCombinator::intersect($type, new NonEmptyArrayType()); + return $context->truthy() ? TypeCombinator::intersect($type, new NonEmptyArrayType()) : new NeverType(); }); return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr); diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php index 17077d7bfc..f4d3898790 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11480.php +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -84,7 +84,7 @@ public function intUnionCount(): void if (count($x) >= $count) { assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } else { - assertType("array{}", $x); + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index c51ea31efc..e8915d0447 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -369,7 +369,7 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $t if (count($row) >= $maxThree) { assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } else { - assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } } From f647fc85a078d52e807b55d1a88d7da3442479e2 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 26 Dec 2024 01:18:54 +0100 Subject: [PATCH 6/7] Simplify by not using TypeTraverser --- src/Analyser/TypeSpecifier.php | 101 +++++++++++++++++---------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 88671fff04..5475ea72f4 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1065,71 +1065,74 @@ private function specifyTypesForCountFuncCall( return null; } - $resultType = TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($sizeType, $context) { - if ($type instanceof UnionType) { - return $traverse($type); - } - - $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($type->getArraySize()); + $resultTypes = []; + $innerTypes = $type instanceof UnionType ? $type->getTypes() : [$type]; + foreach ($innerTypes as $innerType) { + $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($innerType->getArraySize()); if ($isSizeSuperTypeOfArraySize->no()) { - return new NeverType(); + continue; } if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) { - return new NeverType(); + continue; } - if ($type->isList()->yes()) { - if ( - $sizeType instanceof ConstantIntegerType - && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT - ) { - // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - for ($i = 0; $i < $sizeType->getValue(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); - } - return $valueTypesBuilder->getArray(); + if ( + $innerType->isList()->yes() + && $sizeType instanceof ConstantIntegerType + && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + ) { + // turn optional offsets non-optional + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $sizeType->getValue(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType)); } + $resultTypes[] = $valueTypesBuilder->getArray(); + continue; + } - if ( - $sizeType instanceof IntegerRangeType - && $sizeType->getMin() !== null - ) { - // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - for ($i = 0; $i < $sizeType->getMin(); $i++) { + if ( + $innerType->isList()->yes() + && $sizeType instanceof IntegerRangeType + && $sizeType->getMin() !== null + ) { + // turn optional offsets non-optional + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $sizeType->getMin(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType)); + } + if ($sizeType->getMax() !== null) { + for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); + $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType), true); } - if ($sizeType->getMax() !== null) { - for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true); - } - } elseif ($type->isConstantArray()->yes()) { - for ($i = $sizeType->getMin();; $i++) { - $offsetType = new ConstantIntegerType($i); - $hasOffset = $type->hasOffsetValueType($offsetType); - if ($hasOffset->no()) { - break; - } - $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes()); + } elseif ($innerType->isConstantArray()->yes()) { + for ($i = $sizeType->getMin();; $i++) { + $offsetType = new ConstantIntegerType($i); + $hasOffset = $innerType->hasOffsetValueType($offsetType); + if ($hasOffset->no()) { + break; } - } else { - return TypeCombinator::intersect($type, new NonEmptyArrayType()); + $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType), !$hasOffset->yes()); } - - return $valueTypesBuilder->getArray(); + } else { + $resultTypes[] = TypeCombinator::intersect($innerType, new NonEmptyArrayType()); + continue; } - return $context->truthy() ? $type : new NeverType(); + $resultTypes[] = $valueTypesBuilder->getArray(); + continue; } - return $context->truthy() ? TypeCombinator::intersect($type, new NonEmptyArrayType()) : new NeverType(); - }); + if (!$context->truthy()) { + continue; + } + + $resultTypes[] = $innerType; + } - return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr); + return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr); } private function specifyTypesForConstantBinaryExpression( From 6153190339be794a7ac0af98cbc65ba835389776 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 6 Jan 2025 21:04:22 +0100 Subject: [PATCH 7/7] Simplify and improve :) --- src/Analyser/TypeSpecifier.php | 37 +++++++++++----------- tests/PHPStan/Analyser/nsrt/bug11480.php | 2 +- tests/PHPStan/Analyser/nsrt/list-count.php | 2 +- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 5475ea72f4..73a34c2175 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1061,23 +1061,28 @@ private function specifyTypesForCountFuncCall( $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate()); } - if (!$isNormalCount->yes() || (!$type->isConstantArray()->yes() && !$type->isList()->yes())) { + $isList = $type->isList(); + if ( + !$isNormalCount->yes() + || (!$type->isConstantArray()->yes() && !$isList->yes()) + || $type->isIterableAtLeastOnce()->no() // array{} cannot be used for further narrowing + ) { return null; } $resultTypes = []; - $innerTypes = $type instanceof UnionType ? $type->getTypes() : [$type]; - foreach ($innerTypes as $innerType) { - $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($innerType->getArraySize()); + foreach ($type->getArrays() as $arrayType) { + $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize()); if ($isSizeSuperTypeOfArraySize->no()) { continue; } + if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) { continue; } if ( - $innerType->isList()->yes() + $isList->yes() && $sizeType instanceof ConstantIntegerType && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT ) { @@ -1085,14 +1090,14 @@ private function specifyTypesForCountFuncCall( $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); for ($i = 0; $i < $sizeType->getValue(); $i++) { $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType)); + $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType)); } $resultTypes[] = $valueTypesBuilder->getArray(); continue; } if ( - $innerType->isList()->yes() + $isList->yes() && $sizeType instanceof IntegerRangeType && $sizeType->getMin() !== null ) { @@ -1100,24 +1105,24 @@ private function specifyTypesForCountFuncCall( $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); for ($i = 0; $i < $sizeType->getMin(); $i++) { $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType)); + $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType)); } if ($sizeType->getMax() !== null) { for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType), true); + $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), true); } - } elseif ($innerType->isConstantArray()->yes()) { + } elseif ($arrayType->isConstantArray()->yes()) { for ($i = $sizeType->getMin();; $i++) { $offsetType = new ConstantIntegerType($i); - $hasOffset = $innerType->hasOffsetValueType($offsetType); + $hasOffset = $arrayType->hasOffsetValueType($offsetType); if ($hasOffset->no()) { break; } - $valueTypesBuilder->setOffsetValueType($offsetType, $innerType->getOffsetValueType($offsetType), !$hasOffset->yes()); + $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()); } } else { - $resultTypes[] = TypeCombinator::intersect($innerType, new NonEmptyArrayType()); + $resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); continue; } @@ -1125,11 +1130,7 @@ private function specifyTypesForCountFuncCall( continue; } - if (!$context->truthy()) { - continue; - } - - $resultTypes[] = $innerType; + $resultTypes[] = $arrayType; } return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr); diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php index f4d3898790..17077d7bfc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11480.php +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -84,7 +84,7 @@ public function intUnionCount(): void 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{}", $x); } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index e8915d0447..c51ea31efc 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -369,7 +369,7 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $t if (count($row) >= $maxThree) { assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } else { - assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } }