Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
parameters:
featureToggles:
bleedingEdge: true
checkParameterCastableToNumberFunctions: true
skipCheckGenericClasses!: []
stricterFunctionMap: true
6 changes: 6 additions & 0 deletions conf/config.level5.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ parameters:
checkFunctionArgumentTypes: true
checkArgumentsPassedByReference: true

conditionalTags:
PHPStan\Rules\Functions\ParameterCastableToNumberRule:
phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions%

rules:
- PHPStan\Rules\DateTimeInstantiationRule
- PHPStan\Rules\Functions\CallUserFuncRule
Expand Down Expand Up @@ -36,3 +40,5 @@ services:
treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\ParameterCastableToNumberRule
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ parameters:
tooWideThrowType: true
featureToggles:
bleedingEdge: false
checkParameterCastableToNumberFunctions: false
skipCheckGenericClasses: []
stricterFunctionMap: false
fileExtensions:
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ parametersSchema:
])
featureToggles: structure([
bleedingEdge: bool(),
checkParameterCastableToNumberFunctions: bool(),
skipCheckGenericClasses: listOf(string()),
stricterFunctionMap: bool()
])
Expand Down
84 changes: 84 additions & 0 deletions src/Rules/Functions/ParameterCastableToNumberRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\ParameterCastableToStringCheck;
use PHPStan\Rules\Rule;
use PHPStan\Type\Type;
use function count;
use function in_array;

/**
* @implements Rule<Node\Expr\FuncCall>
*/
final class ParameterCastableToNumberRule implements Rule
{

public function __construct(
private ReflectionProvider $reflectionProvider,
private ParameterCastableToStringCheck $parameterCastableToStringCheck,
)
{
}

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

public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Node\Name)) {
return [];
}

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

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

if (!in_array($functionName, ['array_sum', 'array_product'], true)) {
return [];
}

$origArgs = $node->getArgs();

if (count($origArgs) !== 1) {
return [];
}

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

$errorMessage = 'Parameter %s of function %s expects an array of values castable to number, %s given.';
$functionParameters = $parametersAcceptor->getParameters();
$error = $this->parameterCastableToStringCheck->checkParameter(
$origArgs[0],
$scope,
$errorMessage,
static fn (Type $t) => $t->toNumber(),
$functionName,
$this->parameterCastableToStringCheck->getParameterName(
$origArgs[0],
0,
$functionParameters[0] ?? null,
),
);

return $error !== null
? [$error]
: [];
}

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

namespace PHPStan\Rules\Functions;

use PHPStan\Rules\ParameterCastableToStringCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;
use function array_map;
use function str_replace;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<ParameterCastableToNumberRule>
*/
class ParameterCastableToNumberRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false)));
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/param-castable-to-number-functions.php'], $this->hackPhp74ErrorMessages([
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, array<int, int>> given.',
20,
],
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, stdClass> given.',
21,
],
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, string> given.',
22,
],
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, resource|false> given.',
23,
],
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, CurlHandle> given.',
24,
],
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, ParamCastableToNumberFunctions\\ClassWithToString> given.',
25,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, array<int, int>> given.',
27,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, stdClass> given.',
28,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, string> given.',
29,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, resource|false> given.',
30,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, CurlHandle> given.',
31,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, ParamCastableToNumberFunctions\\ClassWithToString> given.',
32,
],
]));
}

public function testNamedArguments(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Test requires PHP 8.0.');
}

$this->analyse([__DIR__ . '/data/param-castable-to-number-functions-named-args.php'], [
[
'Parameter $array of function array_sum expects an array of values castable to number, array<int, array<int, int>> given.',
7,
],
[
'Parameter $array of function array_product expects an array of values castable to number, array<int, array<int, int>> given.',
8,
],
]);
}

public function testEnum(): void
{
if (PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Test requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/param-castable-to-number-functions-enum.php'], [
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, ParamCastableToNumberFunctionsEnum\\FooEnum::A> given.',
12,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, ParamCastableToNumberFunctionsEnum\\FooEnum::A> given.',
13,
],
]);
}

public function testBug11883(): void
{
if (PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Test requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/bug-11883.php'], [
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, Bug11883\\SomeEnum::A|Bug11883\\SomeEnum::B> given.',
13,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, Bug11883\\SomeEnum::A|Bug11883\\SomeEnum::B> given.',
14,
],
]);
}

/**
* @param list<array{0: string, 1: int, 2?: string|null}> $errors
* @return list<array{0: string, 1: int, 2?: string|null}>
*/
private function hackPhp74ErrorMessages(array $errors): array
{
if (PHP_VERSION_ID >= 80000) {
return $errors;
}

return array_map(static function (array $error): array {
$error[0] = str_replace(
[
'$array of function array_sum',
'$array of function array_product',
'array<int, CurlHandle>',
],
[
'$input of function array_sum',
'$input of function array_product',
'array<int, resource>',
],
$error[0],
);

return $error;
}, $errors);
}

}
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-11883.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1); // lint >= 8.1

namespace Bug11883;

enum SomeEnum: int
{
case A = 1;
case B = 2;
}

$enums1 = [SomeEnum::A, SomeEnum::B];

var_dump(array_sum($enums1));
var_dump(array_product($enums1));
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1); // lint >= 8.1

namespace ParamCastableToNumberFunctionsEnum;

enum FooEnum
{
case A;
}

function invalidUsages()
{
array_sum([FooEnum::A]);
array_product([FooEnum::A]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace ParamCastableToNumberFunctionsNamedArgs;

function invalidUsages()
{
var_dump(array_sum(array: [[0]]));
var_dump(array_product(array: [[0]]));
}

function validUsages()
{
var_dump(array_sum(array: [1]));
var_dump(array_product(array: [1]));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace ParamCastableToNumberFunctions;

class ClassWithoutToString {}
class ClassWithToString
{
public function __toString(): string
{
return 'foo';
}
}

function invalidUsages(): void
{
$curlHandle = curl_init();
// curl_init returns benevolent union and false is castable to number.
assert($curlHandle !== false);

var_dump(array_sum([[0]]));
var_dump(array_sum([new \stdClass()]));
var_dump(array_sum(['ttt']));
var_dump(array_sum([fopen('php://input', 'r')]));
var_dump(array_sum([$curlHandle]));
var_dump(array_sum([new ClassWithToString()]));

var_dump(array_product([[0]]));
var_dump(array_product([new \stdClass()]));
var_dump(array_product(['ttt']));
var_dump(array_product([fopen('php://input', 'r')]));
var_dump(array_product([$curlHandle]));
var_dump(array_product([new ClassWithToString()]));
}

function wrongNumberOfArguments(): void
{
array_sum();
array_product();
}

function validUsages(): void
{
var_dump(array_sum(['5.5', false, true, new \SimpleXMLElement('<a>7.7</a>'), 5, 5.5, null]));
var_dump(array_product(['5.5', false, true, new \SimpleXMLElement('<a>7.7</a>'), 5, 5.5, null]));
}
Loading