From 0a843720af72968eaedab539fc2e97890faaa3da 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/UnionType.php | 7 +- .../Rules/Methods/CallMethodsRuleTest.php | 8 + .../Rules/Methods/ReturnTypeRuleTest.php | 5 - tests/PHPStan/Rules/Methods/data/bug-5591.php | 46 ++++ tests/PHPStan/Type/UnionTypeTest.php | 216 ++++++++++++++++++ 5 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-5591.php diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 4d31f05c9d..cbad84bc79 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 97b53bebd6..bbe47af18b 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 ef489b933c..78cb5edf89 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 0000000000..b8510d13ee --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5591.php @@ -0,0 +1,46 @@ + 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 + { + $class = $this->entityClass; + return new $class(); + } +} + +/** + * @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 ef549da57e..3f60141747 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -344,6 +344,94 @@ 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 +862,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(), + ], + ]; }