Skip to content

Commit ab704f4

Browse files
committed
[PHP 8.5] Support for static Closures in constant expressions (initializers)
1 parent c0ae08e commit ab704f4

File tree

6 files changed

+310
-35
lines changed

6 files changed

+310
-35
lines changed

src/Analyser/MutatingScope.php

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@
119119
use PHPStan\Type\NullType;
120120
use PHPStan\Type\ObjectType;
121121
use PHPStan\Type\ObjectWithoutClassType;
122-
use PHPStan\Type\ParserNodeTypeToPHPStanType;
123122
use PHPStan\Type\StaticType;
124123
use PHPStan\Type\StringType;
125124
use PHPStan\Type\ThisType;
@@ -3878,40 +3877,7 @@ public function isParameterValueNullable(Node\Param $parameter): bool
38783877
*/
38793878
public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type
38803879
{
3881-
if ($isNullable) {
3882-
return TypeCombinator::addNull(
3883-
$this->getFunctionType($type, false, $isVariadic),
3884-
);
3885-
}
3886-
if ($isVariadic) {
3887-
if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
3888-
return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType(
3889-
$type,
3890-
false,
3891-
false,
3892-
));
3893-
}
3894-
3895-
return TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getFunctionType(
3896-
$type,
3897-
false,
3898-
false,
3899-
)), new AccessoryArrayListType());
3900-
}
3901-
3902-
if ($type instanceof Name) {
3903-
$className = (string) $type;
3904-
$lowercasedClassName = strtolower($className);
3905-
if ($lowercasedClassName === 'parent') {
3906-
if ($this->isInClass() && $this->getClassReflection()->getParentClass() !== null) {
3907-
return new ObjectType($this->getClassReflection()->getParentClass()->getName());
3908-
}
3909-
3910-
return new NonexistentParentClassType();
3911-
}
3912-
}
3913-
3914-
return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null);
3880+
return $this->initializerExprTypeResolver->getFunctionType($type, $isNullable, $isVariadic, InitializerExprContext::fromScope($this));
39153881
}
39163882

39173883
private static function intersectButNotNever(Type $nativeType, Type $inferredType): Type

