From e8d9ac2f79e1c77d396d266c63e67f8363aa8c75 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 9 Aug 2024 10:47:11 +0200 Subject: [PATCH 01/11] Fix item-type in list to constant-array conversion with count() --- src/Analyser/TypeSpecifier.php | 4 ++-- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 08e0c65f0c..f05db8bc26 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1060,9 +1060,9 @@ private function specifyTypesForConstantBinaryExpression( $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(); - $itemType = $argType->getIterableValueType(); for ($i = 0; $i < $constantType->getValue(); $i++) { - $valueTypesBuilder->setOffsetValueType(new ConstantIntegerType($i), $itemType); + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $argType->getOffsetValueType($offsetType)); } $valueTypes = $this->create($exprNode->getArgs()[0]->value, $valueTypesBuilder->getArray(), $context, false, $scope, $rootExpr); } else { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index be26adf8b8..001ec26d21 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -330,7 +330,7 @@ function bug11277a(string $value): void if (preg_match('/^\[(.+,?)*\]$/', $value, $matches)) { assertType('array{0: string, 1?: non-empty-string}', $matches); if (count($matches) === 2) { - assertType('array{string, string}', $matches); // could be array{string, non-empty-string} + assertType('array{string, non-empty-string}', $matches); } } } From 766ee4eaa14cf6acb90b8f6e3202a41d2928a536 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 9 Aug 2024 10:48:45 +0200 Subject: [PATCH 02/11] Update list-count.php --- tests/PHPStan/Analyser/nsrt/list-count.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index d3848ae1db..8915d5fa30 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -216,3 +216,17 @@ function countCountable(CountableFoo $x, int $mode) } assertType('ListCount\CountableFoo', $x); } + +class CountWithOptionalKeys +{ + /** + * @param array{mixed}|array{0: mixed, 1?: string|null} $row + */ + protected function testOptionalKeys(array $row): void + { + if (count($row) === 2) { + assertType('array{mixed, string|null}', $row); + } + } + +} From 0421518c9c81f65840f3f7bc074c7ec61e45e84c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 9 Aug 2024 11:20:03 +0200 Subject: [PATCH 03/11] fix --- tests/PHPStan/Analyser/nsrt/list-count.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index 8915d5fa30..9b09bd4a32 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -220,12 +220,32 @@ function countCountable(CountableFoo $x, int $mode) class CountWithOptionalKeys { /** - * @param array{mixed}|array{0: mixed, 1?: string|null} $row + * @param array{0: mixed, 1?: string|null} $row */ protected function testOptionalKeys(array $row): void { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 1) { + assertType('array{mixed}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + if (count($row) === 2) { assertType('array{mixed, string|null}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); } } From b686f79b8bb34d2adcaeefde9fee723e35c2aabd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 9 Aug 2024 11:29:50 +0200 Subject: [PATCH 04/11] more tests --- tests/PHPStan/Analyser/nsrt/list-count.php | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index 9b09bd4a32..1e89ed42f1 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -249,4 +249,34 @@ protected function testOptionalKeys(array $row): void } } + /** + * @param array{mixed}|array{0: mixed, 1?: string|null} $row + */ + protected function testOptionalKeysInUnion(array $row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 1) { + assertType('array{mixed}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{mixed, string|null}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + } + } From ce2c4cd1e4e247048661f210a256cbc0591baa0a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 9 Aug 2024 11:32:47 +0200 Subject: [PATCH 05/11] more tests --- tests/PHPStan/Analyser/nsrt/list-count.php | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index 1e89ed42f1..f3377bb046 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -279,4 +279,34 @@ protected function testOptionalKeysInUnion(array $row): void } } + /** + * @param array{string}|array{0: mixed, 1?: string|null} $row + */ + protected function testOptionalKeysInTaggedUnion(array $row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 1) { + assertType('array{mixed}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{mixed, string|null}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + } + } From ac4de8199ab26bf04b63b917890a4834f2a615d4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 9 Aug 2024 11:39:04 +0200 Subject: [PATCH 06/11] analog fix in tagged union narrowing --- tests/PHPStan/Analyser/nsrt/list-count.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index f3377bb046..fd77edbb19 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -222,7 +222,7 @@ class CountWithOptionalKeys /** * @param array{0: mixed, 1?: string|null} $row */ - protected function testOptionalKeys(array $row): void + protected function testOptionalKeys($row): void { if (count($row) === 0) { assertType('*NEVER*', $row); @@ -252,7 +252,7 @@ protected function testOptionalKeys(array $row): void /** * @param array{mixed}|array{0: mixed, 1?: string|null} $row */ - protected function testOptionalKeysInUnion(array $row): void + protected function testOptionalKeysInUnion($row): void { if (count($row) === 0) { assertType('*NEVER*', $row); @@ -280,32 +280,32 @@ protected function testOptionalKeysInUnion(array $row): void } /** - * @param array{string}|array{0: mixed, 1?: string|null} $row + * @param array{string}|array{0: int, 1?: string|null} $row */ - protected function testOptionalKeysInTaggedUnion(array $row): void + protected function testOptionalKeysInTaggedUnion($row): void { if (count($row) === 0) { assertType('*NEVER*', $row); } else { - assertType('array{0: mixed, 1?: string|null}', $row); + assertType('array{0: int, 1?: string|null}|array{string}', $row); } if (count($row) === 1) { - assertType('array{mixed}', $row); + assertType('array{0: int, 1?: string|null}|array{string}', $row); } else { - assertType('array{0: mixed, 1?: string|null}', $row); + assertType('array{0: int, 1?: string|null}', $row); } if (count($row) === 2) { - assertType('array{mixed, string|null}', $row); + assertType('array{int, string|null}', $row); } else { - assertType('array{0: mixed, 1?: string|null}', $row); + assertType('array{0: int, 1?: string|null}|array{string}', $row); } if (count($row) === 3) { assertType('*NEVER*', $row); } else { - assertType('array{0: mixed, 1?: string|null}', $row); + assertType('array{0: int, 1?: string|null}|array{string}', $row); } } From 3b4317028b1bce302a5a021d41272b2a5efa167b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 9 Aug 2024 19:26:53 +0200 Subject: [PATCH 07/11] narrow only lists --- tests/PHPStan/Analyser/nsrt/list-count.php | 32 +++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index fd77edbb19..006bf149d8 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -282,7 +282,7 @@ protected function testOptionalKeysInUnion($row): void /** * @param array{string}|array{0: int, 1?: string|null} $row */ - protected function testOptionalKeysInTaggedUnion($row): void + protected function testOptionalKeysInListsOfTaggedUnion($row): void { if (count($row) === 0) { assertType('*NEVER*', $row); @@ -309,4 +309,34 @@ protected function testOptionalKeysInTaggedUnion($row): void } } + /** + * @param array{string}|array{0: int, 3?: string|null} $row + */ + protected function testOptionalKeysInUnionArray($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + + if (count($row) === 1) { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } else { + assertType('array{0: int, 3?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{0: int, 3?: string|null}', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + } + } From 95d749ed03309f5f43d948accb490186f08adf31 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 21:50:26 +0200 Subject: [PATCH 08/11] Update TypeSpecifier.php --- src/Analyser/TypeSpecifier.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index f05db8bc26..0da1d636a6 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -970,6 +970,16 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT if ($isSize->no()) { continue; } + + if ($sizeType instanceof ConstantIntegerType && $innerType->isList()->yes()) { + // 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)); + } + $innerType = $valueTypesBuilder->getArray(); + } } if ($context->falsey()) { if (!$isSize->yes()) { @@ -1059,6 +1069,7 @@ private function specifyTypesForConstantBinaryExpression( $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); if ($isNormalCount->yes() && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + // turn optional offsets non-optional $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); for ($i = 0; $i < $constantType->getValue(); $i++) { $offsetType = new ConstantIntegerType($i); From 04d8bbd34b27ac8dab90f7aa83b95301045e8d36 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 22:08:04 +0200 Subject: [PATCH 09/11] Update TypeSpecifier.php --- src/Analyser/TypeSpecifier.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 0da1d636a6..21b324cb3f 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -971,7 +971,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT continue; } - if ($sizeType instanceof ConstantIntegerType && $innerType->isList()->yes()) { + if ($sizeType instanceof ConstantIntegerType && $innerType->isList()->yes() && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { // turn optional offsets non-optional $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); for ($i = 0; $i < $sizeType->getValue(); $i++) { @@ -1060,6 +1060,14 @@ private function specifyTypesForConstantBinaryExpression( } 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); + } + if (count($exprNode->getArgs()) === 1) { $isNormalCount = TrinaryLogic::createYes(); } else { From e90211b36b391f5f92bf49ed1074430295628611 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 22:40:53 +0200 Subject: [PATCH 10/11] de-duplicate --- src/Analyser/TypeSpecifier.php | 58 ++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 21b324cb3f..9e49de8698 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -971,14 +971,9 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT continue; } - if ($sizeType instanceof ConstantIntegerType && $innerType->isList()->yes() && $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)); - } - $innerType = $valueTypesBuilder->getArray(); + $constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope); + if ($constArray !== null) { + $innerType = $constArray; } } if ($context->falsey()) { @@ -996,6 +991,35 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT return null; } + private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $listOrArray, Type $sizeType, Scope $scope): ?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)->or($argType->getIterableValueType()->isArray()->negate()); + } + + if ( + $isNormalCount->yes() + && $sizeType instanceof ConstantIntegerType + && $listOrArray->isList()->yes() + && $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, $listOrArray->getOffsetValueType($offsetType)); + } + return $valueTypesBuilder->getArray(); + } + + return null; + } + private function specifyTypesForConstantBinaryExpression( Expr $exprNode, ConstantScalarType $constantType, @@ -1068,22 +1092,10 @@ private function specifyTypesForConstantBinaryExpression( return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); } - 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) { - // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - for ($i = 0; $i < $constantType->getValue(); $i++) { - $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $argType->getOffsetValueType($offsetType)); - } - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $valueTypesBuilder->getArray(), $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); } From 509ac4f702876b3ba93fed88f117d027d8e8991a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 22:43:31 +0200 Subject: [PATCH 11/11] Update TypeSpecifier.php --- src/Analyser/TypeSpecifier.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 9e49de8698..1c1732089d 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -991,7 +991,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT return null; } - private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $listOrArray, Type $sizeType, Scope $scope): ?Type + private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type { $argType = $scope->getType($countFuncCall->getArgs()[0]->value); @@ -1004,15 +1004,15 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $listOr if ( $isNormalCount->yes() + && $type->isList()->yes() && $sizeType instanceof ConstantIntegerType - && $listOrArray->isList()->yes() && $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, $listOrArray->getOffsetValueType($offsetType)); + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); } return $valueTypesBuilder->getArray(); }