Skip to content

Commit f658b3c

Browse files
committed
Resolve ExtendedFunctionVariant::getReturnType() more lazily
This will allow the `$this` or `static` in PHPDoc return type to be first replaced with final-overriden ObjectType before being thrown into `TypehintHelper::decideType()`.
1 parent 6b6c9c4 commit f658b3c

10 files changed

+107
-49
lines changed

src/Reflection/ExtendedFunctionVariant.php

+18-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Type\Generic\TemplateTypeMap;
66
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
77
use PHPStan\Type\Type;
8+
use PHPStan\Type\TypehintHelper;
89

910
/**
1011
* @api
@@ -21,7 +22,7 @@ public function __construct(
2122
?TemplateTypeMap $resolvedTemplateTypeMap,
2223
array $parameters,
2324
bool $isVariadic,
24-
Type $returnType,
25+
private ?Type $returnType,
2526
private Type $phpDocReturnType,
2627
private Type $nativeReturnType,
2728
?TemplateTypeVarianceMap $callSiteVarianceMap = null,
@@ -32,7 +33,10 @@ public function __construct(
3233
$resolvedTemplateTypeMap,
3334
$parameters,
3435
$isVariadic,
35-
$returnType,
36+
$returnType ?? TypehintHelper::decideType(
37+
$nativeReturnType,
38+
$phpDocReturnType,
39+
),
3640
$callSiteVarianceMap,
3741
);
3842
}
@@ -48,6 +52,18 @@ public function getParameters(): array
4852
return $parameters;
4953
}
5054

55+
public function getReturnType(): Type
56+
{
57+
if ($this->returnType === null) {
58+
return $this->returnType = TypehintHelper::decideType(
59+
$this->nativeReturnType,
60+
$this->phpDocReturnType,
61+
);
62+
}
63+
64+
return $this->returnType;
65+
}
66+
5167
public function getPhpDocReturnType(): Type
5268
{
5369
return $this->phpDocReturnType;

src/Reflection/Php/PhpClassReflectionExtension.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -954,19 +954,18 @@ private function createNativeMethodVariant(
954954
}
955955

956956
if ($stubPhpDocReturnType !== null) {
957-
$returnType = $stubPhpDocReturnType;
958957
$phpDocReturnType = $stubPhpDocReturnType;
959958
} else {
960-
$returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType);
959+
$phpDocReturnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType);
961960
}
962961

963962
return new ExtendedFunctionVariant(
964963
TemplateTypeMap::createEmpty(),
965964
null,
966965
$parameters,
967966
$methodSignature->isVariadic(),
968-
$returnType,
969-
$phpDocReturnType ?? new MixedType(),
967+
null,
968+
$phpDocReturnType,
970969
$methodSignature->getNativeReturnType(),
971970
);
972971
}

src/Reflection/Php/PhpFunctionFromParserNodeReflection.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public function getVariants(): array
112112
$this->getResolvedTemplateTypeMap(),
113113
$this->getParameters(),
114114
$this->isVariadic(),
115-
$this->getReturnType(),
115+
null,
116116
$this->getPhpDocReturnType(),
117117
$this->getNativeReturnType(),
118118
),

src/Reflection/Php/PhpFunctionReflection.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public function getVariants(): array
9494
null,
9595
$this->getParameters(),
9696
$this->isVariadic(),
97-
$this->getReturnType(),
97+
null,
9898
$this->getPhpDocReturnType(),
9999
$this->getNativeReturnType(),
100100
),

src/Reflection/Php/PhpMethodReflection.php

+24-25
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public function getVariants(): array
202202
null,
203203
$this->getParameters(),
204204
$this->isVariadic(),
205-
$this->getReturnType(),
205+
null,
206206
$this->getPhpDocReturnType(),
207207
$this->getNativeReturnType(),
208208
),
@@ -302,30 +302,9 @@ public function isPublic(): bool
302302
private function getReturnType(): Type
303303
{
304304
if ($this->returnType === null) {
305-
$name = strtolower($this->getName());
306-
$returnType = $this->reflection->getReturnType();
307-
if ($returnType === null) {
308-
if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) {
309-
return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType);
310-
}
311-
if ($name === '__tostring') {
312-
return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType);
313-
}
314-
if ($name === '__isset') {
315-
return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType);
316-
}
317-
if ($name === '__sleep') {
318-
return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType);
319-
}
320-
if ($name === '__set_state') {
321-
return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType);
322-
}
323-
}
324-
325-
$this->returnType = TypehintHelper::decideTypeFromReflection(
326-
$returnType,
305+
$this->returnType = TypehintHelper::decideType(
306+
$this->getNativeReturnType(),
327307
$this->phpDocReturnType,
328-
$this->declaringClass,
329308
);
330309
}
331310

@@ -344,8 +323,28 @@ private function getPhpDocReturnType(): Type
344323
private function getNativeReturnType(): Type
345324
{
346325
if ($this->nativeReturnType === null) {
326+
$returnType = $this->reflection->getReturnType();
327+
if ($returnType === null) {
328+
$name = strtolower($this->getName());
329+
if (in_array($this->getName(), ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) {
330+
return $this->nativeReturnType = new VoidType();
331+
}
332+
if ($name === '__tostring') {
333+
return $this->nativeReturnType = new StringType();
334+
}
335+
if ($name === '__isset') {
336+
return $this->nativeReturnType = new BooleanType();
337+
}
338+
if ($name === '__sleep') {
339+
return $this->nativeReturnType = new ArrayType(new IntegerType(), new StringType());
340+
}
341+
if ($name === '__set_state') {
342+
return $this->nativeReturnType = new ObjectWithoutClassType();
343+
}
344+
}
345+
347346
$this->nativeReturnType = TypehintHelper::decideTypeFromReflection(
348-
$this->reflection->getReturnType(),
347+
$returnType,
349348
null,
350349
$this->declaringClass,
351350
);

src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
139139
);
140140
}, $functionSignature->getParameters()),
141141
$functionSignature->isVariadic(),
142-
TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType),
142+
null,
143143
$phpDocReturnType ?? new MixedType(),
144144
$functionSignature->getReturnType(),
145145
);

src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php

+7-7
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
8686
{
8787
$selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null;
8888
$variantFn = function (ExtendedParametersAcceptor $acceptor) use (&$selfOutType): ExtendedParametersAcceptor {
89-
$originalReturnType = $acceptor->getReturnType();
90-
if ($originalReturnType instanceof ThisType && $selfOutType !== null) {
91-
$returnType = TypeCombinator::intersect($selfOutType, $this->transformStaticType($originalReturnType));
92-
$selfOutType = $returnType;
89+
$originalPhpDocReturnType = $acceptor->getPhpDocReturnType();
90+
if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) {
91+
$phpDocReturnType = TypeCombinator::intersect($selfOutType, $this->transformStaticType($originalPhpDocReturnType));
92+
$selfOutType = $phpDocReturnType;
9393
} else {
94-
$returnType = $this->transformStaticType($originalReturnType);
94+
$phpDocReturnType = $this->transformStaticType($originalPhpDocReturnType);
9595
}
9696
return new ExtendedFunctionVariant(
9797
$acceptor->getTemplateTypeMap(),
@@ -114,8 +114,8 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
114114
$acceptor->getParameters(),
115115
),
116116
$acceptor->isVariadic(),
117-
$returnType,
118-
$this->transformStaticType($acceptor->getPhpDocReturnType()),
117+
null,
118+
$phpDocReturnType,
119119
$this->transformStaticType($acceptor->getNativeReturnType()),
120120
$acceptor->getCallSiteVarianceMap(),
121121
);

src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
8282
{
8383
$selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null;
8484
$variantFn = function (ExtendedParametersAcceptor $acceptor) use ($selfOutType): ExtendedParametersAcceptor {
85-
$originalReturnType = $acceptor->getReturnType();
86-
if ($originalReturnType instanceof ThisType && $selfOutType !== null) {
87-
$returnType = $selfOutType;
85+
$originalPhpDocReturnType = $acceptor->getPhpDocReturnType();
86+
if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) {
87+
$phpDocReturnType = $selfOutType;
8888
} else {
89-
$returnType = $this->transformStaticType($originalReturnType);
89+
$phpDocReturnType = $this->transformStaticType($originalPhpDocReturnType);
9090
}
9191
return new ExtendedFunctionVariant(
9292
$acceptor->getTemplateTypeMap(),
@@ -109,8 +109,8 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
109109
$acceptor->getParameters(),
110110
),
111111
$acceptor->isVariadic(),
112-
$returnType,
113-
$this->transformStaticType($acceptor->getPhpDocReturnType()),
112+
null,
113+
$phpDocReturnType,
114114
$this->transformStaticType($acceptor->getNativeReturnType()),
115115
$acceptor->getCallSiteVarianceMap(),
116116
);

src/Reflection/Type/IntersectionTypeMethodReflection.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public function getPrototype(): ClassMemberReflection
7979

8080
public function getVariants(): array
8181
{
82-
$returnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getReturnType(), $method->getVariants())), $this->methods));
8382
$phpDocReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getPhpDocReturnType(), $method->getVariants())), $this->methods));
8483
$nativeReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getNativeReturnType(), $method->getVariants())), $this->methods));
8584

@@ -88,7 +87,7 @@ public function getVariants(): array
8887
$acceptor->getResolvedTemplateTypeMap(),
8988
$acceptor->getParameters(),
9089
$acceptor->isVariadic(),
91-
$returnType,
90+
null,
9291
$phpDocReturnType,
9392
$nativeReturnType,
9493
$acceptor->getCallSiteVarianceMap(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace ConditionalReturnStaticUnion;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Config {}
8+
class MainConfig
9+
{
10+
/**
11+
* @param array<mixed>|Config $value
12+
* @return ($value is array ? Config : $this)
13+
*/
14+
public function invalidReturn(array|Config $value = []): Config|static
15+
{
16+
if (is_array($value)) {
17+
return new Config();
18+
}
19+
return $this;
20+
}
21+
22+
/**
23+
* @param array<mixed>|Config $value
24+
* @return ($value is array ? Config : $this)
25+
*/
26+
public function validReturn(array|Config $value = []): Config|self
27+
{
28+
if (is_array($value)) {
29+
return new Config();
30+
}
31+
return $this;
32+
}
33+
}
34+
35+
function (MainConfig $c): void {
36+
assertType(Config::class, (new MainConfig())->invalidReturn());
37+
assertType(Config::class, (new MainConfig())->validReturn());
38+
assertType(MainConfig::class, (new MainConfig())->invalidReturn(new Config()));
39+
assertType(MainConfig::class, (new MainConfig())->validReturn(new Config()));
40+
41+
assertType(Config::class, $c->invalidReturn());
42+
assertType(Config::class, $c->validReturn());
43+
assertType(MainConfig::class, $c->invalidReturn(new Config()));
44+
assertType(MainConfig::class, $c->validReturn(new Config()));
45+
};

0 commit comments

Comments
 (0)