Skip to content

Commit aa40aab

Browse files
committed
Enter the Closure::bind() scope for named arguments in any order
The visitor resolved the scope argument by name, but member accessibility went through the bind-scope factory, which captured the un-normalized call and read $newThis/$newScope from getArgs()[1]/[2] — the written positions. A reordered call therefore kept a false positive and only its message changed (outOfClass.self -> classConstant.protected). Normalize the call inside the factory so both arguments are found at their parameter positions. The rule test covers all six argument orders of the report.
1 parent b746d38 commit aa40aab

3 files changed

Lines changed: 34 additions & 1 deletion

File tree

‎src/Analyser/ExprHandler/StaticCallHandler.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
126126
// deferred until the closure argument is processed: with
127127
// closures processed last, the bound $this/scope arguments
128128
// are already evaluated on the scope the factory receives
129-
$closureBindScopeFactory = static function (MutatingScope $boundScope) use ($expr): MutatingScope {
129+
$closureBindScopeFactory = static function (MutatingScope $boundScope) use ($expr, $parametersAcceptor): MutatingScope {
130+
// normalized so that $newThis and $newScope are found at their
131+
// parameter positions even when the call names its arguments
132+
$expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
130133
$thisType = null;
131134
$nativeThisType = null;
132135
if (isset($expr->getArgs()[1])) {

‎tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ public function testClassConstantVisibility(): void
188188
]);
189189
}
190190

191+
#[RequiresPhp('>= 8.0.0')]
192+
public function testClosureBindNamedArguments(): void
193+
{
194+
$this->phpVersion = PHP_VERSION_ID;
195+
$this->analyse([__DIR__ . '/data/closure-bind-named-arguments.php'], []);
196+
}
197+
191198
public function testClassExists(): void
192199
{
193200
$this->phpVersion = PHP_VERSION_ID;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace ClosureBindNamedArguments;
6+
7+
use Closure;
8+
9+
class Target
10+
{
11+
12+
protected const C = 'c';
13+
14+
}
15+
16+
// Every form binds the scope to Target, so accessing its protected constant is allowed
17+
// no matter how the arguments are written.
18+
Closure::bind(static fn () => self::C, null, Target::class);
19+
Closure::bind(closure: static fn () => self::C, newThis: null, newScope: Target::class);
20+
Closure::bind(closure: static fn () => self::C, newScope: Target::class, newThis: null);
21+
Closure::bind(newScope: Target::class, closure: static fn () => self::C, newThis: null);
22+
Closure::bind(static fn () => self::C, null, newScope: Target::class);
23+
Closure::bind(static fn () => self::C, newScope: Target::class, newThis: null);

0 commit comments

Comments
 (0)