Thanks to visit codestin.com
Credit goes to github.com

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
Expand Up @@ -40,3 +40,4 @@ parameters:
invalidPhpDocTagLine: true
detectDeadTypeInMultiCatch: true
zeroFiles: true
callUserFunc: true
2 changes: 1 addition & 1 deletion conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ rules:
- PHPStan\Rules\Exceptions\ThrowExpressionRule
- PHPStan\Rules\Functions\ArrowFunctionAttributesRule
- PHPStan\Rules\Functions\ArrowFunctionReturnNullsafeByRefRule
- PHPStan\Rules\Functions\CallToFunctionParametersRule
- PHPStan\Rules\Functions\ClosureAttributesRule
- PHPStan\Rules\Functions\DefineParametersRule
- PHPStan\Rules\Functions\ExistingClassesInArrowFunctionTypehintsRule
- PHPStan\Rules\Functions\CallToFunctionParametersRule
- PHPStan\Rules\Functions\ExistingClassesInClosureTypehintsRule
- PHPStan\Rules\Functions\ExistingClassesInTypehintsRule
- PHPStan\Rules\Functions\FunctionAttributesRule
Expand Down
5 changes: 5 additions & 0 deletions conf/config.level5.neon
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ parameters:
conditionalTags:
PHPStan\Rules\Functions\ArrayFilterRule:
phpstan.rules.rule: %featureToggles.arrayFilter%
PHPStan\Rules\Functions\CallUserFuncRule:
phpstan.rules.rule: %featureToggles.callUserFunc%

rules:
- PHPStan\Rules\DateTimeInstantiationRule
Expand All @@ -25,3 +27,6 @@ services:
class: PHPStan\Rules\Functions\ArrayFilterRule
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%

-
class: PHPStan\Rules\Functions\CallUserFuncRule
2 changes: 2 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ parameters:
invalidPhpDocTagLine: false
detectDeadTypeInMultiCatch: false
zeroFiles: false
callUserFunc: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -311,6 +312,7 @@ parametersSchema:
invalidPhpDocTagLine: bool()
detectDeadTypeInMultiCatch: bool()
zeroFiles: bool()
callUserFunc: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
53 changes: 53 additions & 0 deletions src/Analyser/ArgumentsNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Constant\ConstantArrayType;
use function array_key_exists;
Expand All @@ -23,6 +24,58 @@ final class ArgumentsNormalizer

public const ORIGINAL_ARG_ATTRIBUTE = 'originalArg';

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

$passThruArgs = [];
$callbackArg = 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;
}
}

$passThruArgs[] = $arg;
}

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

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

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

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

public static function reorderFuncArguments(
ParametersAcceptor $parametersAcceptor,
FuncCall $functionCall,
Expand Down
9 changes: 9 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,15 @@ private function resolveType(string $exprString, Expr $node): Type
return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType();
}

if ($functionReflection->getName() === 'call_user_func') {
$result = ArgumentsNormalizer::reorderCallUserFuncArguments($node, $this);
if ($result !== null) {
[, $innerFuncCall] = $result;

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

$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$this,
$node->getArgs(),
Expand Down
81 changes: 81 additions & 0 deletions src/Rules/Functions/CallUserFuncRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\Rule;
use function count;
use function ucfirst;

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

public function __construct(
private ReflectionProvider $reflectionProvider,
private FunctionCallParametersCheck $check,
)
{
}

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

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

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

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

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

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

$callableDescription = 'callable passed to call_user_func()';

return $this->check->check($parametersAcceptor, $scope, false, $funcCall, [
ucfirst($callableDescription) . ' invoked with %d parameter, %d required.',
ucfirst($callableDescription) . ' invoked with %d parameters, %d required.',
ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.',
ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.',
ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.',
ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.',
'Parameter %s of ' . $callableDescription . ' expects %s, %s given.',
'Result of ' . $callableDescription . ' (void) is used.',
'Parameter %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.',
'Unable to resolve the template type %s in call to ' . $callableDescription,
'Missing parameter $%s in call to ' . $callableDescription . '.',
'Unknown parameter $%s in call to ' . $callableDescription . '.',
'Return type of call to ' . $callableDescription . ' contains unresolvable type.',
'Parameter %s of ' . $callableDescription . ' contains unresolvable type.',
]);
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,13 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5782b-php7.php');
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func.php');
if (PHP_VERSION_ID >= 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php8.php');
} else {
yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php7.php');
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/gettype.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array_splice.php');
}
Expand Down
26 changes: 26 additions & 0 deletions tests/PHPStan/Analyser/data/call-user-func-php7.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace CallUserFuncPhp7;

use function PHPStan\Testing\assertType;

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

class Foo {

/**
* @param string $params,...
*/
function doVariadics(...$params) {
// because of named arguments support in php8 we have a different return type as in php7
// see https://phpstan.org/r/58c30346-9568-47ca-82e5-53b2fffda7d0
assertType('list<string>', call_user_func('CallUserFuncPhp7\generic', $params));
}
}
55 changes: 55 additions & 0 deletions tests/PHPStan/Analyser/data/call-user-func-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace CallUserFuncPhp8;

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 {

/**
* @param string $params,...
*/
function doVariadics(...$params) {
// because of named arguments support in php8 we have a different return type as in php7
// see https://phpstan.org/r/58c30346-9568-47ca-82e5-53b2fffda7d0
assertType('array<int|string, string>', call_user_func('CallUserFuncPhp8\generic', $params));
}

function doNamed() {
assertType('int', call_user_func('CallUserFuncPhp8\generic', t: 1));
assertType('array{int, int, int}', call_user_func('CallUserFuncPhp8\generic', t: [1, 2, 3]));

assertType('array{int, int, int}', call_user_func('CallUserFuncPhp8\generic3', t: [1, 2, 3]));
assertType('string', call_user_func('CallUserFuncPhp8\generic3', b: 150));
assertType('string', call_user_func('CallUserFuncPhp8\generic3', c: 'lala'));
assertType('string', call_user_func(c: 'lala', callback: 'CallUserFuncPhp8\generic3'));

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

namespace CallUserFunc;

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(...$params));
}

/**
* @param string $params,...
*/
function doVariadics(...$params) {
assertType('string', call_user_func('CallUserFunc\generic', ...$params));
}

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

assertType('int', call_user_func('CallUserFunc\fun'));
assertType('int', call_user_func('CallUserFunc\fun3', 1 ,2 ,3));
assertType('string', call_user_func(['CallUserFunc\c', 'm']));
}
}
Loading