Skip to content

Commit ffbfec8

Browse files
committed
Support named arguments in the Closure::bind() scope binding
The visitor read the arguments positionally, so a call like Closure::bind(closure: ..., newScope: Target::class, newThis: null) was not rescoped (it failed safe to the out-of-class report). Resolve the closure/newThis/newScope arguments by name and position instead.
1 parent 5c4cac0 commit ffbfec8

2 files changed

Lines changed: 60 additions & 6 deletions

File tree

‎src/Parser/ClosureBindArgVisitor.php‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use function array_key_exists;
1313
use function array_shift;
1414
use function array_unshift;
15-
use function count;
1615
use function spl_object_id;
1716

1817
#[AutowiredService]
@@ -58,14 +57,39 @@ public function enterNode(Node $node): ?Node
5857
&& $node->name->toLowerString() === 'bind'
5958
&& !$node->isFirstClassCallable()
6059
) {
61-
$args = $node->getArgs();
62-
if (count($args) > 1) {
63-
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
60+
$closureArg = null;
61+
$newThisArg = null;
62+
$newScopeArg = null;
63+
foreach ($node->getArgs() as $i => $arg) {
64+
if ($arg->name === null) {
65+
if ($i === 0) {
66+
$closureArg = $arg;
67+
} elseif ($i === 1) {
68+
$newThisArg = $arg;
69+
} elseif ($i === 2) {
70+
$newScopeArg = $arg;
71+
}
72+
continue;
73+
}
74+
75+
if ($arg->name->toString() === 'closure') {
76+
$closureArg = $arg;
77+
} elseif ($arg->name->toString() === 'newThis') {
78+
$newThisArg = $arg;
79+
} elseif ($arg->name->toString() === 'newScope') {
80+
$newScopeArg = $arg;
81+
}
82+
}
83+
84+
if ($closureArg !== null) {
85+
if ($newThisArg !== null) {
86+
$closureArg->setAttribute(self::ATTRIBUTE_NAME, true);
87+
}
6488

65-
$closure = $args[0]->value;
89+
$closure = $closureArg->value;
6690
if ($closure instanceof Expr\Closure || $closure instanceof Expr\ArrowFunction) {
6791
// null means default scope "static"
68-
$this->boundClosures[spl_object_id($closure)] = $args[2]->value ?? null;
92+
$this->boundClosures[spl_object_id($closure)] = $newScopeArg !== null ? $newScopeArg->value : null;
6993
}
7094
}
7195
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace ClosureBindScopeNamedArguments;
6+
7+
use Closure;
8+
use function PHPStan\Testing\assertType;
9+
10+
class Foo
11+
{
12+
13+
protected const A = 'Foo';
14+
15+
}
16+
17+
final class Bar extends Foo
18+
{
19+
20+
protected const A = 'Bar';
21+
22+
}
23+
24+
// The bind scope is resolved by argument name, so any order works.
25+
assertType("'Foo'", Closure::bind(closure: static fn () => self::A, newThis: null, newScope: Foo::class)());
26+
assertType("'Foo'", Closure::bind(closure: static fn () => self::A, newScope: Foo::class, newThis: null)());
27+
assertType("'Bar'", Closure::bind(newScope: Bar::class, closure: static fn () => self::A, newThis: null)());
28+
29+
// Positional and named arguments mixed together.
30+
assertType("'Foo'", Closure::bind(static fn () => parent::A, null, newScope: Bar::class)());

0 commit comments

Comments
 (0)