Skip to content

Commit 1788ab6

Browse files
Updated array type inference when assigning to empty dimension - now arrays are inferred, not tuples (#1079)
* wip * Fix styling * wip * Fix styling * wip wip * wip wip * array incremental inference when dim is empty * Fix styling * fix type widening of explicit empty array and array --------- Co-authored-by: romalytvynenko <romalytvynenko@users.noreply.github.com>
1 parent 3991a1a commit 1788ab6

File tree

3 files changed

+66
-56
lines changed

3 files changed

+66
-56
lines changed

src/Support/Type/OffsetSetType.php

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,14 @@ public function resolve(): Type
3030
return new UnknownType;
3131
}
3232

33-
if ($this->type instanceof ArrayType) {
34-
return $this->type; // ??
35-
}
36-
3733
$path = $this->normalizePath($this->offset);
3834
if (! $path) {
3935
return new UnknownType;
4036
}
4137

42-
return $this->applyPath($this->type->clone(), $path, $this->value);
38+
$result = $this->type->clone();
39+
40+
return $this->applyPath($result, $path, $this->value);
4341
}
4442

4543
public function acceptedBy(Type $otherType): bool
@@ -68,78 +66,74 @@ public function toString(): string
6866
}
6967

7068
/**
71-
* @param array<int, int|string|null> $path
69+
* @param list<int|string|null> $path
7270
*/
73-
private function applyPath(KeyedArrayType $target, array $path, Type $value): KeyedArrayType
71+
private function applyPath(ArrayType|KeyedArrayType &$target, array $path, Type $value): ArrayType|KeyedArrayType
7472
{
75-
$modifyingType = $target;
76-
77-
foreach ($path as $i => $pathItem) {
78-
$isLast = $i === array_key_last($path);
79-
80-
$modifyingType = $isLast
81-
? $this->applyLeafAssignment($modifyingType, $pathItem, $value)
82-
: $this->applyIntermediateStep($modifyingType, $pathItem);
83-
84-
if ($modifyingType === null) {
85-
return $target;
86-
}
73+
if (count($path) === 0) {
74+
return $target;
8775
}
8876

89-
return $target;
90-
}
77+
$pathItem = array_shift($path);
9178

92-
private function applyIntermediateStep(KeyedArrayType $modifyingType, string|int|null $pathItem): ?KeyedArrayType
93-
{
94-
$targetItems = $modifyingType->items;
79+
if ($pathItem === null && $target instanceof KeyedArrayType && count($target->items) === 0) {
80+
$target = (new ArrayType)->mergeAttributes($target->attributes());
9581

96-
$targetItem = Arr::first(
97-
$targetItems,
98-
fn (ArrayItemType_ $t) => $t->key === $pathItem,
99-
);
82+
if ($path) {
83+
$target->value = new KeyedArrayType;
10084

101-
if ($targetItem) {
102-
if (! $targetItem->value instanceof KeyedArrayType) {
103-
return null;
85+
$this->applyPath($target->value, $path, $value);
86+
} else {
87+
$target->value = $value;
10488
}
105-
$newModifyingType = $targetItem->value;
106-
} else {
107-
$targetItem = new ArrayItemType_(
108-
key: $pathItem,
109-
value: $newModifyingType = new KeyedArrayType,
110-
);
111-
$targetItems[] = $targetItem;
89+
90+
return $target;
11291
}
11392

114-
$modifyingType->items = $targetItems;
115-
$modifyingType->isList = KeyedArrayType::checkIsList($targetItems);
93+
if ($target instanceof ArrayType) {
94+
$target->value = Union::wrap([$target->value, $value]);
11695

117-
return $newModifyingType;
118-
}
96+
$this->applyPath($target, $path, $value);
11997

120-
private function applyLeafAssignment(KeyedArrayType $modifyingType, string|int|null $pathItem, Type $value): KeyedArrayType
121-
{
122-
$targetItems = $modifyingType->items;
98+
return $target;
99+
}
123100

124-
$targetItem = $pathItem !== null ? Arr::first(
125-
$targetItems,
101+
$targetItem = Arr::first(
102+
$target->items,
126103
fn (ArrayItemType_ $t) => $t->key === $pathItem,
127-
) : null;
104+
);
128105

129106
if ($targetItem) {
130-
$targetItem->value = $value;
107+
if ($path) {
108+
if (! $targetItem->value instanceof KeyedArrayType && ! $targetItem->value instanceof ArrayType) {
109+
return $target;
110+
}
111+
112+
$this->applyPath($targetItem->value, $path, $value);
113+
} else {
114+
$targetItem->value = $value;
115+
}
131116
} else {
132-
$targetItems[] = $targetItem = new ArrayItemType_(key: $pathItem, value: $value);
117+
if ($path) {
118+
$targetItem = new ArrayItemType_(
119+
key: $pathItem,
120+
value: new KeyedArrayType,
121+
);
122+
$target->items[] = $targetItem;
123+
124+
$this->applyPath($targetItem->value, $path, $value); // @phpstan-ignore argument.type
125+
} else {
126+
$target->items[] = new ArrayItemType_(key: $pathItem, value: $value);
127+
}
133128
}
134129

135-
$modifyingType->items = $targetItems;
136-
$modifyingType->isList = KeyedArrayType::checkIsList($targetItems);
130+
$target->isList = KeyedArrayType::checkIsList($target->items);
137131

138-
return $modifyingType;
132+
return $target;
139133
}
140134

141135
/**
142-
* @return null|list<string|int|null>
136+
* @return null|non-empty-list<string|int|null>
143137
*/
144138
private function normalizePath(KeyedArrayType $path): ?array
145139
{
@@ -152,6 +146,7 @@ private function normalizePath(KeyedArrayType $path): ?array
152146

153147
continue;
154148
}
149+
155150
if ($pathItemType instanceof LiteralString || $pathItemType instanceof LiteralIntegerType) {
156151
$normalizedPath[] = $pathItemType->getValue();
157152

@@ -161,6 +156,10 @@ private function normalizePath(KeyedArrayType $path): ?array
161156
return null;
162157
}
163158

159+
if (! count($normalizedPath)) {
160+
return null;
161+
}
162+
164163
return $normalizedPath;
165164
}
166165
}

src/Support/Type/TypeWidener.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ private function widenPair(Type $a, Type $b): ?Type
8888
return new StringType;
8989
}
9090

