Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ parameters:
count: 1
path: src/Analyser/AnalyserResultFinalizer.php

-
rawMessage: PHPDoc tag @var with type int|string is not subtype of type string.
identifier: varTag.type
count: 1
path: src/Analyser/ArgumentsNormalizer.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
Expand Down
102 changes: 102 additions & 0 deletions src/Analyser/ArgumentsNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
namespace PHPStan\Analyser;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\Int_;
use PhpParser\Node\Scalar\String_;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
Expand All @@ -18,6 +22,8 @@
use function array_keys;
use function array_values;
use function count;
use function is_string;
use function key;
use function ksort;
use function max;
use function sprintf;
Expand Down Expand Up @@ -89,6 +95,102 @@
), $acceptsNamedArguments];
}

/**
* @return array{ParametersAcceptor, FuncCall, TrinaryLogic}|null
*/
public static function reorderCallUserFuncArrayArguments(
FuncCall $callUserFuncArrayCall,
Scope $scope,
): ?array
{
$args = $callUserFuncArrayCall->getArgs();
if (count($args) < 2) {
return null;
}

$callbackArg = null;
$argsArrayArg = null;
foreach ($args as $i => $arg) {
if ($callbackArg === null) {
if ($arg->name === null && $i === 0) {
$callbackArg = $arg;
continue;
}
if ($arg->name !== null && $arg->name->toString() === 'callback') {
$callbackArg = $arg;
continue;
}
}

if ($argsArrayArg !== null) {
continue;
}
if ($arg->name === null && $i === 1) {
$argsArrayArg = $arg;
continue;
}
if ($arg->name === null || $arg->name->toString() !== 'args') {
continue;
}
$argsArrayArg = $arg;
}

if ($callbackArg === null || $argsArrayArg === null) {
return null;
}

if (!$argsArrayArg->value instanceof Array_) {
return null;
}

$calledOnType = $scope->getType($callbackArg->value);
if (!$calledOnType->isCallable()->yes()) {

Check warning on line 147 in src/Analyser/ArgumentsNormalizer.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $calledOnType = $scope->getType($callbackArg->value); - if (!$calledOnType->isCallable()->yes()) { + if ($calledOnType->isCallable()->no()) { return null; }

Check warning on line 147 in src/Analyser/ArgumentsNormalizer.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $calledOnType = $scope->getType($callbackArg->value); - if (!$calledOnType->isCallable()->yes()) { + if ($calledOnType->isCallable()->no()) { return null; }
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the following loop might return early, we could move this 4 lines nearer to the usage in line 174 to prevent unnecessary work


$passThruArgs = [];
foreach ($argsArrayArg->value->items as $item) {
$key = null;
if ($item->key instanceof String_) {
/** @var int|string $key */
$key = key([$item->key->value => null]);
if ($key === '') {
return null;
}
} elseif ($item->key !== null && !$item->key instanceof Int_) {
// Dynamic key, we cannot be sure.
return null;
}

$passThruArgs[] = new Arg(
$item->value,
$item->byRef,
$item->unpack,
$item->getAttributes(),
is_string($key) ? new Identifier($key) : null,
);
}

$callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope);
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$passThruArgs,
$callableParametersAcceptors,
null,
);

$acceptsNamedArguments = TrinaryLogic::createYes();
foreach ($callableParametersAcceptors as $callableParametersAcceptor) {
$acceptsNamedArguments = $acceptsNamedArguments->and($callableParametersAcceptor->acceptsNamedArguments());
}

return [$parametersAcceptor, new FuncCall(
$callbackArg->value,
$passThruArgs,
$callUserFuncArrayCall->getAttributes(),
), $acceptsNamedArguments];
}

public static function reorderFuncArguments(
ParametersAcceptor $parametersAcceptor,
FuncCall $functionCall,
Expand Down
9 changes: 9 additions & 0 deletions src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,15 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
}
}

if ($functionReflection->getName() === 'call_user_func_array') {
$result = ArgumentsNormalizer::reorderCallUserFuncArrayArguments($expr, $scope);
if ($result !== null) {
[, $innerFuncCall] = $result;

return $scope->getType($innerFuncCall);
}
}

$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$expr->getArgs(),
Expand Down
22 changes: 15 additions & 7 deletions src/Rules/Functions/CallUserFuncRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\Rule;
use function count;
use function sprintf;
use function ucfirst;

/**
Expand Down Expand Up @@ -47,20 +48,27 @@ public function processNode(Node $node, Scope $scope): array
}

$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
if ($functionReflection->getName() !== 'call_user_func') {

$functionName = $functionReflection->getName();
if ($functionName === 'call_user_func') {
$result = ArgumentsNormalizer::reorderCallUserFuncArguments(
$node,
$scope,
);
} elseif ($functionName === 'call_user_func_array') {
$result = ArgumentsNormalizer::reorderCallUserFuncArrayArguments(
$node,
$scope,
);
} else {
return [];
}

$result = ArgumentsNormalizer::reorderCallUserFuncArguments(
$node,
$scope,
);
if ($result === null) {
return [];
}
[$parametersAcceptor, $funcCall, $acceptsNamedArguments] = $result;

$callableDescription = 'callable passed to call_user_func()';
$callableDescription = sprintf('callable passed to %s()', $functionName);

return $this->check->check(
$parametersAcceptor,
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/nsrt/call-user-func-array-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php // lint >= 8.0

namespace CallUserFuncArrayPhp8;

use function PHPStan\Testing\assertType;

/**
* @template T
* @param T $t
* @return T
*/
function generic($t) {
return $t;
}

