diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4115a752e0..276d219dce 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -984,31 +984,49 @@ private function specifyTypesForConstantBinaryExpression( && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) && $constantType instanceof ConstantIntegerType ) { + $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; + } + + return $this->create($exprNode->getArgs()[0]->value, TypeCombinator::union(...$result), $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->isArray()->yes()) { - if (count($exprNode->getArgs()) === 1) { - $isNormalCount = true; - } else { - $mode = $scope->getType($exprNode->getArgs()[1]->value); - if (!$mode->isInteger()->yes()) { - return new SpecifiedTypes(); - } - - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->yes(); - if (!$isNormalCount) { - $isNormalCount = $argType->getIterableValueType()->isArray()->no(); - } - } - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - if ($isNormalCount && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + 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++) { diff --git a/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php new file mode 100644 index 0000000000..8ecf3438e7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php @@ -0,0 +1,111 @@ +, numeric-string} $arr */ + public function nestedArrays(array $arr): void + { + // don't narrow when $arr contains recursive arrays + if (count($arr, COUNT_RECURSIVE) === 3) { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + + if (count($arr, COUNT_NORMAL) === 3) { + assertType("array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + + /** @param array{string, '', non-empty-string}|array $arr */ + public function mixedArrays(array $arr): void + { + if (count($arr, COUNT_NORMAL) === 3) { + assertType("non-empty-array", $arr); // could be array{string, '', non-empty-string}|non-empty-array + } else { + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + + public function arrayIntRangeSize(): void + { + $x = []; + if (rand(0,1)) { + $x[] = 'ab'; + } + if (rand(0,1)) { + $x[] = 'xy'; + } + + if (count($x) === 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +} +