Skip to content

Commit bba35df

Browse files
committed
Resolve self/parent/static class constants inside Closure::bind() scope
Annotate special class-name nodes (self/parent/static) that appear inside a Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and resolve the class constant type against that bound class in ClassConstFetchHandler. Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original MutatingScope::getType() ClassConstFetch branch no longer exists after expression handling was split into ExprHandler services.
1 parent 65fd609 commit bba35df

3 files changed

Lines changed: 125 additions & 1 deletion

File tree

‎src/Analyser/ExprHandler/ClassConstFetchHandler.php‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node\Expr;
66
use PhpParser\Node\Expr\ClassConstFetch;
77
use PhpParser\Node\Identifier;
8+
use PhpParser\Node\Name;
89
use PhpParser\Node\Stmt;
910
use PHPStan\Analyser\ExpressionContext;
1011
use PHPStan\Analyser\ExpressionResult;
@@ -18,10 +19,14 @@
1819
use PHPStan\Analyser\TypeSpecifier;
1920
use PHPStan\Analyser\TypeSpecifierContext;
2021
use PHPStan\DependencyInjection\AutowiredService;
22+
use PHPStan\Parser\ClosureBindArgVisitor;
23+
use PHPStan\Reflection\ClassReflection;
2124
use PHPStan\Reflection\InitializerExprTypeResolver;
25+
use PHPStan\Reflection\ReflectionProvider;
2226
use PHPStan\Type\MixedType;
2327
use PHPStan\Type\Type;
2428
use function array_merge;
29+
use function count;
2530

2631
/**
2732
* @implements ExprHandler<ClassConstFetch>
@@ -33,6 +38,7 @@ final class ClassConstFetchHandler implements ExprHandler
3338
public function __construct(
3439
private InitializerExprTypeResolver $initializerExprTypeResolver,
3540
private ExpressionResultFactory $expressionResultFactory,
41+
private ReflectionProvider $reflectionProvider,
3642
)
3743
{
3844
}
@@ -48,14 +54,51 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
4854
return new MixedType();
4955
}
5056

57+
$classReflection = $scope->isInClass() ? $scope->getClassReflection() : null;
58+
$bindScopeReflection = $this->resolveClosureBindScopeReflection($scope, $expr->class);
59+
if ($bindScopeReflection !== null) {
60+
$classReflection = $bindScopeReflection;
61+
}
62+
5163
return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection(
5264
$expr->class,
5365
$expr->name->name,
54-
$scope->isInClass() ? $scope->getClassReflection() : null,
66+
$classReflection,
5567
static fn (Expr $e): Type => $scope->getType($e),
5668
);
5769
}
5870

71+
/**
72+
* Resolves the `Closure::bind()` scope class annotated on a `self`/`parent`/`static`
73+
* class name node by {@see ClosureBindArgVisitor}. Returns null when the node is not
74+
* inside a bound closure or the scope argument does not resolve to a single known class.
75+
*/
76+
private function resolveClosureBindScopeReflection(MutatingScope $scope, Expr|Name $class): ?ClassReflection
77+
{
78+
if (!$class instanceof Name || !$class->hasAttribute(ClosureBindArgVisitor::SCOPE_ATTRIBUTE_NAME)) {
79+
return null;
80+
}
81+
82+
$scopeArg = $class->getAttribute(ClosureBindArgVisitor::SCOPE_ATTRIBUTE_NAME);
83+
if (!$scopeArg instanceof Expr) {
84+
// null attribute means the default "static" scope: keep the enclosing class.
85+
return null;
86+
}
87+
88+
$scopeArgType = $scope->getType($scopeArg);
89+
$objectClassNames = $scopeArgType->getClassStringObjectType()->getObjectClassNames();
90+
if (count($objectClassNames) !== 1) {
91+
return null;
92+
}
93+
94+
$className = $objectClassNames[0];
95+
if (!$this->reflectionProvider->hasClass($className)) {
96+
return null;
97+
}
98+
99+
return $this->reflectionProvider->getClass($className);
100+
}
101+
59102
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
60103
{
61104
$beforeScope = $scope;

‎src/Parser/ClosureBindArgVisitor.php‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
use Override;
66
use PhpParser\Node;
7+
use PhpParser\Node\Expr;
78
use PhpParser\Node\Identifier;
9+
use PhpParser\Node\Name;
810
use PhpParser\NodeVisitorAbstract;
911
use PHPStan\DependencyInjection\AutowiredService;
12+
use function array_key_exists;
13+
use function array_shift;
14+
use function array_unshift;
1015
use function count;
1116

1217
#[AutowiredService]
@@ -15,6 +20,11 @@ final class ClosureBindArgVisitor extends NodeVisitorAbstract
1520

1621
public const ATTRIBUTE_NAME = 'closureBindArg';
1722

23+
public const SCOPE_ATTRIBUTE_NAME = 'closureBindScope';
24+
25+
/** @var list<?Expr> */
26+
private array $scopeStack = [];
27+
1828
#[Override]
1929
public function enterNode(Node $node): ?Node
2030
{
@@ -30,7 +40,35 @@ public function enterNode(Node $node): ?Node
3040
if (count($args) > 1) {
3141
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
3242
}
43+
44+
// null means default scope "static"
45+
array_unshift($this->scopeStack, $args[2]->value ?? null);
46+
}
47+
48+
if ($node instanceof Name
49+
&& array_key_exists(0, $this->scopeStack)
50+
&& $node->isSpecialClassName()
51+
) {
52+
$node->setAttribute(self::SCOPE_ATTRIBUTE_NAME, $this->scopeStack[0]);
53+
}
54+
55+
return null;
56+
}
57+
58+
#[Override]
59+
public function leaveNode(Node $node): ?Node
60+
{
61+
if (
62+
$node instanceof Node\Expr\StaticCall
63+
&& $node->class instanceof Node\Name
64+
&& $node->class->toLowerString() === 'closure'
65+
&& $node->name instanceof Identifier
66+
&& $node->name->toLowerString() === 'bind'
67+
&& !$node->isFirstClassCallable()
68+
) {
69+
array_shift($this->scopeStack);
3370
}
71+
3472
return null;
3573
}
3674

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ClosureBindScope;
4+
5+
use Closure;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
11+
protected const A = 'Foo';
12+
13+
}
14+
15+
final class Bar extends Foo
16+
{
17+
18+
protected const A = 'Bar';
19+
20+
}
21+
22+
// Explicit class names resolve regardless of the bound scope.
23+
assertType("'Foo'", Closure::bind(static fn () => Foo::A, null, Foo::class)());
24+
assertType("'Foo'", Closure::bind(static fn () => Foo::A, null, Bar::class)());
25+
assertType("'Bar'", Closure::bind(static fn () => Bar::A, null, Bar::class)());
26+
27+
// `self` / `parent` resolve against the Closure::bind() scope (3rd argument).
28+
assertType("'Foo'", Closure::bind(static fn () => self::A, null, Foo::class)());
29+
assertType("'Bar'", Closure::bind(static fn () => self::A, null, Bar::class)());
30+
assertType("'Foo'", Closure::bind(static fn () => parent::A, null, Bar::class)());
31+
32+
class Container
33+
{
34+
35+
public function doBindFromInsideClass(): void
36+
{
37+
// Even when Closure::bind() is called from inside another class, the bound
38+
// scope wins over the enclosing class.
39+
assertType("'Foo'", Closure::bind(static fn () => self::A, null, Foo::class)());
40+
assertType("'Bar'", Closure::bind(static fn () => self::A, null, Bar::class)());
41+
}
42+
43+
}

0 commit comments

Comments
 (0)