Skip to content

Commit

Permalink
Introduce strict array_filter call (require callback method)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamil-zacek committed Feb 15, 2024
1 parent 7a50e96 commit 41a04a8
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 1 deletion.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.10.34"
"phpstan/phpstan": "^1.10.42"
},
"require-dev": {
"nikic/php-parser": "^4.13.0",
Expand Down
9 changes: 9 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ parameters:
strictCalls: %strictRules.allRules%
switchConditionsMatchingType: %strictRules.allRules%
noVariableVariables: %strictRules.allRules%
strictArrayFilter: %strictRules.allRules%

parametersSchema:
strictRules: structure([
Expand All @@ -45,6 +46,7 @@ parametersSchema:
strictCalls: anyOf(bool(), arrayOf(bool()))
switchConditionsMatchingType: anyOf(bool(), arrayOf(bool()))
noVariableVariables: anyOf(bool(), arrayOf(bool()))
strictArrayFilter: anyOf(bool(), arrayOf(bool()))
])

conditionalTags:
Expand Down Expand Up @@ -78,6 +80,8 @@ conditionalTags:
phpstan.rules.rule: %strictRules.overwriteVariablesWithLoop%
PHPStan\Rules\ForLoop\OverwriteVariablesWithForLoopInitRule:
phpstan.rules.rule: %strictRules.overwriteVariablesWithLoop%
PHPStan\Rules\Functions\ArrayFilterStrictRule:
phpstan.rules.rule: %strictRules.strictArrayFilter%
PHPStan\Rules\Functions\ClosureUsesThisRule:
phpstan.rules.rule: %strictRules.closureUsesThis%
PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule:
Expand Down Expand Up @@ -184,6 +188,11 @@ services:
-
class: PHPStan\Rules\ForLoop\OverwriteVariablesWithForLoopInitRule

-
class: PHPStan\Rules\Functions\ArrayFilterStrictRule
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%

-
class: PHPStan\Rules\Functions\ClosureUsesThisRule

Expand Down
104 changes: 104 additions & 0 deletions src/Rules/Functions/ArrayFilterStrictRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use function count;

/**
* @implements Rule<FuncCall>
*/
class ArrayFilterStrictRule implements Rule
{

/** @var ReflectionProvider */
private $reflectionProvider;

/** @var bool */
private $treatPhpDocTypesAsCertain;

public function __construct(ReflectionProvider $reflectionProvider, bool $treatPhpDocTypesAsCertain)
{
$this->reflectionProvider = $reflectionProvider;
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
}

public function getNodeType(): string
{
return FuncCall::class;
}

/**
* @param FuncCall $node
* @return RuleError[] errors
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Name) {
return [];
}

if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
return [];
}

$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);

if ($functionReflection->getName() !== 'array_filter') {
return [];
}

$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$node->getArgs(),
$functionReflection->getVariants(),
$functionReflection->getNamedArgumentsVariants()
);

$normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);

if ($normalizedFuncCall === null) {
return [];
}

$args = $normalizedFuncCall->getArgs();
if (count($args) === 0) {
return [];
}

if (count($args) === 1) {
return [RuleErrorBuilder::message('Call to function array_filter() requires parameter #2 to be set.')->build()];
}

$nativeCallbackType = $scope->getNativeType($args[1]->value);

if ($this->treatPhpDocTypesAsCertain) {
$callbackType = $scope->getType($args[1]->value);
} else {
$callbackType = $nativeCallbackType;
}

if ($callbackType->isNull()->yes() || $callbackType->isNull()->maybe()) {
$message = 'Call to function array_filter() requires parameter #2 to have a callback function.';
$errorBuilder = RuleErrorBuilder::message($message);

if ($nativeCallbackType->isNull()->no() && $this->treatPhpDocTypesAsCertain) {
$errorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.');
}

return [$errorBuilder->build()];
}

return [];
}

}
54 changes: 54 additions & 0 deletions tests/Rules/Functions/ArrayFilterStrictRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ArrayFilterStrictRule>
*/
class ArrayFilterStrictRuleTest extends RuleTestCase
{

/** @var bool */
private $treatPhpDocTypesAsCertain;

protected function getRule(): Rule
{
return new ArrayFilterStrictRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain);
}

protected function shouldTreatPhpDocTypesAsCertain(): bool
{
return $this->treatPhpDocTypesAsCertain;
}

public function testRule(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/array-filter-strict.php'], [
[
'Call to function array_filter() requires parameter #2 to be set.',
15,
],
[
'Call to function array_filter() requires parameter #2 to be set.',
25,
],
[
'Call to function array_filter() requires parameter #2 to be set.',
26,
],
[
'Call to function array_filter() requires parameter #2 to have a callback function.',
28,
],
[
'Call to function array_filter() requires parameter #2 to have a callback function.',
34,
],
]);
}

}
34 changes: 34 additions & 0 deletions tests/Rules/Functions/data/array-filter-strict.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace ArrayFilterStrict;

/** @var list<int> $list */
$list = [1, 2, 3];

/** @var array<string, int> $array */
$array = ["a" => 1, "b" => 2, "c" => 3];

array_filter([1, 2, 3], function (int $value): bool {
return $value > 1;
});

array_filter([1, 2, 3]);

array_filter([1, 2, 3], function (int $value): bool {
return $value > 1;
}, ARRAY_FILTER_USE_KEY);

array_filter([1, 2, 3], function (int $value): int {
return $value;
});

array_filter($list);
array_filter($array);

array_filter($array, null);

array_filter($list, 'intval');

/** @var bool $bool */
$bool = doFoo();
array_filter($list, foo() ? null : fn (int $value): bool => $value > 1);

0 comments on commit 41a04a8

Please sign in to comment.