Skip to content

Commit

Permalink
Support narrowing down array_filter return type via string callable
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm authored and ondrejmirtes committed Feb 1, 2022
1 parent 34587e5 commit 1789915
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php
Expand Up @@ -2,13 +2,16 @@

namespace PHPStan\Type\Php;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Error;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\Scope;
Expand Down Expand Up @@ -74,6 +77,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $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);
}
}

Expand All @@ -85,6 +92,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
[$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $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);
}
}

Expand All @@ -96,6 +107,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}
} 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);
} 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);
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -281,6 +281,7 @@ public function dataFileAsserts(): iterable

yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-callables.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-string-callables.php');
if (PHP_VERSION_ID >= 70400) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-arrow-functions.php');
}
Expand Down
79 changes: 79 additions & 0 deletions tests/PHPStan/Analyser/data/array-filter-string-callables.php
@@ -0,0 +1,79 @@
<?php

namespace ArrayFilter;

use function PHPStan\Testing\assertType;

/**
* @param array<int, int> $list1
* @param array<int, int> $list2
* @param array<int, int> $list3
*/
function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void
{
$filtered1 = array_filter($list1, 'is_string');
assertType('array{}', $filtered1);

$filtered2 = array_filter($list2, 'is_string', ARRAY_FILTER_USE_KEY);
assertType('array{}', $filtered2);

$filtered3 = array_filter($list3, 'is_string', ARRAY_FILTER_USE_BOTH);
assertType('array{}', $filtered3);
}

/**
* @param array<int|string, int|string> $map1
* @param array<int|string, int|string> $map2
* @param array<int|string, int|string> $map3
*/
function filtersString(array $map1, array $map2, array $map3): void
{
$filtered1 = array_filter($map1, 'is_string');
assertType('array<int|string, string>', $filtered1);

$filtered2 = array_filter($map2, 'is_string', ARRAY_FILTER_USE_KEY);
assertType('array<string, int|string>', $filtered2);

$filtered3 = array_filter($map3, 'is_string', ARRAY_FILTER_USE_BOTH);
assertType('array<int|string, string>', $filtered3);
}

/**
* @param array<int, int> $list1
*/
function nonCallableStringIsIgnored(array $list1): void
{
$filtered1 = array_filter($list1, 'foo');
assertType('array<int, int>', $filtered1);
}

/**
* @param array<int|string, int|string> $map1
* @param array<int|string, int|string> $map2
*/
function nonBuiltInFunctionsAreNotSupportedYetAndThereforeIgnored(array $map1, array $map2): void
{
$filtered1 = array_filter($map1, '\ArrayFilter\isString');
assertType('array<int|string, int|string>', $filtered1);

$filtered2 = array_filter($map2, '\ArrayFilter\Filters::isString');
assertType('array<int|string, int|string>', $filtered2);
}

/**
* @param mixed $value
*/
function isString($value): bool
{
return is_string($value);
}

class Filters {
/**
* @param mixed $value
*/
public static function isString($value): bool
{
return is_string($value);
}
}

0 comments on commit 1789915

Please sign in to comment.