Skip to content

Commit 3eab462

Browse files
committed
ConstantArrayTypeBuilder - preserve ConstantArrayType for integer range offsets
1 parent ebfe7cf commit 3eab462

File tree

3 files changed

+128
-48
lines changed

3 files changed

+128
-48
lines changed

Diff for: src/Type/Constant/ConstantArrayTypeBuilder.php

+64-47
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use function count;
1414
use function is_float;
1515
use function max;
16+
use function range;
1617

1718
/** @api */
1819
class ConstantArrayTypeBuilder
@@ -59,67 +60,83 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
5960
$offsetType = ArrayType::castToArrayKeyType($offsetType);
6061
}
6162

62-
if (
63-
!$this->degradeToGeneralArray
64-
&& ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType)
65-
) {
66-
/** @var ConstantIntegerType|ConstantStringType $keyType */
67-
foreach ($this->keyTypes as $i => $keyType) {
68-
if ($keyType->getValue() === $offsetType->getValue()) {
69-
$this->valueTypes[$i] = $valueType;
70-
$this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
71-
return;
63+
if (!$this->degradeToGeneralArray) {
64+
if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {
65+
/** @var ConstantIntegerType|ConstantStringType $keyType */
66+
foreach ($this->keyTypes as $i => $keyType) {
67+
if ($keyType->getValue() === $offsetType->getValue()) {
68+
$this->valueTypes[$i] = $valueType;
69+
$this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
70+
return;
71+
}
7272
}
73-
}
7473

75-
$this->keyTypes[] = $offsetType;
76-
$this->valueTypes[] = $valueType;
74+
$this->keyTypes[] = $offsetType;
75+
$this->valueTypes[] = $valueType;
7776

78-
if ($optional) {
79-
$this->optionalKeys[] = count($this->keyTypes) - 1;
80-
}
77+
if ($optional) {
78+
$this->optionalKeys[] = count($this->keyTypes) - 1;
79+
}
8180

82-
/** @var int|float $newNextAutoIndex */
83-
$newNextAutoIndex = $offsetType instanceof ConstantIntegerType
84-
? max($this->nextAutoIndex, $offsetType->getValue() + 1)
85-
: $this->nextAutoIndex;
86-
if (!is_float($newNextAutoIndex)) {
87-
$this->nextAutoIndex = $newNextAutoIndex;
81+
/** @var int|float $newNextAutoIndex */
82+
$newNextAutoIndex = $offsetType instanceof ConstantIntegerType
83+
? max($this->nextAutoIndex, $offsetType->getValue() + 1)
84+
: $this->nextAutoIndex;
85+
if (!is_float($newNextAutoIndex)) {
86+
$this->nextAutoIndex = $newNextAutoIndex;
87+
}
88+
return;
8889
}
89-
return;
90-
}
9190

92-
$scalarTypes = TypeUtils::getConstantScalars($offsetType);
93-
if (!$this->degradeToGeneralArray && count($scalarTypes) > 0) {
94-
$match = true;
95-
$valueTypes = $this->valueTypes;
96-
foreach ($scalarTypes as $scalarType) {
97-
$scalarOffsetType = ArrayType::castToArrayKeyType($scalarType);
98-
if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) {
99-
throw new ShouldNotHappenException();
91+
$scalarTypes = TypeUtils::getConstantScalars($offsetType);
92+
if (count($scalarTypes) === 0) {
93+
$integerRanges = TypeUtils::getIntegerRanges($offsetType);
94+
if (count($integerRanges) > 0) {
95+
foreach ($integerRanges as $integerRange) {
96+
if ($integerRange->getMin() === null) {
97+
break;
98+
}
99+
if ($integerRange->getMax() === null) {
100+
break;
101+
}
102+
103+
foreach (range($integerRange->getMin(), $integerRange->getMax()) as $rangeValue) {
104+
$scalarTypes[] = new ConstantIntegerType($rangeValue);
105+
}
106+
}
100107
}
101-
$offsetMatch = false;
108+
}
109+
if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) {
110+
$match = true;
111+
$valueTypes = $this->valueTypes;
112+
foreach ($scalarTypes as $scalarType) {
113+
$scalarOffsetType = ArrayType::castToArrayKeyType($scalarType);
114+
if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) {
115+
throw new ShouldNotHappenException();
116+
}
117+
$offsetMatch = false;
102118

103-
/** @var ConstantIntegerType|ConstantStringType $keyType */
104-
foreach ($this->keyTypes as $i => $keyType) {
105-
if ($keyType->getValue() !== $scalarOffsetType->getValue()) {
119+
/** @var ConstantIntegerType|ConstantStringType $keyType */
120+
foreach ($this->keyTypes as $i => $keyType) {
121+
if ($keyType->getValue() !== $scalarOffsetType->getValue()) {
122+
continue;
123+
}
124+
125+
$valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType);
126+
$offsetMatch = true;
127+
}
128+
129+
if ($offsetMatch) {
106130
continue;
107131
}
108132

109-
$valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType);
110-
$offsetMatch = true;
133+
$match = false;
111134
}
112135

113-
if ($offsetMatch) {
114-
continue;
136+
if ($match) {
137+
$this->valueTypes = $valueTypes;
138+
return;
115139
}
116-
117-
$match = false;
118-
}
119-
120-
if ($match) {
121-
$this->valueTypes = $valueTypes;
122-
return;
123140
}
124141
}
125142

Diff for: src/Type/TypeUtils.php

+8
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ public static function getDirectClassNames(Type $type): array
150150
return [];
151151
}
152152

153+
/**
154+
* @return IntegerRangeType[]
155+
*/
156+
public static function getIntegerRanges(Type $type): array
157+
{
158+
return self::map(IntegerRangeType::class, $type, false);
159+
}
160+
153161
/**
154162
* @return ConstantScalarType[]
155163
*/

Diff for: tests/PHPStan/Analyser/data/constant-array-type-set.php

+56-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function doFoo(int $i)
2727
/** @var int<0, 2> $offset2 */
2828
$offset2 = doFoo();
2929
$d[$offset2] = true;
30-
//assertType('array{bool, bool, bool}', $d);
30+
assertType('array{bool, bool, bool}', $d);
3131

3232
$e = [false, false, false];
3333
/** @var 0|1|2|3 $offset3 */
@@ -42,4 +42,59 @@ public function doFoo(int $i)
4242
assertType('array{bool, bool, false}', $f);
4343
}
4444

45+
/**
46+
* @param int<0, 1> $offset
47+
* @return void
48+
*/
49+
public function doBar(int $offset): void
50+
{
51+
$a = [false, false, false];
52+
$a[$offset] = true;
53+
assertType('array{bool, bool, false}', $a);
54+
}
55+
56+
/**
57+
* @param int<0, 1>|int<3, 4> $offset
58+
* @return void
59+
*/
60+
public function doBar2(int $offset): void
61+
{
62+
$a = [false, false, false, false, false];
63+
$a[$offset] = true;
64+
assertType('array{bool, bool, false, bool, bool}', $a);
65+
}
66+
67+
/**
68+
* @param int<0, max> $offset
69+
* @return void
70+
*/
71+
public function doBar3(int $offset): void
72+
{
73+
$a = [false, false, false, false, false];
74+
$a[$offset] = true;
75+
assertType('non-empty-array<int<0, max>, bool>', $a);
76+
}
77+
78+
/**
79+
* @param int<min, 0> $offset
80+
* @return void
81+
*/
82+
public function doBar4(int $offset): void
83+
{
84+
$a = [false, false, false, false, false];
85+
$a[$offset] = true;
86+
assertType('non-empty-array<int<min, 4>, bool>', $a);
87+
}
88+
89+
/**
90+
* @param int<0, 4> $offset
91+
* @return void
92+
*/
93+
public function doBar5(int $offset): void
94+
{
95+
$a = [false, false, false];
96+
$a[$offset] = true;
97+
assertType('non-empty-array<int<0, 4>, bool>', $a);
98+
}
99+
45100
}

0 commit comments

Comments
 (0)