Skip to content

Commit 70ebcb1

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents e1bece8 + c8106c3 commit 70ebcb1

5 files changed

Lines changed: 140 additions & 8 deletions

File tree

src/Type/Constant/ConstantArrayTypeBuilder.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ final class ConstantArrayTypeBuilder
4141

4242
private bool $oversized = false;
4343

44+
private TrinaryLogic $isNonEmpty;
45+
4446
/**
4547
* @param list<Type> $keyTypes
4648
* @param array<int, Type> $valueTypes
@@ -55,6 +57,7 @@ private function __construct(
5557
private TrinaryLogic $isList,
5658
)
5759
{
60+
$this->isNonEmpty = TrinaryLogic::createNo();
5861
}
5962

6063
public static function createEmpty(): self
@@ -71,6 +74,7 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType
7174
$startArrayType->getOptionalKeys(),
7275
$startArrayType->isList(),
7376
);
77+
$builder->isNonEmpty = $startArrayType->isIterableAtLeastOnce();
7478

7579
if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) {
7680
$builder->degradeToGeneralArray(true);
@@ -89,6 +93,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
8993
return;
9094
}
9195

96+
if (!$optional) {
97+
$this->isNonEmpty = TrinaryLogic::createYes();
98+
}
99+
92100
if (!$this->degradeToGeneralArray) {
93101
if (
94102
$valueType instanceof ClosureType
@@ -388,7 +396,11 @@ public function getArray(): Type
388396
if (!$this->degradeToGeneralArray) {
389397
/** @var list<ConstantIntegerType|ConstantStringType> $keyTypes */
390398
$keyTypes = $this->keyTypes;
391-
return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
399+
$array = new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
400+
if ($this->isNonEmpty->yes() && !$array->isIterableAtLeastOnce()->yes()) {
401+
return TypeCombinator::intersect($array, new NonEmptyArrayType());
402+
}
403+
return $array;
392404
}
393405

394406
if ($this->degradeClosures === true) {
@@ -410,7 +422,7 @@ public function getArray(): Type
410422
);
411423

412424
$types = [];
413-
if (count($this->optionalKeys) < $keyTypesCount) {
425+
if ($this->isNonEmpty->yes() || count($this->optionalKeys) < $keyTypesCount) {
414426
$types[] = new NonEmptyArrayType();
415427
}
416428

tests/PHPStan/Analyser/nsrt/bug-14551.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function foreachTwoKeys(array $keys): void
1515
$result[$k]['y'] = 2;
1616
}
1717

18-
assertType("array{a?: array{x: 1, y: 2}, b?: array{x: 1, y: 2}}", $result);
18+
assertType("non-empty-array{a?: array{x: 1, y: 2}, b?: array{x: 1, y: 2}}", $result);
1919
}
2020

2121
/**
@@ -30,7 +30,7 @@ function foreachThreeKeys(array $keys): void
3030
$result[$k]['z'] = 3;
3131
}
3232

33-
assertType("array{a?: array{x: 1, y: 2, z: 3}, b?: array{x: 1, y: 2, z: 3}}", $result);
33+
assertType("non-empty-array{a?: array{x: 1, y: 2, z: 3}, b?: array{x: 1, y: 2, z: 3}}", $result);
3434
}
3535

3636
/**
@@ -43,7 +43,7 @@ function withoutForeach(string $k): void
4343
$result[$k]['x'] = 1;
4444
$result[$k]['y'] = 2;
4545

46-
assertType("array{a?: array{x: 1, y: 2}, b?: array{x: 1, y: 2}}", $result);
46+
assertType("non-empty-array{a?: array{x: 1, y: 2}, b?: array{x: 1, y: 2}}", $result);
4747
}
4848

4949
/**
@@ -56,7 +56,7 @@ function integerKeys(int $k): void
5656
$result[$k]['x'] = 1;
5757
$result[$k]['y'] = 2;
5858

59-
assertType("array{0?: array{x: 1, y: 2}, 1?: array{x: 1, y: 2}}", $result);
59+
assertType("non-empty-array{0?: array{x: 1, y: 2}, 1?: array{x: 1, y: 2}}", $result);
6060
}
6161

6262
/**
@@ -69,7 +69,7 @@ function threeWayUnion(string $k): void
6969
$result[$k]['x'] = 1;
7070
$result[$k]['y'] = 2;
7171

72-
assertType("array{a?: array{x: 1, y: 2}, b?: array{x: 1, y: 2}, c?: array{x: 1, y: 2}}", $result);
72+
assertType("non-empty-array{a?: array{x: 1, y: 2}, b?: array{x: 1, y: 2}, c?: array{x: 1, y: 2}}", $result);
7373
}
7474

7575
/**
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14552;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param non-empty-list<'a'|'b'> $keys
9+
*/
10+
function nonEmptyListForeach(array $keys): void
11+
{
12+
$out = [];
13+
foreach ($keys as $k) {
14+
$out[$k] = 1;
15+
}
16+
assertType("non-empty-array{a?: 1, b?: 1}", $out);
17+
}
18+
19+
/**
20+
* @param list<'a'|'b'> $keys
21+
*/
22+
function possiblyEmptyListForeach(array $keys): void
23+
{
24+
$out = [];
25+
foreach ($keys as $k) {
26+
$out[$k] = 1;
27+
}
28+
assertType("array{}|array{a?: 1, b?: 1}", $out);
29+
}
30+
31+
/**
32+
* @param non-empty-list<'x'|'y'|'z'> $keys
33+
*/
34+
function nonEmptyListThreeKeys(array $keys): void
35+
{
36+
$out = [];
37+
foreach ($keys as $k) {
38+
$out[$k] = true;
39+
}
40+
assertType("non-empty-array{x?: true, y?: true, z?: true}", $out);
41+
}
42+
43+
/**
44+
* Direct assignment (non-foreach) with union key on empty array.
45+
* @param 'a'|'b' $key
46+
*/
47+
function directAssignment(string $key): void
48+
{
49+
$arr = [];
50+
$arr[$key] = 1;
51+
assertType("non-empty-array{a?: 1, b?: 1}", $arr);
52+
}
53+
54+
/**
55+
* Direct assignment with integer union key on empty array.
56+
* @param 0|1|2 $key
57+
*/
58+
function directAssignmentIntKey(int $key): void
59+
{
60+
$arr = [];
61+
$arr[$key] = 'val';
62+
assertType("non-empty-array{0?: 'val', 1?: 'val', 2?: 'val'}", $arr);
63+
}
64+
65+
/**
66+
* Setting union key on already non-empty array should stay non-empty.
67+
* @param 'x'|'y' $key
68+
*/
69+
function setOnNonEmptyArray(string $key): void
70+
{
71+
$arr = ['existing' => 0];
72+
$arr[$key] = 1;
73+
assertType("array{existing: 0, x?: 1, y?: 1}", $arr);
74+
}

