diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3a1f8bd98e..a066bd1e0e 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -725,7 +725,41 @@ public function specifyTypesInCondition( if ($context->null()) { $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); + } else { + $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr); + } + + // infer $arr[$key] after $key = array_key_first/last($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->expr->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ($arrayType->isArray()->yes()) { + if ($context->true()) { + $specifiedTypes = $specifiedTypes->unionWith( + $this->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope), + ); + $isNonEmpty = true; + } else { + $isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes(); + } + + if ($isNonEmpty) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + $specifiedTypes = $specifiedTypes->unionWith( + $this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), + ); + } + } + } + + if ($context->null()) { // infer $arr[$key] after $key = array_rand($arr) if ( $expr->expr instanceof FuncCall @@ -755,30 +789,6 @@ public function specifyTypesInCondition( } } - // infer $arr[$key] after $key = array_key_first/last($arr) - if ( - $expr->expr instanceof FuncCall - && $expr->expr->name instanceof Name - && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) - && count($expr->expr->getArgs()) >= 1 - ) { - $arrayArg = $expr->expr->getArgs()[0]->value; - $arrayType = $scope->getType($arrayArg); - if ( - $arrayType->isArray()->yes() - && $arrayType->isIterableAtLeastOnce()->yes() - ) { - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - $iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first' - ? $arrayType->getIterableValueType() - : $arrayType->getIterableValueType(); - - return $specifiedTypes->unionWith( - $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope), - ); - } - } - // infer $list[$count] after $count = count($list) - 1 if ( $expr->expr instanceof Expr\BinaryOp\Minus @@ -806,8 +816,6 @@ public function specifyTypesInCondition( return $specifiedTypes; } - $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr); - if ($context->true()) { // infer $arr[$key] after $key = array_search($needle, $arr) if ( diff --git a/tests/PHPStan/Analyser/nsrt/bug-14081.php b/tests/PHPStan/Analyser/nsrt/bug-14081.php new file mode 100644 index 0000000000..277c811499 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14081.php @@ -0,0 +1,85 @@ + $array */ +function first(array $array): mixed +{ + if (($key = array_key_first($array))) { + assertType('int<1, max>', $key); + assertType('non-empty-list', $array); + assertType('string', $array[$key]); + return $array[$key]; + } + return null; +} + +/** @param list $array */ +function last(array $array): mixed +{ + if (($key = array_key_last($array))) { + assertType('int<1, max>', $key); + assertType('non-empty-list', $array); + assertType('string', $array[$key]); + return $array[$key]; + } + return null; +} + +function maybeNonEmpty(): void +{ + if (rand(0,1)) { + $array = ['one', 'two']; + } else { + $array = []; + } + assertType("array{}|array{'one', 'two'}", $array); + $key = array_key_last($array); + assertType('0|1|null', $key); + assertType("'one'|'two'", $array[$key]); +} + +/** @param list $array */ +function firstNotNull(array $array): mixed +{ + if (($key = array_key_first($array)) !== null) { + assertType('int<0, max>', $key); + assertType('list', $array); // could be non-empty-list + assertType('string', $array[$key]); + return $array[$key]; + } + return null; +} + +/** @param list $array */ +function lastNotNull(array $array): mixed +{ + if (($key = array_key_last($array)) !== null) { + assertType('int<0, max>', $key); + assertType('list', $array); // could be non-empty-list + assertType('string', $array[$key]); + return $array[$key]; + } + return null; +} + +/** @param list $array */ +function noIf(array $array): void +{ + $key = array_key_first($array); + assertType('int<0, max>|null', $key); + assertType('list', $array); + assertType('string', $array[$key]); + + if ($array === []) { + return; + } + $key = array_key_first($array); + assertType('int<0, max>', $key); + assertType('non-empty-list', $array); + assertType('string', $array[$key]); +}