Skip to content

Commit 3cf80b2

Browse files
committed
Check arguments passed through forward_static_call() against the callable
Extend CallUserFuncRule to forward_static_call() and forward_static_call_array(), reusing their argument normalization, so missing or mismatched arguments of the forwarded callable are reported like for call_user_func(). The synthesized static call's name nodes carry the original call's position attributes so the errors point at the call site. Nonexistent methods are already covered by the generic callable parameter check.
1 parent 523ec08 commit 3cf80b2

4 files changed

Lines changed: 69 additions & 2 deletions

File tree

src/Analyser/ArgumentsNormalizer.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,11 @@ private static function createForwardingStaticCall(FuncCall $funcCall, Scope $sc
273273
return null;
274274
}
275275

276+
// The original call's position attributes are propagated to the name nodes too,
277+
// so rule errors about the synthesized call point at the original call site.
276278
return new StaticCall(
277-
new FullyQualified($className),
278-
new Identifier($methodName),
279+
new FullyQualified($className, $funcCall->getAttributes()),
280+
new Identifier($methodName, $funcCall->getAttributes()),
279281
$funcCall->getArgs(),
280282
$funcCall->getAttributes(),
281283
);

src/Rules/Functions/CallUserFuncRule.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
6262
$node,
6363
$scope,
6464
);
65+
} elseif ($functionName === 'forward_static_call') {
66+
$result = ArgumentsNormalizer::reorderForwardStaticCallArguments(
67+
$node,
68+
$scope,
69+
);
70+
} elseif ($functionName === 'forward_static_call_array') {
71+
$result = ArgumentsNormalizer::reorderForwardStaticCallArrayArguments(
72+
$node,
73+
$scope,
74+
);
6575
} else {
6676
return [];
6777
}

tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,32 @@ protected function getRule(): Rule
4646
}
4747

4848
#[RequiresPhp('>= 8.0.0')]
49+
public function testForwardStaticCall(): void
50+
{
51+
$this->analyse([__DIR__ . '/data/forward-static-call.php'], [
52+
[
53+
'Callable passed to forward_static_call() invoked with 0 parameters, 1 required.',
54+
21,
55+
],
56+
[
57+
'Parameter #1 $name of callable passed to forward_static_call() expects string, int given.',
58+
22,
59+
],
60+
[
61+
'Callable passed to forward_static_call() invoked with 0 parameters, 1 required.',
62+
23,
63+
],
64+
[
65+
'Callable passed to forward_static_call_array() invoked with 0 parameters, 1 required.',
66+
25,
67+
],
68+
[
69+
'Parameter #1 $name of callable passed to forward_static_call_array() expects string, int given.',
70+
26,
71+
],
72+
]);
73+
}
74+
4975
public function testRule(): void
5076
{
5177
$this->analyse([__DIR__ . '/data/call-user-func.php'], [
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ForwardStaticCallRule;
4+
5+
class Base
6+
{
7+
8+
public static function greet(string $name): string
9+
{
10+
return 'hi ' . $name;
11+
}
12+
13+
}
14+
15+
class Caller extends Base
16+
{
17+
18+
public static function doFoo(): void
19+
{
20+
forward_static_call([Base::class, 'greet'], 'John');
21+
forward_static_call([Base::class, 'greet']);
22+
forward_static_call([Base::class, 'greet'], 42);
23+
forward_static_call('ForwardStaticCallRule\Base::greet');
24+
forward_static_call_array([Base::class, 'greet'], ['John']);
25+
forward_static_call_array([Base::class, 'greet'], []);
26+
forward_static_call_array([Base::class, 'greet'], [42]);
27+
}
28+
29+
}

0 commit comments

Comments
 (0)