src/Analyser/NodeScopeResolver.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5058,6 +5058,25 @@ private function processAttributeGroups(
50585058
{
50595059
foreach ($attrGroups as $attrGroup) {
50605060
foreach ($attrGroup->attrs as $attr) {
5061+
$className = $scope->resolveName($attr->name);
5062+
if ($this->reflectionProvider->hasClass($className)) {
5063+
$classReflection = $this->reflectionProvider->getClass($className);
5064+
if ($classReflection->hasConstructor()) {
5065+
$constructorReflection = $classReflection->getConstructor();
5066+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
5067+
$scope,
5068+
$attr->args,
5069+
$constructorReflection->getVariants(),
5070+
$constructorReflection->getNamedArgumentsVariants(),
5071+
);
5072+
$expr = new New_($attr->name, $attr->args);
5073+
$expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
5074+
$this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, ExpressionContext::createDeep());
5075+
$nodeCallback($attr, $scope);
5076+
continue;
5077+
}
5078+
}
5079+
50615080
foreach ($attr->args as $arg) {
50625081
$this->processExprNode($stmt, $arg->value, $scope, $nodeCallback, ExpressionContext::createDeep());
50635082
$nodeCallback($arg, $scope);

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Closure;
66
use Nette\Utils\Strings;
77
use PhpParser\Node\Arg;
8+
use PhpParser\Node\ComplexType;
89
use PhpParser\Node\Expr;
910
use PhpParser\Node\Expr\BinaryOp;
1011
use PhpParser\Node\Expr\Cast\Array_;
@@ -16,8 +17,10 @@
1617
use PhpParser\Node\Expr\FuncCall;
1718
use PhpParser\Node\Expr\New_;
1819
use PhpParser\Node\Expr\PropertyFetch;
20+
use PhpParser\Node\Expr\Variable;
1921
use PhpParser\Node\Identifier;
2022
use PhpParser\Node\Name;
23+
use PhpParser\Node\Param;
2124
use PhpParser\Node\Scalar\Float_;
2225
use PhpParser\Node\Scalar\Int_;
2326
use PhpParser\Node\Scalar\MagicConst;
@@ -36,6 +39,7 @@
3639
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
3740
use PHPStan\Reflection\Callables\SimpleImpurePoint;
3841
use PHPStan\Reflection\Callables\SimpleThrowPoint;
42+
use PHPStan\Reflection\Native\NativeParameterReflection;
3943
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
4044
use PHPStan\ShouldNotHappenException;
4145
use PHPStan\TrinaryLogic;
@@ -68,16 +72,19 @@
6872
use PHPStan\Type\GeneralizePrecision;
6973
use PHPStan\Type\Generic\GenericClassStringType;
7074
use PHPStan\Type\Generic\TemplateType;
75+
use PHPStan\Type\Generic\TemplateTypeMap;
7176
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
7277
use PHPStan\Type\IntegerRangeType;
7378
use PHPStan\Type\IntegerType;
7479
use PHPStan\Type\IntersectionType;
7580
use PHPStan\Type\MixedType;
7681
use PHPStan\Type\NeverType;
82+
use PHPStan\Type\NonexistentParentClassType;
7783
use PHPStan\Type\NullType;
7884
use PHPStan\Type\ObjectShapeType;
7985
use PHPStan\Type\ObjectType;
8086
use PHPStan\Type\ObjectWithoutClassType;
87+
use PHPStan\Type\ParserNodeTypeToPHPStanType;
8188
use PHPStan\Type\StaticType;
8289
use PHPStan\Type\StringType;
8390
use PHPStan\Type\ThisType;
@@ -106,6 +113,7 @@
106113
use function is_float;
107114
use function is_int;
108115
use function is_numeric;
116+
use function is_string;
109117
use function max;
110118
use function min;
111119
use function sprintf;
@@ -201,6 +209,56 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
201209
if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
202210
return $this->getFirstClassCallableType($expr, $context, false);
203211
}
212+
if ($expr instanceof Expr\Closure && $expr->static) {
213+
$parameters = [];
214+
$isVariadic = false;
215+
$firstOptionalParameterIndex = null;
216+
foreach ($expr->params as $i => $param) {
217+
$isOptionalCandidate = $param->default !== null || $param->variadic;
218+
219+
if ($isOptionalCandidate) {
220+
if ($firstOptionalParameterIndex === null) {
221+
$firstOptionalParameterIndex = $i;
222+
}
223+
} else {
224+
$firstOptionalParameterIndex = null;
225+
}
226+
}
227+
228+
foreach ($expr->params as $i => $param) {
229+
if ($param->variadic) {
230+
$isVariadic = true;
231+
}
232+
if (!$param->var instanceof Variable || !is_string($param->var->name)) {
233+
throw new ShouldNotHappenException();
234+
}
235+
$parameters[] = new NativeParameterReflection(
236+
$param->var->name,
237+
$firstOptionalParameterIndex !== null && $i >= $firstOptionalParameterIndex,
238+
$this->getFunctionType($param->type, $this->isParameterValueNullable($param), false, $context),
239+
$param->byRef
240+
? PassedByReference::createCreatesNewVariable()
241+
: PassedByReference::createNo(),
242+
$param->variadic,
243+
$param->default !== null ? $this->getType($param->default, $context) : null,
244+
);
245+
}
246+
247+
$returnType = new MixedType(false);
248+
if ($expr->returnType !== null) {
249+
$returnType = $this->getFunctionType($expr->returnType, false, false, $context);
250+
}
251+
252+
return new ClosureType(
253+
$parameters,
254+
$returnType,
255+
$isVariadic,
256+
TemplateTypeMap::createEmpty(),
257+
TemplateTypeMap::createEmpty(),
258+
TemplateTypeVarianceMap::createEmpty(),
259+
acceptsNamedArguments: TrinaryLogic::createYes(),
260+
);
261+
}
204262
if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
205263
$var = $this->getType($expr->var, $context);
206264
$dim = $this->getType($expr->dim, $context);
@@ -683,6 +741,67 @@ public function getCastType(Expr\Cast $expr, callable $getTypeCallback): Type
683741
return new MixedType();
684742
}
685743

744+
/**
745+
* @param Name|Identifier|ComplexType|null $type
746+
*/
747+
public function getFunctionType($type, bool $isNullable, bool $isVariadic, InitializerExprContext $context): Type
748+
{
749+
if ($isNullable) {
750+
return TypeCombinator::addNull(
751+
$this->getFunctionType($type, false, $isVariadic, $context),
752+
);
753+
}
754+
if ($isVariadic) {
755+
if (!$this->phpVersion->supportsNamedArguments()) {
756+
return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType(
757+
$type,
758+
false,
759+
false,
760+
$context,
761+
));
762+
}
763+
764+
return TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getFunctionType(
765+
$type,
766+
false,
767+
false,
768+
$context,
769+
)), new AccessoryArrayListType());
770+
}
771+
772+
if ($type instanceof Name) {
773+
$className = (string) $type;
774+
$lowercasedClassName = strtolower($className);
775+
if ($lowercasedClassName === 'parent') {
776+
$classReflection = null;
777+
if ($context->getClassName() !== null && $this->getReflectionProvider()->hasClass($context->getClassName())) {
778+
$classReflection = $this->getReflectionProvider()->getClass($context->getClassName());
779+
}
780+
if ($classReflection !== null && $classReflection->getParentClass() !== null) {
781+
return new ObjectType($classReflection->getParentClass()->getName());
782+
}
783+
784+
return new NonexistentParentClassType();
785+
}
786+
}
787+
788+
$classReflection = null;
789+
if ($context->getClassName() !== null && $this->getReflectionProvider()->hasClass($context->getClassName())) {
790+
$classReflection = $this->getReflectionProvider()->getClass($context->getClassName());
791+
}
792+
793+
return ParserNodeTypeToPHPStanType::resolve($type, $classReflection);
794+
}
795+
796+
private function isParameterValueNullable(Param $parameter): bool
797+
{
798+
if ($parameter->default instanceof ConstFetch) {
799+
return strtolower((string) $parameter->default->name) === 'null';
800+
}
801+
802+
return false;
803+
}
804+
686805
public function getFirstClassCallableType(Expr\CallLike $expr, InitializerExprContext $context, bool $nativeTypesPromoted): Type
687806
{
688807
if ($expr instanceof FuncCall) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php // lint >= 8.5
2+
3+
namespace ClosureInAttributeTypes;
4+
5+
use Attribute;
6+
use Closure;
7+
use function PHPStan\Testing\assertType;
8+
9+
#[Attribute]
10+
class AttrWithCallback
11+
{
12+
13+
/**
14+
* @param Closure(int): void $callback
15+
*/
16+
public function __construct(public Closure $callback)
17+
{
18+
19+
}
20+
21+
}
22+
23+
class AttrWithCallback2
24+
{
25+
26+
/**
27+
* @param Closure(positive-int): void $callback
28+
*/
29+
public function __construct(public Closure $callback)
30+
{
31+
32+
}
33+
34+
}
35+
36+
class Bar
37+
{
38+
39+
public function doBar(
40+
#[AttrWithCallback(static function ($i) {
41+
assertType('int', $i);
42+
})]
43+
$three,
44+
#[AttrWithCallback2(static function ($i) {
45+
assertType('int<1, max>', $i);
46+
})]
47+
$four,
48+
#[AttrWithCallback2(static function (int $i) {
49+
assertType('int<1, max>', $i);
50+
})]
51+
$five,
52+
)
53+
{
54+
55+
}
56+
57+
}

