From 176f15f3f11fe26eec8cc29937c0d8e3010f6024 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 1 Feb 2022 18:56:38 +0100 Subject: [PATCH] Fix interaction between template type and union type --- src/Type/Generic/TemplateTypeTrait.php | 1 + src/Type/UnionType.php | 7 +- .../Rules/Methods/CallMethodsRuleTest.php | 8 + .../Rules/Methods/ReturnTypeRuleTest.php | 5 - tests/PHPStan/Rules/Methods/data/bug-5591.php | 45 ++++ tests/PHPStan/Type/UnionTypeTest.php | 221 ++++++++++++++++++ 6 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-5591.php diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 387587c5a6c..10d52a25b6d 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\TrinaryLogic; +use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 4d31f05c9d1..cbad84bc79d 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -92,7 +92,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return TrinaryLogic::createYes(); } - if ($type instanceof CompoundType && !$type instanceof CallableType) { + if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateUnionType) { return $type->isAcceptedBy($this, $strictTypes); } @@ -106,7 +106,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function isSuperTypeOf(Type $otherType): TrinaryLogic { - if ($otherType instanceof self || $otherType instanceof IterableType) { + if ( + ($otherType instanceof self && !$otherType instanceof TemplateUnionType) + || $otherType instanceof IterableType + ) { return $otherType->isSubTypeOf($this); } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 97b53bebd69..bbe47af18b6 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2052,6 +2052,14 @@ public function testBug5258(): void $this->analyse([__DIR__ . '/data/bug-5258.php'], []); } + public function testBug5591(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5591.php'], []); + } + public function testGenericObjectLowerBound(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index ef489b933c7..78cb5edf89e 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -528,11 +528,6 @@ public function testTemplateUnion(): void 'Method ReturnTemplateUnion\Foo::doFoo2() should return T of bool|float|int|string but returns (T of bool|float|int|string)|null.', 25, ], - [ - // should not be reported - 'Method ReturnTemplateUnion\Foo::doFoo3() should return (T of bool|float|int|string)|null but returns (T of bool|float|int|string)|null.', - 35, - ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5591.php b/tests/PHPStan/Rules/Methods/data/bug-5591.php new file mode 100644 index 00000000000..866ec28eca7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5591.php @@ -0,0 +1,45 @@ + The fully-qualified (::class) class name of the entity being managed. */ + protected $entityClass; + + /** @param TEntity|null $record */ + public function outerMethod($record = null): void + { + $record = $this->innerMethod($record); + } + + /** + * @param TEntity|null $record + * + * @return TEntity + */ + public function innerMethod($record = null): object + { + return new ($this->entityClass)(); + } +} + +/** + * @template TEntity as EntityA|EntityB + * @extends TestClass + */ +class TestClass2 extends TestClass +{ + public function outerMethod($record = null): void + { + $record = $this->innerMethod($record); + } +} diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index ef549da57ee..5dca80e30d8 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -20,11 +20,15 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\IntegerType; +use PHPStan\Type\NullType; +use PHPStan\Type\StringType; use stdClass; use function array_map; use function array_merge; @@ -344,6 +348,95 @@ public function dataIsSuperTypeOf(): Iterator new IntersectionType([new StringType(), new CallableType()]), TrinaryLogic::createNo(), ]; + + yield 'is super type of template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'maybe super type of template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ]; + + yield 'is super type of template-of-string equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'maybe super type of template-of-string sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ]; + } /** @@ -774,6 +867,134 @@ public function dataAccepts(): array new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], + 'accepts template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'maybe accepts template-of-union sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts template-of-string equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'maybe accepts template-of-string sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts template-of-union containing a union member' => [ + new UnionType([ + new IntegerType(), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts intersection with template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new ObjectType('Iterator'), + new ObjectType('IteratorAggregate'), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new ObjectType('Iterator'), + new ObjectType('IteratorAggregate'), + ]), + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('Countable'), + ]), + TrinaryLogic::createYes(), + ], + ]; }