91+
// list{}|array<T> -> array<T>
92+
if ($a instanceof KeyedArrayType && count($a->items) === 0 && $b instanceof ArrayType) {
93+
return $b;
94+
}
95+
9196
if (
9297
$a instanceof Generic
9398
&& $b instanceof Generic

tests/Support/Type/OffsetSetTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717
expect($a)->toHaveType('array{foo: int(42)}');
1818
});
1919

20+
it('infers empty array as keyed array type', function () {
21+
$a = [];
22+
23+
expect($a)->toHaveType('list{}');
24+
});
25+
2026
it('handles array push type', function () {
2127
$a = [];
2228
$a[] = 42;
2329
$a[] = 1;
2430

25-
expect($a)->toHaveType('list{int(42), int(1)}');
31+
expect($a)->toHaveType('array<int(42)|int(1)>');
2632
});
2733

2834
it('handles array modify type', function () {
@@ -52,7 +58,7 @@
5258
$a['foo']['bar'][] = 42;
5359
$a['foo']['bar'][] = 1;
5460

55-
expect($a)->toHaveType('array{foo: array{bar: list{int(42), int(1)}}}');
61+
expect($a)->toHaveType('array{foo: array{bar: array<int(42)|int(1)>}}');
5662
});
5763

5864
it('allows setting keys on template type', function () {

0 commit comments

Comments
 (0)