Skip to content

Commit 74d0ddf

Browse files
authored
Merge branch refs/heads/2.1.x into 2.2.x
2 parents e696ed0 + ff98196 commit 74d0ddf

File tree

2 files changed

+119
-26
lines changed

2 files changed

+119
-26
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,41 @@ public function specifyTypesInCondition(
725725

726726
if ($context->null()) {
727727
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
728+
} else {
729+
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);
730+
}
731+
732+
// infer $arr[$key] after $key = array_key_first/last($arr)
733+
if (
734+
$expr->expr instanceof FuncCall
735+
&& $expr->expr->name instanceof Name
736+
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
737+
&& count($expr->expr->getArgs()) >= 1
738+
) {
739+
$arrayArg = $expr->expr->getArgs()[0]->value;
740+
$arrayType = $scope->getType($arrayArg);
728741

742+
if ($arrayType->isArray()->yes()) {
743+
if ($context->true()) {
744+
$specifiedTypes = $specifiedTypes->unionWith(
745+
$this->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
746+
);
747+
$isNonEmpty = true;
748+
} else {
749+
$isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes();
750+
}
751+
752+
if ($isNonEmpty) {
753+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
754+
755+
$specifiedTypes = $specifiedTypes->unionWith(
756+
$this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
757+
);
758+
}
759+
}
760+
}
761+
762+
if ($context->null()) {
729763
// infer $arr[$key] after $key = array_rand($arr)
730764
if (
731765
$expr->expr instanceof FuncCall
@@ -755,30 +789,6 @@ public function specifyTypesInCondition(
755789
}
756790
}
757791

758-
// infer $arr[$key] after $key = array_key_first/last($arr)
759-
if (
760-
$expr->expr instanceof FuncCall
761-
&& $expr->expr->name instanceof Name
762-
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
763-
&& count($expr->expr->getArgs()) >= 1
764-
) {
765-
$arrayArg = $expr->expr->getArgs()[0]->value;
766-
$arrayType = $scope->getType($arrayArg);
767-
if (
768-
$arrayType->isArray()->yes()
769-
&& $arrayType->isIterableAtLeastOnce()->yes()
770-
) {
771-
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
772-
$iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first'
773-
? $arrayType->getIterableValueType()
774-
: $arrayType->getIterableValueType();
775-
776-
return $specifiedTypes->unionWith(
777-
$this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope),
778-
);
779-
}
780-
}
781-
782792
// infer $list[$count] after $count = count($list) - 1
783793
if (
784794
$expr->expr instanceof Expr\BinaryOp\Minus
@@ -806,8 +816,6 @@ public function specifyTypesInCondition(
806816
return $specifiedTypes;
807817
}
808818

809-
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);
810-
811819
if ($context->true()) {
812820
// infer $arr[$key] after $key = array_search($needle, $arr)
813821
if (
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14081;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/** @param list<string> $array */
10+
function first(array $array): mixed
11+
{
12+
if (($key = array_key_first($array))) {
13+
assertType('int<1, max>', $key);
14+
assertType('non-empty-list<string>', $array);
15+
assertType('string', $array[$key]);
16+
return $array[$key];
17+
}
18+
return null;
19+
}
20+
21+
/** @param list<string> $array */
22+
function last(array $array): mixed
23+
{
24+
if (($key = array_key_last($array))) {
25+
assertType('int<1, max>', $key);
26+
assertType('non-empty-list<string>', $array);
27+
assertType('string', $array[$key]);
28+
return $array[$key];
29+
}
30+
return null;
31+
}
32+
33+
function maybeNonEmpty(): void
34+
{
35+
if (rand(0,1)) {
36+
$array = ['one', 'two'];
37+
} else {
38+
$array = [];
39+
}
40+
assertType("array{}|array{'one', 'two'}", $array);
41+
$key = array_key_last($array);
42+
assertType('0|1|null', $key);
43+
assertType("'one'|'two'", $array[$key]);
44+
}
45+
46+
/** @param list<string> $array */
47+
function firstNotNull(array $array): mixed
48+
{
49+
if (($key = array_key_first($array)) !== null) {
50+
assertType('int<0, max>', $key);
51+
assertType('list<string>', $array); // could be non-empty-list<string>
52+
assertType('string', $array[$key]);
53+
return $array[$key];
54+
}
55+
return null;
56+
}
57+
58+
/** @param list<string> $array */
59+
function lastNotNull(array $array): mixed
60+
{
61+
if (($key = array_key_last($array)) !== null) {
62+
assertType('int<0, max>', $key);
63+
assertType('list<string>', $array); // could be non-empty-list<string>
64+
assertType('string', $array[$key]);
65+
return $array[$key];
66+
}
67+
return null;
68+
}
69+
70+
/** @param list<string> $array */
71+
function noIf(array $array): void
72+
{
73+
$key = array_key_first($array);
74+
assertType('int<0, max>|null', $key);
75+
assertType('list<string>', $array);
76+
assertType('string', $array[$key]);
77+
78+
if ($array === []) {
79+
return;
80+
}
81+
$key = array_key_first($array);
82+
assertType('int<0, max>', $key);
83+
assertType('non-empty-list<string>', $array);
84+
assertType('string', $array[$key]);
85+
}

0 commit comments

Comments
 (0)