diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 51995ac665..2b13bb7428 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -51,6 +51,7 @@ use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; @@ -73,6 +74,8 @@ use function in_array; use function is_int; use function is_string; +use function min; +use function strtolower; /** * @implements ExprHandler @@ -313,6 +316,36 @@ public function processAssignVar( $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType); } + if ( + $assignedExpr instanceof FuncCall + && $assignedExpr->name instanceof Name + && in_array(strtolower((string) $assignedExpr->name), ['count', 'sizeof'], true) + && count($assignedExpr->getArgs()) >= 1 + ) { + $countArgType = $scope->getType($assignedExpr->getArgs()[0]->value); + if ($countArgType->isList()->yes() || $countArgType->isConstantArray()->yes()) { + $arraySize = $countArgType->getArraySize(); + $maxSize = ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT; + if ($arraySize instanceof ConstantIntegerType) { + $maxSize = $arraySize->getValue(); + } elseif ($arraySize instanceof IntegerRangeType && $arraySize->getMax() !== null) { + $maxSize = min($maxSize, $arraySize->getMax()); + } + + for ($i = 1; $i <= $maxSize; $i++) { + $sizeType = new ConstantIntegerType($i); + if (!$type->isSuperTypeOf($sizeType)->yes()) { + continue; + } + + $identicalConditionExpr = new Expr\BinaryOp\Identical($assignedExpr, new Node\Scalar\Int_($i)); + $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $identicalConditionExpr, TypeSpecifierContext::createTrue()); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $sizeType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $sizeType); + } + } + } + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14464.php b/tests/PHPStan/Analyser/nsrt/bug-14464.php new file mode 100644 index 0000000000..197754bc51 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14464.php @@ -0,0 +1,109 @@ +', $colParts); + $numParts = count($colParts); + + if ($numParts == 3) { + assertType('array{non-empty-string, non-empty-string, non-empty-string}', $colParts); + } elseif ($numParts == 2) { + assertType('array{non-empty-string, non-empty-string}', $colParts); + } elseif ($numParts == 1) { + assertType('array{non-empty-string}', $colParts); + } + } + + /** @param list $list */ + public function indirectCountCheck(array $list): void + { + $n = count($list); + if ($n === 3) { + assertType('array{string, string, string}', $list); + } + if ($n === 2) { + assertType('array{string, string}', $list); + } + if ($n === 1) { + assertType('array{string}', $list); + } + } + + /** @param list $list */ + public function directCountCheck(array $list): void + { + if (count($list) === 3) { + assertType('array{string, string, string}', $list); + } + if (count($list) === 2) { + assertType('array{string, string}', $list); + } + if (count($list) === 1) { + assertType('array{string}', $list); + } + } + + /** @param list $list */ + public function sizeofIndirect(array $list): void + { + $n = sizeof($list); + if ($n === 2) { + assertType('array{string, string}', $list); + } + } + + /** @param list $list */ + public function looseEqualityCheck(array $list): void + { + $n = count($list); + if ($n == 3) { + assertType('array{int, int, int}', $list); + } + } + + /** + * Non-list arrays should not get specific shapes since keys are unknown + * @param array $map + */ + public function nonListArray(array $map): void + { + $n = count($map); + if ($n === 2) { + assertType('non-empty-array', $map); + } + } + + /** @param array{string}|array{string, string}|array{string, string, string} $list */ + public function constantArrayUnionIndirect(array $list): void + { + $n = count($list); + if ($n === 2) { + assertType('array{string, string}', $list); + } + if ($n === 3) { + assertType('array{string, string, string}', $list); + } + } + + /** @param array{a: string, b: int}|array{x: float, y: float, z: float} $map */ + public function constantNonListDifferentShapes(array $map): void + { + $n = count($map); + if ($n === 2) { + assertType('array{a: string, b: int}', $map); + } + if ($n === 3) { + assertType('array{x: float, y: float, z: float}', $map); + } + } +}