Skip to content

Commit 009847e

Browse files
committed
narrow types in foreach expr
1 parent c37eb1c commit 009847e

File tree

8 files changed

+88
-23
lines changed

8 files changed

+88
-23
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,9 @@ private function processStmtNode(
12491249

12501250
$exprType = $scope->getType($stmt->expr);
12511251
$isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
1252-
if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) {
1252+
if (
1253+
$exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()
1254+
) {
12531255
$finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
12541256
new BinaryOp\Identical(
12551257
$stmt->expr,
@@ -6312,6 +6314,28 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope
63126314
if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
63136315
$scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
63146316
}
6317+
6318+
$foreachType = TypeCombinator::union(
6319+
TypeCombinator::intersect(
6320+
new ArrayType(new MixedType(), new MixedType()),
6321+
new NonEmptyArrayType(),
6322+
),
6323+
new ObjectType(Traversable::class),
6324+
);
6325+
6326+
$scope = $scope->specifyExpressionType(
6327+
$stmt->expr,
6328+
TypeCombinator::intersect(
6329+
$scope->getType($stmt->expr),
6330+
$foreachType,
6331+
),
6332+
TypeCombinator::intersect(
6333+
$scope->getNativeType($stmt->expr),
6334+
$foreachType,
6335+
),
6336+
TrinaryLogic::createYes(),
6337+
);
6338+
63156339
$iterateeType = $originalScope->getType($stmt->expr);
63166340
if (
63176341
($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5816,7 +5816,7 @@ public static function dataIterable(): array
58165816
'$iterableWithoutTypehint[0]',
58175817
],
58185818
[
5819-
'iterable',
5819+
'non-empty-array|Traversable',
58205820
'$iterableWithIterableTypehint',
58215821
],
58225822
[
@@ -5828,7 +5828,7 @@ public static function dataIterable(): array
58285828
'$mixed',
58295829
],
58305830
[
5831-
'iterable<Iterables\Bar>',
5831+
'non-empty-array<Iterables\Bar>|(iterable<Iterables\Bar>&Traversable)',
58325832
'$iterableWithConcreteTypehint',
58335833
],
58345834
[
@@ -5844,7 +5844,7 @@ public static function dataIterable(): array
58445844
'$this->doBar()',
58455845
],
58465846
[
5847-
'iterable<Iterables\Baz>',
5847+
'non-empty-array<Iterables\Baz>|(iterable<Iterables\Baz>&Traversable)',
58485848
'$this->doBaz()',
58495849
],
58505850
[

tests/PHPStan/Analyser/nsrt/bug-13270a.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ final class HelloWorld
1414
public function test(array $data): void
1515
{
1616
foreach($data as $k => $v) {
17-
assertType('non-empty-array<mixed>', $data);
17+
assertType('non-empty-array', $data);
1818
$data[$k]['a'] = true;
1919
assertType("non-empty-array<(non-empty-array&hasOffsetValue('a', true))|(ArrayAccess&hasOffsetValue('a', true))>", $data);
2020
foreach($data[$k] as $val) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Bug13312;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function fooArr(array $arr): void {
8+
assertType('array', $arr);
9+
foreach ($arr as $v) {
10+
assertType('non-empty-array', $arr);
11+
}
12+
assertType('array', $arr);
13+
14+
for ($i = 0; $i < count($arr); ++$i) {
15+
assertType('non-empty-array', $arr);
16+
}
17+
assertType('array', $arr);
18+
}
19+
20+
/** @param list<mixed> $arr */
21+
function foo(array $arr): void {
22+
assertType('list<mixed>', $arr);
23+
foreach ($arr as $v) {
24+
assertType('non-empty-list<mixed>', $arr);
25+
}
26+
assertType('list<mixed>', $arr);
27+
28+
for ($i = 0; $i < count($arr); ++$i) {
29+
assertType('non-empty-list<mixed>', $arr);
30+
}
31+
assertType('list<mixed>', $arr);
32+
}
33+
34+
35+
function fooBar(mixed $mixed): void {
36+
assertType('mixed', $mixed);
37+
foreach ($mixed as $v) {
38+
assertType('non-empty-array|Traversable', $mixed);
39+
}
40+
assertType('array|Traversable', $mixed);
41+
}

tests/PHPStan/Analyser/nsrt/composer-array-bug.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,26 @@ public function doFoo(): void
1818
if (!empty($this->config['authors'])) {
1919
assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $this->config['authors']);
2020
foreach ($this->config['authors'] as $key => $author) {
21-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
21+
assertType("non-empty-array|Traversable", $this->config['authors']);
2222

2323
if (!is_array($author)) {
2424
$this->errors[] = 'authors.'.$key.' : should be an array, '.gettype($author).' given';
25-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
25+
assertType("non-empty-array|Traversable", $this->config['authors']);
2626
unset($this->config['authors'][$key]);
27-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
27+
assertType("array|Traversable", $this->config['authors']);
2828
continue;
2929
}
30-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
30+
assertType("non-empty-array|Traversable", $this->config['authors']);
3131
foreach (['homepage', 'email', 'name', 'role'] as $authorData) {
3232
if (isset($author[$authorData]) && !is_string($author[$authorData])) {
3333
$this->errors[] = 'authors.'.$key.'.'.$authorData.' : invalid value, must be a string';
3434
unset($this->config['authors'][$key][$authorData]);
3535
}
3636
}
3737
if (isset($author['homepage'])) {
38-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
38+
assertType("non-empty-array|Traversable", $this->config['authors']);
3939
unset($this->config['authors'][$key]['homepage']);
40-
assertType("array|ArrayAccess|string", $this->config['authors']);
40+
assertType("non-empty-array|Traversable", $this->config['authors']);
4141
}
4242
if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) {
4343
unset($this->config['authors'][$key]['email']);
@@ -47,14 +47,14 @@ public function doFoo(): void
4747
}
4848
}
4949

50-
assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|0.0|false|null))", $this->config);
51-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
50+
assertType("non-empty-array&hasOffsetValue('authors', array|Traversable)", $this->config);
51+
assertType("array|Traversable", $this->config['authors']);
5252

5353
if (empty($this->config['authors'])) {
5454
unset($this->config['authors']);
5555
assertType("array<mixed~'authors', mixed>", $this->config);
5656
} else {
57-
assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|0.0|''|'0'|array{}|false|null))", $this->config);
57+
assertType("non-empty-array&hasOffsetValue('authors', non-empty-array|Traversable)", $this->config);
5858
}
5959

6060
assertType('array', $this->config);

tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ public function doFoo()
1515
if (!empty($this->config['authors'])) {
1616
assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $this->config['authors']);
1717
foreach ($this->config['authors'] as $key => $author) {
18-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
18+
assertType("non-empty-array|Traversable", $this->config['authors']);
1919
if (!is_array($author)) {
2020
unset($this->config['authors'][$key]);
21-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
21+
assertType("array|Traversable", $this->config['authors']);
2222
continue;
2323
}
2424
foreach (['homepage', 'email', 'name', 'role'] as $authorData) {
@@ -33,13 +33,13 @@ public function doFoo()
3333
unset($this->config['authors'][$key]['email']);
3434
}
3535
if (empty($this->config['authors'][$key])) {
36-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
36+
assertType("non-empty-array|Traversable", $this->config['authors']);
3737
unset($this->config['authors'][$key]);
38-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
38+
assertType("array|Traversable", $this->config['authors']);
3939
}
40-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
40+
assertType("array|Traversable", $this->config['authors']);
4141
}
42-
assertType("mixed~(0|0.0|false|null)", $this->config['authors']);
42+
assertType("array|Traversable", $this->config['authors']);
4343
}
4444
}
4545

tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function getTaxBreakdown($mixed, $arrayMixed): void
2121

2222
foreach ($breakdowns as $type => $bd) {
2323
if (empty($bd)) {
24-
assertType('array{product_tax?: mixed, shipping_tax?: array<mixed>, ecotax_tax?: array<mixed>, wrapping_tax?: array<mixed>}', $breakdowns);
24+
assertType('non-empty-array{product_tax?: mixed, shipping_tax?: array<mixed>, ecotax_tax?: array<mixed>, wrapping_tax?: array<mixed>}', $breakdowns);
2525
unset($breakdowns[$type]);
2626
assertType('array{product_tax?: mixed, shipping_tax?: array<mixed>, ecotax_tax?: array<mixed>, wrapping_tax?: array<mixed>}', $breakdowns);
2727
}

tests/PHPStan/Rules/Arrays/data/slevomat-foreach-unset-bug.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ public function doFoo()
1818
continue;
1919
}
2020

21-
assertType('array{items: array<string, stdClass>, isActive: bool, productsCount: int}', $this->foreignSection);
22-
assertType('array<string, stdClass>', $this->foreignSection['items']);
21+
assertType('array{items: non-empty-array<string, stdClass>, isActive: bool, productsCount: int}', $this->foreignSection);
22+
assertType('non-empty-array<string, stdClass>', $this->foreignSection['items']);
2323
unset($this->foreignSection['items'][$foreignCountryNo]);
2424
assertType('array{items: array<string, stdClass>, isActive: bool, productsCount: int}', $this->foreignSection);
2525
assertType('array<string, stdClass>', $this->foreignSection['items']);

0 commit comments

Comments
 (0)