tests/PHPStan/Reflection/AttributeReflectionTest.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,75 @@ public static function dataAttributeReflections(): iterable
173173
],
174174
],
175175
];
176+
yield [
177+
$barClosureInAttributeMethod->getOnlyVariant()->getParameters()[2]->getAttributes(),
178+
[
179+
[
180+
'ClosureInAttribute\\AttrWithCallback',
181+
[
182+
'callback' => 'Closure(int): mixed',
183+
],
184+
],
185+
],
186+
];
187+
yield [
188+
$barClosureInAttributeMethod->getOnlyVariant()->getParameters()[3]->getAttributes(),
189+
[
190+
[
191+
'ClosureInAttribute\\AttrWithCallback',
192+
[
193+
'callback' => 'Closure(int): mixed',
194+
],
195+
],
196+
],
197+
];
198+
yield [
199+
$barClosureInAttributeMethod->getOnlyVariant()->getParameters()[4]->getAttributes(),
200+
[
201+
[
202+
'ClosureInAttribute\\AttrWithCallback',
203+
[
204+
'callback' => 'Closure(int): string',
205+
],
206+
],
207+
],
208+
];
209+
210+
$bazClosureInAttribute = $reflectionProvider->getClass('ClosureInAttribute\\Baz');
211+
$bazClosureInAttributeMethod = $bazClosureInAttribute->getNativeMethod('doBaz');
212+
yield [
213+
$bazClosureInAttributeMethod->getOnlyVariant()->getParameters()[0]->getAttributes(),
214+
[
215+
[
216+
'ClosureInAttribute\\AttrWithCallback2',
217+
[
218+
'callback' => 'Closure(mixed): mixed',
219+
],
220+
],
221+
],
222+
];
223+
yield [
224+
$bazClosureInAttributeMethod->getOnlyVariant()->getParameters()[1]->getAttributes(),
225+
[
226+
[
227+
'ClosureInAttribute\\AttrWithCallback2',
228+
[
229+
'callback' => 'Closure(int=): mixed',
230+
],
231+
],
232+
],
233+
];
234+
yield [
235+
$bazClosureInAttributeMethod->getOnlyVariant()->getParameters()[2]->getAttributes(),
236+
[
237+
[
238+
'ClosureInAttribute\\AttrWithCallback2',
239+
[
240+
'callback' => 'Closure(int ...): mixed',
241+
],
242+
],
243+
],
244+
];
176245
}
177246

178247
yield [

0 commit comments

Comments
 (0)