/**
* @template T
* @param T $t
* @return T
*/
function generic3($t = '', int $b = 100, string $c = '') {
return $t;
}


function fun3($a = '', $b = '', $c = ''): int {
return 1;
}

class Foo {
function doNamed() {
assertType('1', call_user_func_array('CallUserFuncPhp8\generic', ['t' => 1]));
assertType('array{1, 2, 3}', call_user_func_array('CallUserFuncPhp8\generic', ['t' => [1, 2, 3]]));

assertType('array{1, 2, 3}', call_user_func_array('CallUserFuncPhp8\generic3', ['t' => [1, 2, 3]]));
assertType('\'\'', call_user_func_array('CallUserFuncPhp8\generic3', ['b' => 150]));
assertType('\'\'', call_user_func_array('CallUserFuncPhp8\generic3', ['c' => 'lala']));
assertType('\'\'', call_user_func_array(args: ['c' => 'lala'], callback: 'CallUserFuncPhp8\generic3'));

assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['a' => [1, 2, 3]]));
assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['b' => [1, 2, 3]]));
assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['c' => [1, 2, 3]]));
assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['a' => [1, 2, 3], 'c' => 'c']));
}
}
55 changes: 55 additions & 0 deletions tests/PHPStan/Analyser/nsrt/call-user-func_array.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace CallUserFuncArray;

use function PHPStan\Testing\assertType;

/**
* @template T
* @param T $t
* @return T
*/
function generic($t) {
return $t;
}

function fun(): int
{
return 3;
}

function fun3($i, $x, $y): int
{
return 3;
}

class c {
static function m(): string
{
return 'hello';
}
}

class Foo {
function proxy() {
$params = [
'CallUserFunc\generic',
[123, 456],
];

assertType('mixed', call_user_func_array(...$params));
}

/**
* @param string[] $strings
*/
function doFunc($strings) {
assertType('true', call_user_func_array('CallUserFunc\generic', [true]));
assertType('\'hello\'', call_user_func_array('CallUserFunc\generic', ['hello']));
assertType('array<string>', call_user_func_array('CallUserFunc\generic', [$strings]));

assertType('int', call_user_func_array('CallUserFunc\fun', []));
assertType('int', call_user_func_array('CallUserFunc\fun3', [1 ,2 ,3]));
assertType('string', call_user_func_array(['CallUserFunc\c', 'm'], []));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,7 @@ public function testCallUserFuncArray(): void
],
];
}
$this->analyse([__DIR__ . '/data/call-user-func-array.php'], $errors);
$this->analyse([__DIR__ . '/data/call-user-func-array-named-args.php'], $errors);
}
Comment on lines 1130 to 1132
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is call-user-func-array.php no longer tested in this rule?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just renamed the initial call-user-func-array.php file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interessting.. the PR does not include a file which got renamed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I renamed call-user-func-array and recreated a new call-user-func-array.

Git consider call-user-func-array-named-args new, and the other as modified


public function testFirstClassCallables(): void
Expand Down
59 changes: 59 additions & 0 deletions tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,65 @@ public function testRule(): void
]);
}

#[RequiresPhp('>= 8.0')]
public function testRuleCallUserFuncArray(): void
{
$this->analyse([__DIR__ . '/data/call-user-func-array.php'], [
[
'Callable passed to call_user_func_array() invoked with 0 parameters, 1 required.',
15,
],
[
'Parameter #1 $i of callable passed to call_user_func_array() expects int, string given.',
17,
],
[
'Parameter $i of callable passed to call_user_func_array() expects int, string given.',
18,
],
[
'Parameter $i of callable passed to call_user_func_array() expects int, string given.',
19,
],
[
'Unknown parameter $j in call to callable passed to call_user_func_array().',
22,
],
[
'Missing parameter $i (int) in call to callable passed to call_user_func_array().',
22,
],
[
'Callable passed to call_user_func_array() invoked with 0 parameters, 2-4 required.',
30,
],
[
'Callable passed to call_user_func_array() invoked with 1 parameter, 2-4 required.',
31,
],
[
'Callable passed to call_user_func_array() invoked with 0 parameters, at least 2 required.',
40,
],
[
'Callable passed to call_user_func_array() invoked with 1 parameter, at least 2 required.',
41,
],
[
'Result of callable passed to call_user_func_array() (void) is used.',
43,
],
[
'Parameter #1 $i of callable passed to call_user_func_array() expects int|null, string given.',
52,
],
[
'Parameter #1 $i of callable passed to call_user_func_array() expects int|null, string given.',
53,
],
]);
}

public function testBug7057(): void
{
$this->analyse([__DIR__ . '/data/bug-7057.php'], []);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

call_user_func_array('array_merge', ['foo' => ['bar' => 2]]);
Loading
Loading