Skip to content

Resolve ExtendedFunctionVariant::getReturnType() more lazily #3966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: 2.1.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1854,6 +1854,12 @@ parameters:
count: 1
path: src/Type/TypehintHelper.php

-
message: '#^Doing instanceof PHPStan\\Type\\TypeWithClassName is error\-prone and deprecated\. Use Type\:\:getObjectClassNames\(\) or Type\:\:getObjectClassReflections\(\) instead\.$#'
identifier: phpstanApi.instanceofType
count: 1
path: src/Type/TypehintHelper.php

-
message: '#^Doing instanceof PHPStan\\Type\\CallableType is error\-prone and deprecated\. Use Type\:\:isCallable\(\) and Type\:\:getCallableParametersAcceptors\(\) instead\.$#'
identifier: phpstanApi.instanceofType
Expand Down
20 changes: 18 additions & 2 deletions src/Reflection/ExtendedFunctionVariant.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\Type;
use PHPStan\Type\TypehintHelper;

/**
* @api
Expand All @@ -21,7 +22,7 @@ public function __construct(
?TemplateTypeMap $resolvedTemplateTypeMap,
array $parameters,
bool $isVariadic,
Type $returnType,
private ?Type $returnType,
private Type $phpDocReturnType,
private Type $nativeReturnType,
?TemplateTypeVarianceMap $callSiteVarianceMap = null,
Expand All @@ -32,7 +33,10 @@ public function __construct(
$resolvedTemplateTypeMap,
$parameters,
$isVariadic,
$returnType,
$returnType ?? TypehintHelper::decideType(
$nativeReturnType,
$phpDocReturnType,
),
$callSiteVarianceMap,
);
}
Expand All @@ -48,6 +52,18 @@ public function getParameters(): array
return $parameters;
}

public function getReturnType(): Type
{
if ($this->returnType === null) {
return $this->returnType = TypehintHelper::decideType(
$this->nativeReturnType,
$this->phpDocReturnType,
);
}

return $this->returnType;
}

public function getPhpDocReturnType(): Type
{
return $this->phpDocReturnType;
Expand Down
7 changes: 3 additions & 4 deletions src/Reflection/Php/PhpClassReflectionExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -954,19 +954,18 @@ private function createNativeMethodVariant(
}

if ($stubPhpDocReturnType !== null) {
$returnType = $stubPhpDocReturnType;
$phpDocReturnType = $stubPhpDocReturnType;
} else {
$returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType);
$phpDocReturnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType);
}

return new ExtendedFunctionVariant(
TemplateTypeMap::createEmpty(),
null,
$parameters,
$methodSignature->isVariadic(),
$returnType,
$phpDocReturnType ?? new MixedType(),
null,
$phpDocReturnType,
$methodSignature->getNativeReturnType(),
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/Php/PhpFunctionFromParserNodeReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public function getVariants(): array
$this->getResolvedTemplateTypeMap(),
$this->getParameters(),
$this->isVariadic(),
$this->getReturnType(),
null,
$this->getPhpDocReturnType(),
$this->getNativeReturnType(),
),
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/Php/PhpFunctionReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function getVariants(): array
null,
$this->getParameters(),
$this->isVariadic(),
$this->getReturnType(),
null,
$this->getPhpDocReturnType(),
$this->getNativeReturnType(),
),
Expand Down
49 changes: 24 additions & 25 deletions src/Reflection/Php/PhpMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public function getVariants(): array
null,
$this->getParameters(),
$this->isVariadic(),
$this->getReturnType(),
null,
$this->getPhpDocReturnType(),
$this->getNativeReturnType(),
),
Expand Down Expand Up @@ -302,30 +302,9 @@ public function isPublic(): bool
private function getReturnType(): Type
{
if ($this->returnType === null) {
$name = strtolower($this->getName());
$returnType = $this->reflection->getReturnType();
if ($returnType === null) {
if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) {
return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType);
}
if ($name === '__tostring') {
return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType);
}
if ($name === '__isset') {
return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType);
}
if ($name === '__sleep') {
return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType);
}
if ($name === '__set_state') {
return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType);
}
}

$this->returnType = TypehintHelper::decideTypeFromReflection(
$returnType,
$this->returnType = TypehintHelper::decideType(
$this->getNativeReturnType(),
$this->phpDocReturnType,
$this->declaringClass,
);
}

Expand All @@ -344,8 +323,28 @@ private function getPhpDocReturnType(): Type
private function getNativeReturnType(): Type
{
if ($this->nativeReturnType === null) {
$returnType = $this->reflection->getReturnType();
if ($returnType === null) {
$name = strtolower($this->getName());
if (in_array($this->getName(), ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) {
return $this->nativeReturnType = new VoidType();
}
if ($name === '__tostring') {
return $this->nativeReturnType = new StringType();
}
if ($name === '__isset') {
return $this->nativeReturnType = new BooleanType();
}
if ($name === '__sleep') {
return $this->nativeReturnType = new ArrayType(new IntegerType(), new StringType());
}
if ($name === '__set_state') {
return $this->nativeReturnType = new ObjectWithoutClassType();
}
}

$this->nativeReturnType = TypehintHelper::decideTypeFromReflection(
$this->reflection->getReturnType(),
$returnType,
null,
$this->declaringClass,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
);
}, $functionSignature->getParameters()),
$functionSignature->isVariadic(),
TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType),
null,
$phpDocReturnType ?? new MixedType(),
$functionSignature->getReturnType(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
{
$selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null;
$variantFn = function (ExtendedParametersAcceptor $acceptor) use (&$selfOutType): ExtendedParametersAcceptor {
$originalReturnType = $acceptor->getReturnType();
if ($originalReturnType instanceof ThisType && $selfOutType !== null) {
$returnType = TypeCombinator::intersect($selfOutType, $this->transformStaticType($originalReturnType));
$selfOutType = $returnType;
$originalPhpDocReturnType = $acceptor->getPhpDocReturnType();
if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) {
$phpDocReturnType = TypeCombinator::intersect($selfOutType, $this->transformStaticType($originalPhpDocReturnType));
$selfOutType = $phpDocReturnType;
} else {
$returnType = $this->transformStaticType($originalReturnType);
$phpDocReturnType = $this->transformStaticType($originalPhpDocReturnType);
}
return new ExtendedFunctionVariant(
$acceptor->getTemplateTypeMap(),
Expand All @@ -114,8 +114,8 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
$acceptor->getParameters(),
),
$acceptor->isVariadic(),
$returnType,
$this->transformStaticType($acceptor->getPhpDocReturnType()),
null,
$phpDocReturnType,
$this->transformStaticType($acceptor->getNativeReturnType()),
$acceptor->getCallSiteVarianceMap(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
{
$selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null;
$variantFn = function (ExtendedParametersAcceptor $acceptor) use ($selfOutType): ExtendedParametersAcceptor {
$originalReturnType = $acceptor->getReturnType();
if ($originalReturnType instanceof ThisType && $selfOutType !== null) {
$returnType = $selfOutType;
$originalPhpDocReturnType = $acceptor->getPhpDocReturnType();
if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) {
$phpDocReturnType = $selfOutType;
} else {
$returnType = $this->transformStaticType($originalReturnType);
$phpDocReturnType = $this->transformStaticType($originalPhpDocReturnType);
}
return new ExtendedFunctionVariant(
$acceptor->getTemplateTypeMap(),
Expand All @@ -109,8 +109,8 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
$acceptor->getParameters(),
),
$acceptor->isVariadic(),
$returnType,
$this->transformStaticType($acceptor->getPhpDocReturnType()),
null,
$phpDocReturnType,
$this->transformStaticType($acceptor->getNativeReturnType()),
$acceptor->getCallSiteVarianceMap(),
);
Expand Down
3 changes: 1 addition & 2 deletions src/Reflection/Type/IntersectionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public function getPrototype(): ClassMemberReflection

public function getVariants(): array
{
$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));
$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));
$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));

Expand All @@ -88,7 +87,7 @@ public function getVariants(): array
$acceptor->getResolvedTemplateTypeMap(),
$acceptor->getParameters(),
$acceptor->isVariadic(),
$returnType,
null,
$phpDocReturnType,
$nativeReturnType,
$acceptor->getCallSiteVarianceMap(),
Expand Down
18 changes: 17 additions & 1 deletion src/Type/TypehintHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use ReflectionType;
use Traversable;
use function array_map;
use function count;
use function get_class;
Expand Down Expand Up @@ -118,9 +119,24 @@ public static function decideType(
}
}

$resolvedPhpDocTypeToBounds = TemplateTypeHelper::resolveToBounds($phpDocType);
$isSuperType = $type->isSuperTypeOf($resolvedPhpDocTypeToBounds);
if (
!$isSuperType->yes()
&& (new ObjectType(Traversable::class))->isSuperTypeOf($phpDocType)->yes()
&& $type instanceof TypeWithClassName
) {
// if native type is Iterator and PHPDoc type is Traversable
// Allow PHPDoc type to win
$traversableAncestor = $type->getAncestorWithClassName(Traversable::class);
if ($traversableAncestor !== null) {
$isSuperType = $traversableAncestor->isSuperTypeOf($resolvedPhpDocTypeToBounds);
}
}

if (
(!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed()))
&& $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes()
&& $isSuperType->yes()
) {
$resultType = $phpDocType;
} else {
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/nsrt/conditional-return-static-union.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace ConditionalReturnStaticUnion;

use function PHPStan\Testing\assertType;

class Config {}
class MainConfig
{
/**
* @param array<mixed>|Config $value
* @return ($value is array ? Config : $this)
*/
public function invalidReturn(array|Config $value = []): Config|static
{
if (is_array($value)) {
return new Config();
}
return $this;
}

/**
* @param array<mixed>|Config $value
* @return ($value is array ? Config : $this)
*/
public function validReturn(array|Config $value = []): Config|self
{
if (is_array($value)) {
return new Config();
}
return $this;
}
}

function (MainConfig $c): void {
assertType(Config::class, (new MainConfig())->invalidReturn());
assertType(Config::class, (new MainConfig())->validReturn());
assertType(MainConfig::class, (new MainConfig())->invalidReturn(new Config()));
assertType(MainConfig::class, (new MainConfig())->validReturn(new Config()));

assertType(Config::class, $c->invalidReturn());
assertType(Config::class, $c->validReturn());
assertType(MainConfig::class, $c->invalidReturn(new Config()));
assertType(MainConfig::class, $c->validReturn(new Config()));
};
Loading