Skip to content

Commit 771b860

Browse files
committed
array_filter() for constant arrays
1 parent a6cec39 commit 771b860

File tree

4 files changed

+92
-19
lines changed

4 files changed

+92
-19
lines changed

src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php

+54-18
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5656
$keyType = $arrayArgType->getIterableKeyType();
5757
$itemType = $arrayArgType->getIterableValueType();
5858

59+
if ($itemType instanceof NeverType || $keyType instanceof NeverType) {
60+
return new ConstantArrayType([], []);
61+
}
62+
5963
if ($arrayArgType instanceof MixedType) {
6064
return new BenevolentUnionType([
6165
new ArrayType(new MixedType(), new MixedType()),
@@ -73,52 +77,48 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
7377
if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
7478
$statement = $callbackArg->stmts[0];
7579
if ($statement instanceof Return_ && $statement->expr !== null) {
76-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $statement->expr);
80+
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $statement->expr);
7781
}
7882
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
79-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $callbackArg->expr);
83+
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $callbackArg->expr);
8084
} elseif ($callbackArg instanceof String_) {
8185
$itemVar = new Variable('item');
8286
$expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar)]);
83-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, null, $keyType, $expr);
87+
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr);
8488
}
8589
}
8690

8791
if ($flagArg instanceof ConstFetch && $flagArg->name->parts[0] === 'ARRAY_FILTER_USE_KEY') {
8892
if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
8993
$statement = $callbackArg->stmts[0];
9094
if ($statement instanceof Return_ && $statement->expr !== null) {
91-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $statement->expr);
95+
return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $statement->expr);
9296
}
9397
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
94-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $callbackArg->expr);
98+
return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $callbackArg->expr);
9599
} elseif ($callbackArg instanceof String_) {
96100
$keyVar = new Variable('key');
97101
$expr = new FuncCall(new Name($callbackArg->value), [new Arg($keyVar)]);
98-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $keyVar, $keyType, $expr);
102+
return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr);
99103
}
100104
}
101105

102106
if ($flagArg instanceof ConstFetch && $flagArg->name->parts[0] === 'ARRAY_FILTER_USE_BOTH') {
103107
if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
104108
$statement = $callbackArg->stmts[0];
105109
if ($statement instanceof Return_ && $statement->expr !== null) {
106-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, $callbackArg->params[1]->var ?? null, $keyType, $statement->expr);
110+
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $statement->expr);
107111
}
108112
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
109-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, $callbackArg->params[1]->var ?? null, $keyType, $callbackArg->expr);
113+
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $callbackArg->expr);
110114
} elseif ($callbackArg instanceof String_) {
111115
$itemVar = new Variable('item');
112116
$keyVar = new Variable('key');
113117
$expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]);
114-
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, $keyVar, $keyType, $expr);
118+
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr);
115119
}
116120
}
117121

118-
if ($itemType instanceof NeverType || $keyType instanceof NeverType) {
119-
return new ConstantArrayType([], []);
120-
}
121-
122122
return new ArrayType($keyType, $itemType);
123123
}
124124

@@ -157,15 +157,51 @@ public function removeFalsey(Type $type): Type
157157
return new ArrayType($keyType, $valueType);
158158
}
159159

160-
/**
161-
* @return array{Type, Type}
162-
*/
163-
private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $itemType, Error|Variable|null $keyVar, Type $keyType, Expr $expr): array
160+
private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $arrayType, Error|Variable|null $keyVar, Expr $expr): Type
164161
{
165162
if (!$scope instanceof MutatingScope) {
166163
throw new ShouldNotHappenException();
167164
}
168165

166+
$constantArrays = TypeUtils::getOldConstantArrays($arrayType);
167+
if (count($constantArrays) > 0) {
168+
$results = [];
169+
foreach ($constantArrays as $constantArray) {
170+
$builder = ConstantArrayTypeBuilder::createEmpty();
171+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
172+
$itemType = $constantArray->getValueTypes()[$i];
173+
[$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $keyType, $itemType, $itemVar, $keyVar, $expr);
174+
if ($newKeyType instanceof NeverType || $newItemType instanceof NeverType) {
175+
continue;
176+
}
177+
if ($itemType->equals($newItemType) && $keyType->equals($newKeyType)) {
178+
$builder->setOffsetValueType($keyType, $itemType);
179+
continue;
180+
}
181+
182+
$builder->setOffsetValueType($newKeyType, $newItemType, true);
183+
}
184+
185+
$results[] = $builder->getArray();
186+
}
187+
188+
return TypeCombinator::union(...$results);
189+
}
190+
191+
[$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $arrayType->getIterableKeyType(), $arrayType->getIterableValueType(), $itemVar, $keyVar, $expr);
192+
193+
if ($newItemType instanceof NeverType || $newKeyType instanceof NeverType) {
194+
return new ConstantArrayType([], []);
195+
}
196+
197+
return new ArrayType($newKeyType, $newItemType);
198+
}
199+
200+
/**
201+
* @return array{Type, Type}
202+
*/
203+
private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type $itemType, Error|Variable|null $itemVar, Error|Variable|null $keyVar, Expr $expr): array
204+
{
169205
$itemVarName = null;
170206
if ($itemVar !== null) {
171207
if (!$itemVar instanceof Variable || !is_string($itemVar->name)) {
@@ -187,8 +223,8 @@ private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar,
187223
$scope = $scope->filterByTruthyValue($expr);
188224

189225
return [
190-
$itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType,
191226
$keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType,
227+
$itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType,
192228
];
193229
}
194230

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -4455,9 +4455,13 @@ public function dataArrayFunctions(): array
44554455
'$filteredIntegers[0]',
44564456
],
44574457
[
4458-
'123',
4458+
'*ERROR*',
44594459
'$filteredMixed[0]',
44604460
],
4461+
[
4462+
'123',
4463+
'$filteredMixed[1]',
4464+
],
44614465
[
44624466
'non-empty-array<0|1|2, 1|2|3>',
44634467
'$uniquedIntegers',

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,7 @@ public function dataFileAsserts(): iterable
996996
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7764.php');
997997
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5845.php');
998998
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-constant.php');
999+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-constant.php');
9991000
}
10001001

10011002
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace ArrayFilterConstantArray;
4+
5+
use function array_filter;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
11+
/**
12+
* @param array{a: int}|array{b: string|null} $a
13+
* @return void
14+
*/
15+
public function doFoo(array $a): void
16+
{
17+
assertType('array{a: int}|array{b: string|null}', $a);
18+
assertType('array{a?: int<min, -1>|int<1, max>}|array{b?: non-falsy-string}', array_filter($a));
19+
20+
assertType('array{a: int}|array{b?: string}', array_filter($a, function ($v): bool {
21+
return $v !== null;
22+
}));
23+
24+
$a = ['a' => 1, 'b' => null];
25+
assertType('array{a: 1}', array_filter($a, function ($v): bool {
26+
return $v !== null;
27+
}));
28+
29+
assertType('array{a: 1}', array_filter($a));
30+
}
31+
32+
}

0 commit comments

Comments
 (0)