Skip to content
Closed
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 @@ -1723,6 +1723,12 @@ parameters:
count: 1
path: src/Type/TypehintHelper.php

-
rawMessage: '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

-
rawMessage: '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 @@ -6,6 +6,7 @@
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\Type;
use PHPStan\Type\TypehintHelper;

/**
* @api
Expand All @@ -23,7 +24,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 @@ -34,7 +35,10 @@ public function __construct(
$resolvedTemplateTypeMap,
$parameters,
$isVariadic,
$returnType,
$returnType ?? TypehintHelper::decideType(
$nativeReturnType,
$phpDocReturnType,
),
$callSiteVarianceMap,
);
}
Expand All @@ -50,6 +54,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
10 changes: 4 additions & 6 deletions src/Reflection/Php/PhpClassReflectionExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -1109,19 +1109,17 @@ private function createNativeMethodVariant(
);
}

if ($phpDocFromStubs && $phpDocReturnType !== null) {
$returnType = $phpDocReturnType;
} else {
$returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType);
if (!$phpDocFromStubs || $phpDocReturnType === null) {
$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 @@ -127,7 +127,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 @@ -96,7 +96,7 @@ public function getVariants(): array
null,
$this->getParameters(),
$this->isVariadic(),
$this->getReturnType(),
null,
$this->getPhpDocReturnType(),
$this->getNativeReturnType(),
),
Expand Down
57 changes: 28 additions & 29 deletions src/Reflection/Php/PhpMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public function getVariants(): array
null,
$this->getParameters(),
$this->isVariadic(),
$this->getReturnType(),
null,
$this->getPhpDocReturnType(),
$this->getNativeReturnType(),
),
Expand Down Expand Up @@ -263,52 +263,51 @@ public function isPublic(): bool

private function getReturnType(): Type
{
if ($this->returnType === null) {
$name = strtolower($this->getName());
return $this->returnType ??= TypehintHelper::decideType(
$this->getNativeReturnType(),
$this->phpDocReturnType,
);
}

private function getPhpDocReturnType(): Type
{
if ($this->phpDocReturnType !== null) {
return $this->phpDocReturnType;
}

return new MixedType();
}

private function getNativeReturnType(): Type
{
if ($this->nativeReturnType === null) {
$returnType = $this->reflection->getReturnType();
if ($returnType === null) {
$name = strtolower($this->getName());
if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) {
return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType);
return $this->nativeReturnType = new VoidType();
}
if ($name === '__tostring') {
return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType);
return $this->nativeReturnType = new StringType();
}
if ($name === '__isset') {
return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType);
return $this->nativeReturnType = new BooleanType();
}
if ($name === '__sleep') {
return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType);
return $this->nativeReturnType = new ArrayType(new IntegerType(), new StringType());
}
if ($name === '__set_state') {
return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType);
return $this->nativeReturnType = new ObjectWithoutClassType();
}
}

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

return $this->returnType;
}

private function getPhpDocReturnType(): Type
{
if ($this->phpDocReturnType !== null) {
return $this->phpDocReturnType;
}

return new MixedType();
}

private function getNativeReturnType(): Type
{
return $this->nativeReturnType ??= TypehintHelper::decideTypeFromReflection(
$this->reflection->getReturnType(),
selfClass: $this->declaringClass,
);
return $this->nativeReturnType;
}

public function getDeprecatedDescription(): ?string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,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 @@ -88,16 +88,15 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
{
$selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null;
$variantFn = function (ExtendedParametersAcceptor $acceptor) use (&$selfOutType): ExtendedParametersAcceptor {
$originalReturnType = $acceptor->getReturnType();
$transformedReturnType = $this->transformStaticType($originalReturnType);
if ($originalReturnType instanceof ThisType && $selfOutType !== null) {
$returnType = TypeCombinator::intersect($selfOutType, $transformedReturnType);
$selfOutType = $returnType;
$originalPhpDocReturnType = $acceptor->getPhpDocReturnType();
$transformedPhpDocReturnType = $this->transformStaticType($originalPhpDocReturnType);
if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) {
$phpDocReturnType = TypeCombinator::intersect($selfOutType, $transformedPhpDocReturnType);
$selfOutType = $phpDocReturnType;
} else {
$returnType = $transformedReturnType;
$phpDocReturnType = $transformedPhpDocReturnType;
}

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

return new ExtendedFunctionVariant(
Expand Down Expand Up @@ -128,9 +127,9 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection {
$acceptor->getParameters(),
),
$acceptor->isVariadic(),
$returnType,
$originalReturnType->equals($phpDocReturnType) ? $transformedReturnType : $this->transformStaticType($phpDocReturnType),
$originalReturnType->equals($nativeReturnType) ? $transformedReturnType : $this->transformStaticType($nativeReturnType),
null,
$phpDocReturnType,
$originalPhpDocReturnType->equals($nativeReturnType) ? $transformedPhpDocReturnType : $this->transformStaticType($nativeReturnType),
$acceptor->getCallSiteVarianceMap(),
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,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 @@ -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
8 changes: 1 addition & 7 deletions src/Reflection/Type/IntersectionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,17 @@ public function getPrototype(): ClassMemberReflection

public function getVariants(): array
{
$returnTypes = [];
$phpDocReturnTypes = [];
$nativeReturnTypes = [];
foreach ($this->methods as $method) {
$variants = $method->getVariants();

foreach ($variants as $acceptor) {
$returnTypes[] = $acceptor->getReturnType();
$phpDocReturnTypes[] = $acceptor->getPhpDocReturnType();
$nativeReturnTypes[] = $acceptor->getNativeReturnType();
}
}

$returnType = $returnTypes[0];
for ($i = 1, $count = count($returnTypes); $i < $count; $i++) {
$returnType = TypeCombinator::intersect($returnType, $returnTypes[$i]);
}
$phpDocReturnType = $phpDocReturnTypes[0];
for ($i = 1, $count = count($phpDocReturnTypes); $i < $count; $i++) {
$phpDocReturnType = TypeCombinator::intersect($phpDocReturnType, $phpDocReturnTypes[$i]);
Expand All @@ -111,7 +105,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 @@ -13,6 +13,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 @@ -126,11 +127,26 @@ 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 (
($type->isCallable()->yes() && $phpDocType->isCallable()->yes())
|| (
(!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed()))
&& $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes()
&& $isSuperType->yes()
)
) {
$resultType = $phpDocType;
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 // lint >= 8.0

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()));
};
4 changes: 2 additions & 2 deletions turbo-ext/src/AcceptorValues.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ inline constexpr int8_t functionVariantSlots[PT_PA_MEMBER_COUNT] = {
};

inline constexpr int8_t extendedFunctionVariantSlots[PT_PA_MEMBER_COUNT] = {
PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_FV(returnType),
PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_EFV(returnType),
PT_ACC_EFV(phpDocReturnType), PT_ACC_EFV(nativeReturnType), PT_ACC_FV(callSiteVarianceMap), PT_ACC_NONE, PT_ACC_NONE,
PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE,
};

inline constexpr int8_t extendedCallableFunctionVariantSlots[PT_PA_MEMBER_COUNT] = {
PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_FV(returnType),
PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_EFV(returnType),
PT_ACC_EFV(phpDocReturnType), PT_ACC_EFV(nativeReturnType), PT_ACC_FV(callSiteVarianceMap), PT_ACC_NONE, PT_ACC_NONE,
PT_ACC_ECFV(throwPoints), PT_ACC_ECFV(isPure), PT_ACC_ECFV(impurePoints), PT_ACC_ECFV(invalidateExpressions), PT_ACC_ECFV(usedVariables),
PT_ACC_ECFV(acceptsNamedArguments), PT_ACC_ECFV(mustUseReturnValue), PT_ACC_NONE, PT_ACC_NONE,
Expand Down
Loading
Loading