Skip to content

Commit 807bca9

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 091c5a7 commit 807bca9

18 files changed

Lines changed: 216 additions & 126 deletions

‎src/Reflection/ExtendedFunctionVariant.php‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Type\Generic\TemplateTypeMap;
77
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
88
use PHPStan\Type\Type;
9+
use PHPStan\Type\TypehintHelper;
910

1011
/**
1112
* @api
@@ -23,7 +24,7 @@ public function __construct(
2324
?TemplateTypeMap $resolvedTemplateTypeMap,
2425
array $parameters,
2526
bool $isVariadic,
26-
Type $returnType,
27+
private ?Type $returnType,
2728
private Type $phpDocReturnType,
2829
private Type $nativeReturnType,
2930
?TemplateTypeVarianceMap $callSiteVarianceMap = null,
@@ -34,7 +35,10 @@ public function __construct(
3435
$resolvedTemplateTypeMap,
3536
$parameters,
3637
$isVariadic,
37-
$returnType,
38+
$returnType ?? TypehintHelper::decideType(
39+
$nativeReturnType,
40+
$phpDocReturnType,
41+
),
3842
$callSiteVarianceMap,
3943
);
4044
}
@@ -50,6 +54,18 @@ public function getParameters(): array
5054
return $parameters;
5155
}
5256

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

‎src/Reflection/Php/PhpClassReflectionExtension.php‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,19 +1109,17 @@ private function createNativeMethodVariant(
11091109
);
11101110
}
11111111

1112-
if ($phpDocFromStubs && $phpDocReturnType !== null) {
1113-
$returnType = $phpDocReturnType;
1114-
} else {
1115-
$returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType);
1112+
if (!$phpDocFromStubs || $phpDocReturnType === null) {
1113+
$phpDocReturnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType);
11161114
}
11171115

11181116
return new ExtendedFunctionVariant(
11191117
TemplateTypeMap::createEmpty(),
11201118
null,
11211119
$parameters,
11221120
$methodSignature->isVariadic(),
1123-
$returnType,
1124-
$phpDocReturnType ?? new MixedType(),
1121+
null,
1122+
$phpDocReturnType,
11251123
$methodSignature->getNativeReturnType(),
11261124
);
11271125
}

‎src/Reflection/Php/PhpFunctionFromParserNodeReflection.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public function getVariants(): array
127127
$this->getResolvedTemplateTypeMap(),
128128
$this->getParameters(),
129129
$this->isVariadic(),
130-
$this->getReturnType(),
130+
null,
131131
$this->getPhpDocReturnType(),
132132
$this->getNativeReturnType(),
133133
),

‎src/Reflection/Php/PhpFunctionReflection.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public function getVariants(): array
9696
null,
9797
$this->getParameters(),
9898
$this->isVariadic(),
99-
$this->getReturnType(),
99+
null,
100100
$this->getPhpDocReturnType(),
101101
$this->getNativeReturnType(),
102102
),

‎src/Reflection/Php/PhpMethodReflection.php‎

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ public function getVariants(): array
209209
null,
210210
$this->getParameters(),
211211
$this->isVariadic(),
212-
$this->getReturnType(),
212+
null,
213213
$this->getPhpDocReturnType(),
214214
$this->getNativeReturnType(),
215215
),
@@ -263,52 +263,51 @@ public function isPublic(): bool
263263

264264
private function getReturnType(): Type
265265
{
266-
if ($this->returnType === null) {
267-
$name = strtolower($this->getName());
266+
return $this->returnType ??= TypehintHelper::decideType(
267+
$this->getNativeReturnType(),
268+
$this->phpDocReturnType,
269+
);
270+
}
271+
272+
private function getPhpDocReturnType(): Type
273+
{
274+
if ($this->phpDocReturnType !== null) {
275+
return $this->phpDocReturnType;
276+
}
277+
278+
return new MixedType();
279+
}
280+
281+
private function getNativeReturnType(): Type
282+
{
283+
if ($this->nativeReturnType === null) {
268284
$returnType = $this->reflection->getReturnType();
269285
if ($returnType === null) {
286+
$name = strtolower($this->getName());
270287
if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) {
271-
return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType);
288+
return $this->nativeReturnType = new VoidType();
272289
}
273290
if ($name === '__tostring') {
274-
return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType);
291+
return $this->nativeReturnType = new StringType();
275292
}
276293
if ($name === '__isset') {
277-
return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType);
294+
return $this->nativeReturnType = new BooleanType();
278295
}
279296
if ($name === '__sleep') {
280-
return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType);
297+
return $this->nativeReturnType = new ArrayType(new IntegerType(), new StringType());
281298
}
282299
if ($name === '__set_state') {
283-
return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType);
300+
return $this->nativeReturnType = new ObjectWithoutClassType();
284301
}
285302
}
286303

287-
$this->returnType = TypehintHelper::decideTypeFromReflection(
304+
$this->nativeReturnType = TypehintHelper::decideTypeFromReflection(
288305
$returnType,
289-
$this->phpDocReturnType,
290-
$this->declaringClass,
306+
selfClass: $this->declaringClass,
291307
);
292308
}
293309

294-
return $this->returnType;
295-
}
296-
297-
private function getPhpDocReturnType(): Type
298-
{
299-
if ($this->phpDocReturnType !== null) {
300-
return $this->phpDocReturnType;
301-
}
302-
303-
return new MixedType();
304-
}
305-
306-
private function getNativeReturnType(): Type
307-
{
308-
return $this->nativeReturnType ??= TypehintHelper::decideTypeFromReflection(
309-
$this->reflection->getReturnType(),
310-
selfClass: $this->declaringClass,
311-
);
310+
return $this->nativeReturnType;
312311
}
313312

314313
public function getDeprecatedDescription(): ?string

‎src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
176176
);
177177
}, $functionSignature->getParameters()),
178178
$functionSignature->isVariadic(),
179-
TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType),
179+
null,
180180
$phpDocReturnType ?? new MixedType(),
181181
$functionSignature->getReturnType(),
182182
);

‎src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,15 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
8888
{
8989
$selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null;
9090
$variantFn = function (ExtendedParametersAcceptor $acceptor) use (&$selfOutType): ExtendedParametersAcceptor {
91-
$originalReturnType = $acceptor->getReturnType();
92-
$transformedReturnType = $this->transformStaticType($originalReturnType);
93-
if ($originalReturnType instanceof ThisType && $selfOutType !== null) {
94-
$returnType = TypeCombinator::intersect($selfOutType, $transformedReturnType);
95-
$selfOutType = $returnType;
91+
$originalPhpDocReturnType = $acceptor->getPhpDocReturnType();
92+
$transformedPhpDocReturnType = $this->transformStaticType($originalPhpDocReturnType);
93+
if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) {
94+
$phpDocReturnType = TypeCombinator::intersect($selfOutType, $transformedPhpDocReturnType);
95+
$selfOutType = $phpDocReturnType;
9696
} else {
97-
$returnType = $transformedReturnType;
97+
$phpDocReturnType = $transformedPhpDocReturnType;
9898
}
9999

100-
$phpDocReturnType = $acceptor->getPhpDocReturnType();
101100
$nativeReturnType = $acceptor->getNativeReturnType();
102101

103102
return new ExtendedFunctionVariant(
@@ -128,9 +127,9 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection {
128127
$acceptor->getParameters(),
129128
),
130129
$acceptor->isVariadic(),
131-
$returnType,
132-
$originalReturnType->equals($phpDocReturnType) ? $transformedReturnType : $this->transformStaticType($phpDocReturnType),
133-
$originalReturnType->equals($nativeReturnType) ? $transformedReturnType : $this->transformStaticType($nativeReturnType),
130+
null,
131+
$phpDocReturnType,
132+
$originalPhpDocReturnType->equals($nativeReturnType) ? $transformedPhpDocReturnType : $this->transformStaticType($nativeReturnType),
134133
$acceptor->getCallSiteVarianceMap(),
135134
);
136135
};

‎src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
8484
{
8585
$selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null;
8686
$variantFn = function (ExtendedParametersAcceptor $acceptor) use ($selfOutType): ExtendedParametersAcceptor {
87-
$originalReturnType = $acceptor->getReturnType();
88-
if ($originalReturnType instanceof ThisType && $selfOutType !== null) {
89-
$returnType = $selfOutType;
87+
$originalPhpDocReturnType = $acceptor->getPhpDocReturnType();
88+
if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) {
89+
$phpDocReturnType = $selfOutType;
9090
} else {
91-
$returnType = $this->transformStaticType($originalReturnType);
91+
$phpDocReturnType = $this->transformStaticType($originalPhpDocReturnType);
9292
}
9393
return new ExtendedFunctionVariant(
9494
$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/IntersectionTypeMethodReflection.php‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,23 +81,17 @@ public function getPrototype(): ClassMemberReflection
8181

8282
public function getVariants(): array
8383
{
84-
$returnTypes = [];
8584
$phpDocReturnTypes = [];
8685
$nativeReturnTypes = [];
8786
foreach ($this->methods as $method) {
8887
$variants = $method->getVariants();
8988

9089
foreach ($variants as $acceptor) {
91-
$returnTypes[] = $acceptor->getReturnType();
9290
$phpDocReturnTypes[] = $acceptor->getPhpDocReturnType();
9391
$nativeReturnTypes[] = $acceptor->getNativeReturnType();
9492
}
9593
}
9694

97-
$returnType = $returnTypes[0];
98-
for ($i = 1, $count = count($returnTypes); $i < $count; $i++) {
99-
$returnType = TypeCombinator::intersect($returnType, $returnTypes[$i]);
100-
}
10195
$phpDocReturnType = $phpDocReturnTypes[0];
10296
for ($i = 1, $count = count($phpDocReturnTypes); $i < $count; $i++) {
10397
$phpDocReturnType = TypeCombinator::intersect($phpDocReturnType, $phpDocReturnTypes[$i]);
@@ -111,7 +105,7 @@ public function getVariants(): array
111105
$acceptor->getResolvedTemplateTypeMap(),
112106
$acceptor->getParameters(),
113107
$acceptor->isVariadic(),
114-
$returnType,
108+
null,
115109
$phpDocReturnType,
116110
$nativeReturnType,
117111
$acceptor->getCallSiteVarianceMap(),
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php // lint >= 8.0
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)