-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce strict array_filter call (require callback method)
- Loading branch information
1 parent
7a50e96
commit cbdf0ed
Showing
6 changed files
with
237 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?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 PHPStan\Type\Type; | ||
use PHPStan\Type\VerbosityLevel; | ||
use function count; | ||
use function sprintf; | ||
|
||
/** | ||
* @implements Rule<FuncCall> | ||
*/ | ||
class ArrayFilterStrictRule implements Rule | ||
{ | ||
|
||
/** @var ReflectionProvider */ | ||
private $reflectionProvider; | ||
|
||
/** @var bool */ | ||
private $treatPhpDocTypesAsCertain; | ||
|
||
/** @var bool */ | ||
private $reportMaybes; | ||
|
||
public function __construct( | ||
ReflectionProvider $reflectionProvider, | ||
bool $treatPhpDocTypesAsCertain, | ||
bool $reportMaybes | ||
) | ||
{ | ||
$this->reflectionProvider = $reflectionProvider; | ||
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; | ||
$this->reportMaybes = $reportMaybes; | ||
} | ||
|
||
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 passed to avoid loose comparison semantics.')->build()]; | ||
} | ||
|
||
$nativeCallbackType = $scope->getNativeType($args[1]->value); | ||
|
||
if ($this->treatPhpDocTypesAsCertain) { | ||
$callbackType = $scope->getType($args[1]->value); | ||
} else { | ||
$callbackType = $nativeCallbackType; | ||
} | ||
|
||
if ($this->isCallbackTypeNull($callbackType)) { | ||
$message = 'Call to function array_filter() requires parameter #2 to be callable, %s given.'; | ||
$errorBuilder = RuleErrorBuilder::message(sprintf( | ||
$message, | ||
$callbackType->describe(VerbosityLevel::typeOnly()) | ||
)); | ||
|
||
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 []; | ||
} | ||
|
||
private function isCallbackTypeNull(Type $callbackType): bool | ||
{ | ||
if ($callbackType->isNull()->yes()) { | ||
return true; | ||
} | ||
|
||
if ($callbackType->isNull()->no()) { | ||
return false; | ||
} | ||
|
||
return $this->reportMaybes; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?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; | ||
|
||
/** @var bool */ | ||
private $reportMaybes; | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new ArrayFilterStrictRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain, $this->reportMaybes); | ||
} | ||
|
||
protected function shouldTreatPhpDocTypesAsCertain(): bool | ||
{ | ||
return $this->treatPhpDocTypesAsCertain; | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
$this->treatPhpDocTypesAsCertain = true; | ||
$this->reportMaybes = true; | ||
$this->analyse([__DIR__ . '/data/array-filter-strict.php'], [ | ||
[ | ||
'Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.', | ||
15, | ||
], | ||
[ | ||
'Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.', | ||
25, | ||
], | ||
[ | ||
'Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.', | ||
26, | ||
], | ||
[ | ||
'Call to function array_filter() requires parameter #2 to be callable, null given.', | ||
28, | ||
], | ||
[ | ||
'Call to function array_filter() requires parameter #2 to be callable, (Closure)|null given.', | ||
34, | ||
], | ||
]); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?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 : function (int $value): bool { | ||
return $value > 1; | ||
}); |