tests/PHPStan/Analyser/nsrt/set-union-offset-preserve-constant-array.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function doFoo(): void
1313

1414
$k = rand(0, 1) ? 1 : 2;
1515
$a[$k] = true;
16-
assertType('array{1?: true, 2?: true}', $a);
16+
assertType('non-empty-array{1?: true, 2?: true}', $a);
1717
}
1818

1919
}

tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,50 @@ public function testSetOffsetValueTypeOnConstantArrayWithEmptyNextAutoIndexesRet
267267
$this->assertInstanceOf(ErrorType::class, $result);
268268
}
269269

270+
public function testNonOptionalUnionOffsetOnEmptyArrayIsNonEmpty(): void
271+
{
272+
$builder = ConstantArrayTypeBuilder::createEmpty();
273+
274+
$aOrB = TypeCombinator::union(
275+
new ConstantStringType('a'),
276+
new ConstantStringType('b'),
277+
);
278+
$builder->setOffsetValueType($aOrB, new ConstantIntegerType(1));
279+
280+
$array = $builder->getArray();
281+
$this->assertSame('non-empty-array{a?: 1, b?: 1}', $array->describe(VerbosityLevel::precise()));
282+
}
283+
284+
public function testOptionalSingleOffsetOnEmptyArrayIsPossiblyEmpty(): void
285+
{
286+
$builder = ConstantArrayTypeBuilder::createEmpty();
287+
$builder->setOffsetValueType(new ConstantStringType('a'), new ConstantIntegerType(1), true);
288+
289+
$array = $builder->getArray();
290+
$this->assertSame('array{a?: 1}', $array->describe(VerbosityLevel::precise()));
291+
}
292+
293+
public function testOptionalUnionOffsetOnEmptyArrayIsPossiblyEmpty(): void
294+
{
295+
$builder = ConstantArrayTypeBuilder::createEmpty();
296+
297+
$aOrB = TypeCombinator::union(
298+
new ConstantStringType('a'),
299+
new ConstantStringType('b'),
300+
);
301+
$builder->setOffsetValueType($aOrB, new ConstantIntegerType(1), true);
302+
303+
$array = $builder->getArray();
304+
$this->assertSame('array{a?: 1, b?: 1}', $array->describe(VerbosityLevel::precise()));
305+
}
306+
307+
public function testOptionalNullOffsetOnEmptyArrayIsPossiblyEmpty(): void
308+
{
309+
$builder = ConstantArrayTypeBuilder::createEmpty();
310+
$builder->setOffsetValueType(null, new ConstantIntegerType(1), true);
311+
312+
$array = $builder->getArray();
313+
$this->assertSame('array{0?: 1}', $array->describe(VerbosityLevel::precise()));
314+
}
315+
270316
}

0 commit comments

Comments
 (0)