Skip to content

Commit

Permalink
array_filter() for constant arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 26, 2022
1 parent a6cec39 commit 771b860
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 19 deletions.
72 changes: 54 additions & 18 deletions src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php
Expand Up @@ -56,6 +56,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
$keyType = $arrayArgType->getIterableKeyType();
$itemType = $arrayArgType->getIterableValueType();

if ($itemType instanceof NeverType || $keyType instanceof NeverType) {
return new ConstantArrayType([], []);
}

if ($arrayArgType instanceof MixedType) {
return new BenevolentUnionType([
new ArrayType(new MixedType(), new MixedType()),
Expand All @@ -73,52 +77,48 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
$statement = $callbackArg->stmts[0];
if ($statement instanceof Return_ && $statement->expr !== null) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $statement->expr);
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $statement->expr);
}
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $callbackArg->expr);
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $callbackArg->expr);
} elseif ($callbackArg instanceof String_) {
$itemVar = new Variable('item');
$expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar)]);
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, null, $keyType, $expr);
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr);
}
}

if ($flagArg instanceof ConstFetch && $flagArg->name->parts[0] === 'ARRAY_FILTER_USE_KEY') {
if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
$statement = $callbackArg->stmts[0];
if ($statement instanceof Return_ && $statement->expr !== null) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $statement->expr);
return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $statement->expr);
}
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $callbackArg->expr);
return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $callbackArg->expr);
} elseif ($callbackArg instanceof String_) {
$keyVar = new Variable('key');
$expr = new FuncCall(new Name($callbackArg->value), [new Arg($keyVar)]);
[$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $keyVar, $keyType, $expr);
return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr);
}
}

if ($flagArg instanceof ConstFetch && $flagArg->name->parts[0] === 'ARRAY_FILTER_USE_BOTH') {
if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
$statement = $callbackArg->stmts[0];
if ($statement instanceof Return_ && $statement->expr !== null) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, $callbackArg->params[1]->var ?? null, $keyType, $statement->expr);
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $statement->expr);
}
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, $callbackArg->params[1]->var ?? null, $keyType, $callbackArg->expr);
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $callbackArg->expr);
} elseif ($callbackArg instanceof String_) {
$itemVar = new Variable('item');
$keyVar = new Variable('key');
$expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]);
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, $keyVar, $keyType, $expr);
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr);
}
}

if ($itemType instanceof NeverType || $keyType instanceof NeverType) {
return new ConstantArrayType([], []);
}

return new ArrayType($keyType, $itemType);
}

Expand Down Expand Up @@ -157,15 +157,51 @@ public function removeFalsey(Type $type): Type
return new ArrayType($keyType, $valueType);
}

/**
* @return array{Type, Type}
*/
private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $itemType, Error|Variable|null $keyVar, Type $keyType, Expr $expr): array
private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $arrayType, Error|Variable|null $keyVar, Expr $expr): Type
{
if (!$scope instanceof MutatingScope) {
throw new ShouldNotHappenException();
}

$constantArrays = TypeUtils::getOldConstantArrays($arrayType);
if (count($constantArrays) > 0) {
$results = [];
foreach ($constantArrays as $constantArray) {
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
$itemType = $constantArray->getValueTypes()[$i];
[$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $keyType, $itemType, $itemVar, $keyVar, $expr);
if ($newKeyType instanceof NeverType || $newItemType instanceof NeverType) {
continue;
}
if ($itemType->equals($newItemType) && $keyType->equals($newKeyType)) {
$builder->setOffsetValueType($keyType, $itemType);
continue;
}

$builder->setOffsetValueType($newKeyType, $newItemType, true);
}

$results[] = $builder->getArray();
}

return TypeCombinator::union(...$results);
}

[$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $arrayType->getIterableKeyType(), $arrayType->getIterableValueType(), $itemVar, $keyVar, $expr);

if ($newItemType instanceof NeverType || $newKeyType instanceof NeverType) {
return new ConstantArrayType([], []);
}

return new ArrayType($newKeyType, $newItemType);
}

/**
* @return array{Type, Type}
*/
private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type $itemType, Error|Variable|null $itemVar, Error|Variable|null $keyVar, Expr $expr): array
{
$itemVarName = null;
if ($itemVar !== null) {
if (!$itemVar instanceof Variable || !is_string($itemVar->name)) {
Expand All @@ -187,8 +223,8 @@ private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar,
$scope = $scope->filterByTruthyValue($expr);

return [
$itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType,
$keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType,
$itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType,
];
}

Expand Down
6 changes: 5 additions & 1 deletion tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Expand Up @@ -4455,9 +4455,13 @@ public function dataArrayFunctions(): array
'$filteredIntegers[0]',
],
[
'123',
'*ERROR*',
'$filteredMixed[0]',
],
[
'123',
'$filteredMixed[1]',
],
[
'non-empty-array<0|1|2, 1|2|3>',
'$uniquedIntegers',
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -996,6 +996,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7764.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5845.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-constant.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-constant.php');
}

/**
Expand Down
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/data/array-filter-constant.php
@@ -0,0 +1,32 @@
<?php

namespace ArrayFilterConstantArray;

use function array_filter;
use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param array{a: int}|array{b: string|null} $a
* @return void
*/
public function doFoo(array $a): void
{
assertType('array{a: int}|array{b: string|null}', $a);
assertType('array{a?: int<min, -1>|int<1, max>}|array{b?: non-falsy-string}', array_filter($a));

assertType('array{a: int}|array{b?: string}', array_filter($a, function ($v): bool {
return $v !== null;
}));

$a = ['a' => 1, 'b' => null];
assertType('array{a: 1}', array_filter($a, function ($v): bool {
return $v !== null;
}));

assertType('array{a: 1}', array_filter($a));
}

}

0 comments on commit 771b860

Please sign in to comment.