Skip to content

Commit 5e793f7

Browse files
committed
Next autoindexes can be empty and setOffsetValueType with null offsetType returns ErrorType
Closes phpstan/phpstan#14548 Because code like this: ``` $a = [ PHP_INT_MAX => 4, ]; $a[] = 5; ``` Triggers PHP Error: ``` Cannot add element to the array as the next element is already occupied ```
1 parent 2a88b4f commit 5e793f7

8 files changed

Lines changed: 188 additions & 45 deletions

File tree

src/Type/Constant/ConstantArrayType.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class ConstantArrayType implements Type
101101
* @api
102102
* @param list<ConstantIntegerType|ConstantStringType> $keyTypes
103103
* @param array<int, Type> $valueTypes
104-
* @param non-empty-list<int> $nextAutoIndexes
104+
* @param list<int> $nextAutoIndexes
105105
* @param int[] $optionalKeys
106106
*/
107107
public function __construct(
@@ -128,7 +128,7 @@ public function __construct(
128128
/**
129129
* @param list<ConstantIntegerType|ConstantStringType> $keyTypes
130130
* @param array<int, Type> $valueTypes
131-
* @param non-empty-list<int> $nextAutoIndexes
131+
* @param list<int> $nextAutoIndexes
132132
* @param int[] $optionalKeys
133133
*/
134134
protected function recreate(
@@ -208,7 +208,7 @@ public function isConstantValue(): TrinaryLogic
208208
}
209209

210210
/**
211-
* @return non-empty-list<int>
211+
* @return list<int>
212212
*/
213213
public function getNextAutoIndexes(): array
214214
{
@@ -745,6 +745,10 @@ public function getOffsetValueType(Type $offsetType): Type
745745

746746
public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
747747
{
748+
if ($offsetType === null && count($this->nextAutoIndexes) === 0) {
749+
return new ErrorType();
750+
}
751+
748752
$builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
749753
$builder->setOffsetValueType($offsetType, $valueType);
750754

src/Type/Constant/ConstantArrayTypeBuilder.php

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ final class ConstantArrayTypeBuilder
4444
/**
4545
* @param list<Type> $keyTypes
4646
* @param array<int, Type> $valueTypes
47-
* @param non-empty-list<int> $nextAutoIndexes
47+
* @param list<int> $nextAutoIndexes
4848
* @param array<int> $optionalKeys
4949
*/
5050
private function __construct(
@@ -85,6 +85,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
8585
$offsetType = $offsetType->toArrayKey();
8686
}
8787

88+
if ($offsetType === null && count($this->nextAutoIndexes) === 0) {
89+
return;
90+
}
91+
8892
if (!$this->degradeToGeneralArray) {
8993
if (
9094
$valueType instanceof ClosureType
@@ -108,6 +112,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
108112
}
109113

110114
if ($offsetType === null) {
115+
if (count($this->nextAutoIndexes) === 0) {
116+
return;
117+
}
118+
111119
$newAutoIndexes = $optional ? $this->nextAutoIndexes : [];
112120
$hasOptional = false;
113121
foreach ($this->keyTypes as $i => $keyType) {
@@ -127,11 +135,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
127135

128136
/** @var int|float $newAutoIndex */
129137
$newAutoIndex = $keyType->getValue() + 1;
130-
if (is_float($newAutoIndex)) {
131-
$newAutoIndex = $keyType->getValue();
138+
if (!is_float($newAutoIndex)) {
139+
$newAutoIndexes[] = $newAutoIndex;
132140
}
133141

134-
$newAutoIndexes[] = $newAutoIndex;
135142
$hasOptional = true;
136143
}
137144

@@ -142,11 +149,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
142149

143150
/** @var int|float $newAutoIndex */
144151
$newAutoIndex = $max + 1;
145-
if (is_float($newAutoIndex)) {
146-
$newAutoIndex = $max;
152+
if (!is_float($newAutoIndex)) {
153+
$newAutoIndexes[] = $newAutoIndex;
147154
}
148155

149-
$newAutoIndexes[] = $newAutoIndex;
150156
$this->nextAutoIndexes = array_values(array_unique($newAutoIndexes));
151157

152158
if ($optional || $hasOptional) {
@@ -180,11 +186,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
180186
if (!$optional) {
181187
$this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i));
182188
if ($keyType instanceof ConstantIntegerType) {
183-
$nextAutoIndexes = array_values(array_filter($this->nextAutoIndexes, static fn (int $index) => $index > $keyType->getValue()));
184-
if (count($nextAutoIndexes) === 0) {
185-
throw new ShouldNotHappenException();
186-
}
187-
$this->nextAutoIndexes = $nextAutoIndexes;
189+
$this->nextAutoIndexes = array_values(array_filter($this->nextAutoIndexes, static fn (int $index) => $index > $keyType->getValue()));
188190
}
189191
}
190192
return;
@@ -194,33 +196,38 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
194196
$this->valueTypes[] = $valueType;
195197

196198
if ($offsetType instanceof ConstantIntegerType) {
197-
$min = min($this->nextAutoIndexes);
198-
$max = max($this->nextAutoIndexes);
199-
$offsetValue = $offsetType->getValue();
200-
if ($offsetValue >= 0) {
201-
if ($offsetValue > $min) {
202-
if ($offsetValue <= $max) {
203-
$this->isList = $this->isList->and(TrinaryLogic::createMaybe());
199+
if (count($this->nextAutoIndexes) > 0) {
200+
$min = min($this->nextAutoIndexes);
201+
$max = max($this->nextAutoIndexes);
202+
$offsetValue = $offsetType->getValue();
203+
if ($offsetValue >= 0) {
204+
if ($offsetValue > $min) {
205+
if ($offsetValue <= $max) {
206+
$this->isList = $this->isList->and(TrinaryLogic::createMaybe());
207+
} else {
208+
$this->isList = TrinaryLogic::createNo();
209+
}
210+
}
211+
} else {
212+
$this->isList = TrinaryLogic::createNo();
213+
}
214+
215+
if ($offsetValue >= $max) {
216+
/** @var int|float $newAutoIndex */
217+
$newAutoIndex = $offsetValue + 1;
218+
if (is_float($newAutoIndex)) {
219+
if (!$optional) {
220+
$this->nextAutoIndexes = [];
221+
}
222+
} elseif (!$optional) {
223+
$this->nextAutoIndexes = [$newAutoIndex];
204224
} else {
205-
$this->isList = TrinaryLogic::createNo();
225+
$this->nextAutoIndexes[] = $newAutoIndex;
206226
}
207227
}
208228
} else {
209229
$this->isList = TrinaryLogic::createNo();
210230
}
211-
212-
if ($offsetValue >= $max) {
213-
/** @var int|float $newAutoIndex */
214-
$newAutoIndex = $offsetValue + 1;
215-
if (is_float($newAutoIndex)) {
216-
$newAutoIndex = $max;
217-
}
218-
if (!$optional) {
219-
$this->nextAutoIndexes = [$newAutoIndex];
220-
} else {
221-
$this->nextAutoIndexes[] = $newAutoIndex;
222-
}
223-
}
224231
} else {
225232
$this->isList = TrinaryLogic::createNo();
226233
}
@@ -300,6 +307,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
300307
continue;
301308
}
302309

310+
if (count($this->nextAutoIndexes) === 0) {
311+
continue;
312+
}
313+
303314
$max = max($this->nextAutoIndexes);
304315
$offsetValue = $scalarType->getValue();
305316
if ($offsetValue < $max) {
@@ -309,7 +320,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
309320
/** @var int|float $newAutoIndex */
310321
$newAutoIndex = $offsetValue + 1;
311322
if (is_float($newAutoIndex)) {
312-
$newAutoIndex = $max;
323+
continue;
313324
}
314325
$this->nextAutoIndexes[] = $newAutoIndex;
315326
}

src/Type/Generic/TemplateConstantArrayType.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
use PHPStan\TrinaryLogic;
66
use PHPStan\Type\Constant\ConstantArrayType;
7-
use PHPStan\Type\Constant\ConstantIntegerType;
8-
use PHPStan\Type\Constant\ConstantStringType;
97
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
108
use PHPStan\Type\Type;
119

@@ -38,12 +36,6 @@ public function __construct(
3836
$this->default = $default;
3937
}
4038

41-
/**
42-
* @param list<ConstantIntegerType|ConstantStringType> $keyTypes
43-
* @param array<int, Type> $valueTypes
44-
* @param non-empty-list<int> $nextAutoIndexes
45-
* @param int[] $optionalKeys
46-
*/
4739
protected function recreate(
4840
array $keyTypes,
4941
array $valueTypes,

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ public function testExtendsPdoStatementCrash(): void
164164
$this->assertNoErrors($errors);
165165
}
166166

167+
public function testBug14548(): void
168+
{
169+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-14548.php');
170+
$this->assertNoErrors($errors);
171+
}
172+
167173
public function testBug12803(): void
168174
{
169175
// crash
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Bug14548;
4+
5+
class TProcessHelper
6+
{
7+
8+
public static function setProcessPriority(int $priority): void
9+
{
10+
$priorityValues = [ // The priority cap to windows text priority.
11+
-15 => 24,
12+
-10 => 13,
13+
-5 => 10,
14+
4 => 8,
15+
9 => 6,
16+
PHP_INT_MAX => 4,
17+
];
18+
foreach ($priorityValues as $keyPriority => $priorityName) {
19+
if ($priority <= $keyPriority) {
20+
break;
21+
}
22+
}
23+
}
24+
}

tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,19 @@ public function testBug8648(): void
207207
$this->analyse([__DIR__ . '/data/bug-8648.php'], []);
208208
}
209209

210+
public function testAppendToArrayWithPhpIntMaxKey(): void
211+
{
212+
$this->checkUnionTypes = true;
213+
$this->analyse([__DIR__ . '/data/offset-access-assignment-php-int-max.php'], [
214+
[
215+
'Cannot assign new offset to array<int, int>.',
216+
9,
217+
],
218+
[
219+
'Cannot assign new offset to array<int, int>.',
220+
15,
221+
],
222+
]);
223+
}
224+
210225
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace OffsetAccessAssignmentPhpIntMax;
4+
5+
function (): void {
6+
$a = [
7+
9223372036854775807 => 4,
8+
];
9+
$a[] = 5;
10+
};
11+
12+
function (): void {
13+
$a = [];
14+
$a[9223372036854775807] = 4;
15+
$a[] = 5;
16+
};
17+
18+
function (): void {
19+
$a = [
20+
9223372036854775807 => 4,
21+
];
22+
$a[10] = 5;
23+
};

tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
use PHPStan\Testing\PHPStanTestCase;
66
use PHPStan\Type\BooleanType;
7+
use PHPStan\Type\ErrorType;
78
use PHPStan\Type\IntegerType;
89
use PHPStan\Type\NullType;
910
use PHPStan\Type\StringType;
1011
use PHPStan\Type\TypeCombinator;
1112
use PHPStan\Type\VerbosityLevel;
13+
use function sprintf;
14+
use const PHP_INT_MAX;
1215

1316
class ConstantArrayTypeBuilderTest extends PHPStanTestCase
1417
{
@@ -199,4 +202,69 @@ public function testIsListWithUnion(): void
199202
$this->assertFalse($builder->isList());
200203
}
201204

205+
public function testAppendToBuilderWithEmptyNextAutoIndexes(): void
206+
{
207+
$builder = ConstantArrayTypeBuilder::createFromConstantArray(new ConstantArrayType(
208+
[new ConstantIntegerType(PHP_INT_MAX)],
209+
[new ConstantIntegerType(4)],
210+
[],
211+
));
212+
213+
$builder->setOffsetValueType(null, new ConstantIntegerType(5));
214+
215+
$array = $builder->getArray();
216+
$this->assertInstanceOf(ConstantArrayType::class, $array);
217+
$this->assertSame(sprintf('array{%d: 4}', PHP_INT_MAX), $array->describe(VerbosityLevel::precise()));
218+
$this->assertSame([], $array->getNextAutoIndexes());
219+
}
220+
221+
public function testAddIntegerOffsetToBuilderWithEmptyNextAutoIndexes(): void
222+
{
223+
$builder = ConstantArrayTypeBuilder::createFromConstantArray(new ConstantArrayType(
224+
[new ConstantIntegerType(PHP_INT_MAX)],
225+
[new ConstantIntegerType(4)],
226+
[],
227+
));
228+
229+
$builder->setOffsetValueType(new ConstantIntegerType(5), new ConstantStringType('x'));
230+
231+
$array = $builder->getArray();
232+
$this->assertInstanceOf(ConstantArrayType::class, $array);
233+
$this->assertSame(sprintf("array{%d: 4, 5: 'x'}", PHP_INT_MAX), $array->describe(VerbosityLevel::precise()));
234+
$this->assertSame([], $array->getNextAutoIndexes());
235+
$this->assertFalse($builder->isList());
236+
}
237+
238+
public function testAddIntegerUnionOffsetToBuilderWithEmptyNextAutoIndexes(): void
239+
{
240+
$builder = ConstantArrayTypeBuilder::createFromConstantArray(new ConstantArrayType(
241+
[new ConstantIntegerType(PHP_INT_MAX)],
242+
[new ConstantIntegerType(4)],
243+
[],
244+
));
245+
246+
$twoOrThree = TypeCombinator::union(
247+
new ConstantIntegerType(2),
248+
new ConstantIntegerType(3),
249+
);
250+
$builder->setOffsetValueType($twoOrThree, new ConstantStringType('x'));
251+
252+
$array = $builder->getArray();
253+
$this->assertInstanceOf(ConstantArrayType::class, $array);
254+
$this->assertSame(sprintf("array{%d: 4, 2?: 'x', 3?: 'x'}", PHP_INT_MAX), $array->describe(VerbosityLevel::precise()));
255+
$this->assertSame([], $array->getNextAutoIndexes());
256+
}
257+
258+
public function testSetOffsetValueTypeOnConstantArrayWithEmptyNextAutoIndexesReturnsErrorType(): void
259+
{
260+
$arrayType = new ConstantArrayType(
261+
[new ConstantIntegerType(PHP_INT_MAX)],
262+
[new ConstantIntegerType(4)],
263+
[],
264+
);
265+
266+
$result = $arrayType->setOffsetValueType(null, new ConstantIntegerType(5));
267+
$this->assertInstanceOf(ErrorType::class, $result);
268+
}
269+
202270
}

0 commit comments

Comments
 (0)