diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 1373c268ee5..bc3c6846b14 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -13,6 +13,7 @@ use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\ErrorType; @@ -202,7 +203,28 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe if (!$thisVariance->invariant()) { $results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i], $strictVariance); } else { - $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i], $strictVariance); + $varianceResult = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i], $strictVariance); + + // Invariance means `Foo` is not assignable to `Foo`, it does not + // mean the two are disjoint - the same object can satisfy both claims. Saying + // "no" outside of the accepts context would let TypeCombinator::intersect() + // and dead code detection conclude that narrowing `Foo` down to + // `Foo` is impossible. Assignability is still answered with "no", so + // variance keeps being enforced on arguments and return types. + if ( + !$acceptsContext + && $templateType->getVariance()->invariant() + && $varianceResult->no() + && !$this->types[$i]->isSuperTypeOf($ancestor->types[$i])->no() + ) { + $varianceResult = new IsSuperTypeOfResult( + TrinaryLogic::createMaybe(), + $varianceResult->reasons, + $varianceResult->lazyReasons, + ); + } + + $results[] = $varianceResult; } $results[] = IsSuperTypeOfResult::createFromBoolean($thisVariance->validPosition($ancestorVariance)); diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 39ce5efa661..54471ebd014 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -24,11 +24,13 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateArrayType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateUnionType; use function array_fill; use function array_filter; @@ -2076,6 +2078,18 @@ public static function doIntersect(Type ...$types): Type continue; } + $mergedGenericObject = self::mergeGenericObjectTypes($types[$i], $types[$j]); + if ($mergedGenericObject !== null) { + if ($mergedGenericObject instanceof NeverType) { + return $mergedGenericObject; + } + + $types[$i] = $mergedGenericObject; + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + if ( $types[$i] instanceof ArrayType && get_class($types[$i]) === ArrayType::class @@ -2139,6 +2153,61 @@ public static function doIntersect(Type ...$types): Type return new IntersectionType($types); } + /** + * Invariance of a template type means `Foo` is not assignable to `Foo`. + * It does not mean the two are disjoint - PHP generics are erased, so the same object + * can satisfy both claims. isSuperTypeOf() answers "no" in both directions there, which + * would otherwise make intersect() collapse `Foo & Foo` into never. + * + * Returns null when the two types are not the same parameterized class and the caller + * should keep its own handling, NeverType when the type arguments really are disjoint. + */ + private static function mergeGenericObjectTypes(Type $a, Type $b): ?Type + { + if (get_class($a) !== GenericObjectType::class || get_class($b) !== GenericObjectType::class) { + return null; + } + if ($a->getClassName() !== $b->getClassName()) { + return null; + } + if ($a->getSubtractedType() !== null || $b->getSubtractedType() !== null) { + return null; + } + + $aTypes = $a->getTypes(); + $bTypes = $b->getTypes(); + if (count($aTypes) !== count($bTypes)) { + return null; + } + + $aVariances = $a->getVariances(); + $bVariances = $b->getVariances(); + $invariant = TemplateTypeVariance::createInvariant(); + foreach (array_keys($aTypes) as $i) { + if (($aVariances[$i] ?? $invariant)->equals($bVariances[$i] ?? $invariant)) { + continue; + } + + return null; + } + + $newTypes = []; + foreach ($aTypes as $i => $aType) { + $newType = self::intersect($aType, $bTypes[$i]); + if ($newType instanceof NeverType) { + return $newType; + } + + $newTypes[] = $newType; + } + + return new GenericObjectType( + $a->getClassName(), + $newTypes, + variances: $aVariances === [] ? $bVariances : $aVariances, + ); + } + private static function intersectDefiniteConstantArrays(ConstantArrayType $a, ConstantArrayType $b): Type { $aSealed = $a->isUnsealed()->no(); diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 24f55aad643..182467aad87 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -52,10 +52,6 @@ private static function findTestFiles(): iterable yield __DIR__ . '/data/enum-reflection-php81.php'; } - if (PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80400) { - yield __DIR__ . '/data/enum-reflection-backed.php'; - } - if (PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 80500) { yield __DIR__ . '/data/bug-13692.php'; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15165.php b/tests/PHPStan/Analyser/nsrt/bug-15165.php new file mode 100644 index 00000000000..a424a1feef2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15165.php @@ -0,0 +1,44 @@ += 8.1 + +namespace Bug15165; + +use BackedEnum; +use ReflectionEnum; +use UnitEnum; +use function PHPStan\Testing\assertType; + +/** + * @template T of UnitEnum + * @param ReflectionEnum $enum + * @return void + */ +function testUnit(ReflectionEnum $enum): void +{ + if ($enum->isBacked()) { + assertType('ReflectionEnum', $enum); + } +} + +/** + * @template T of BackedEnum + * @param ReflectionEnum $enum + * @return void + */ +function testBacked(ReflectionEnum $enum): void +{ + if (!$enum->isBacked()) { + assertType('*NEVER*', $enum); + } +} + +/** + * @template T of BackedEnum|UnitEnum + * @param ReflectionEnum $enum + * @return void + */ +function testAny(ReflectionEnum $enum): void +{ + if ($enum->isBacked()) { + assertType('ReflectionEnum', $enum); + } +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection-backed.php b/tests/PHPStan/Analyser/nsrt/enum-reflection-backed.php similarity index 94% rename from tests/PHPStan/Analyser/data/enum-reflection-backed.php rename to tests/PHPStan/Analyser/nsrt/enum-reflection-backed.php index 00f1b9634fd..54d3f2ea433 100644 --- a/tests/PHPStan/Analyser/data/enum-reflection-backed.php +++ b/tests/PHPStan/Analyser/nsrt/enum-reflection-backed.php @@ -1,4 +1,4 @@ -= 8.1 namespace EnumReflection; diff --git a/tests/PHPStan/Analyser/nsrt/invariant-generic-narrowing.php b/tests/PHPStan/Analyser/nsrt/invariant-generic-narrowing.php new file mode 100644 index 00000000000..cb001c8d64f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/invariant-generic-narrowing.php @@ -0,0 +1,116 @@ + $b + * @phpstan-assert Box $b + */ +function assertCat(Box $b): void {} + +/** + * @template T + * @param Bag $b + * @phpstan-assert Bag $b + */ +function assertStringBag(Bag $b): void {} + +final class Asserter +{ + + /** + * @template T of object + * @param Box $b + * @phpstan-assert Box $b + */ + public function assertCat(Box $b): void {} + + /** + * @template T of object + * @param Box $b + * @phpstan-assert Box $b + */ + public static function assertCatStatic(Box $b): void {} + +} + +/** @param Box $b */ +function narrowByFunctionAssert(Box $b): void +{ + assertCat($b); + assertType('InvariantGenericNarrowing\Box', $b); +} + +/** @param Box $b */ +function narrowByMethodAssert(Box $b, Asserter $a): void +{ + $a->assertCat($b); + assertType('InvariantGenericNarrowing\Box', $b); +} + +/** @param Box $b */ +function narrowByStaticMethodAssert(Box $b): void +{ + Asserter::assertCatStatic($b); + assertType('InvariantGenericNarrowing\Box', $b); +} + +/** + * @template T of object + * @param Box $b + */ +function narrowTemplateArgument(Box $b): void +{ + assertCat($b); + assertType('InvariantGenericNarrowing\Box', $b); +} + +/** + * @param Box&Box $b + * @param CoBox&CoBox $c + */ +function intersectionInPhpDoc($b, $c): void +{ + assertType('InvariantGenericNarrowing\Box', $b); + assertType('InvariantGenericNarrowing\CoBox', $c); +} + +/** + * @template T of object + * @param Box $b + * @param Box $c + */ +function identicalComparison(Box $b, Box $c): void +{ + if ($b === $c) { + assertType('InvariantGenericNarrowing\Box', $b); + } +} + +/** + * @param Bag $b + * @param Bag $c + */ +function disjointTypeArgumentsAreStillNever(Bag $b, Bag $c): void +{ + assertStringBag($b); + assertType('*NEVER*', $b); + + assertStringBag($c); + assertType('InvariantGenericNarrowing\Bag', $c); +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index cb51bb02687..ce20252cd88 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1398,4 +1398,17 @@ public function testBug6211(): void ]); } + public function testInvariantGenericAssert(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/invariant-generic-assert.php'], [ + [ + 'Call to function assertStringBag() with InvariantGenericAssert\Bag will always evaluate to false.', + 82, + $tipText, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 27c4bbac60a..f4b54740b07 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -320,6 +320,26 @@ public function testInTrait(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15165(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-15165.php'], []); + } + + public function testInvariantGenericAssert(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/invariant-generic-assert.php'], [ + [ + 'Call to method InvariantGenericAssert\Asserter::assertStringBag() with InvariantGenericAssert\Bag will always evaluate to false.', + 88, + $tipText, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 1a479ec9784..924a9492b1d 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -185,6 +185,19 @@ public function testInTrait(): void ]); } + public function testInvariantGenericAssert(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/invariant-generic-assert.php'], [ + [ + 'Call to static method InvariantGenericAssert\Asserter::assertStringBagStatic() with InvariantGenericAssert\Bag will always evaluate to false.', + 94, + $tipText, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index c59730e3b94..723145c2ca4 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1291,4 +1291,15 @@ public function testBug14847(): void ]); } + public function testInvariantGenericComparison(): void + { + $this->analyse([__DIR__ . '/data/invariant-generic-comparison.php'], [ + [ + 'Strict comparison using === between InvariantGenericComparison\Bag and InvariantGenericComparison\Bag will always evaluate to false.', + 43, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-15165.php b/tests/PHPStan/Rules/Comparison/data/bug-15165.php new file mode 100644 index 00000000000..b3d52ba5926 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-15165.php @@ -0,0 +1,43 @@ + $enum + * @return void + */ +function testUnit(ReflectionEnum $enum): void +{ + if ($enum->isBacked()) { + echo "Backed!"; + } +} + +/** + * @template T of BackedEnum + * @param ReflectionEnum $enum + * @return void + */ +function testBacked(ReflectionEnum $enum): void +{ + if (!$enum->isBacked()) { + echo "Unit!"; + } +} + +/** + * @template T of BackedEnum|UnitEnum + * @param ReflectionEnum $enum + * @return void + */ +function testAny(ReflectionEnum $enum): void +{ + if ($enum->isBacked()) { + echo "Backed!"; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/invariant-generic-assert.php b/tests/PHPStan/Rules/Comparison/data/invariant-generic-assert.php new file mode 100644 index 00000000000..0159e903161 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/invariant-generic-assert.php @@ -0,0 +1,95 @@ + $b + * @phpstan-assert Box $b + */ +function assertCat(Box $b): void {} + +/** + * @template T + * @param Bag $b + * @phpstan-assert Bag $b + */ +function assertStringBag(Bag $b): void {} + +final class Asserter +{ + + /** + * @template T of object + * @param Box $b + * @phpstan-assert Box $b + */ + public function assertCat(Box $b): void {} + + /** + * @template T + * @param Bag $b + * @phpstan-assert Bag $b + */ + public function assertStringBag(Bag $b): void {} + + /** + * @template T of object + * @param Box $b + * @phpstan-assert Box $b + */ + public static function assertCatStatic(Box $b): void {} + + /** + * @template T + * @param Bag $b + * @phpstan-assert Bag $b + */ + public static function assertStringBagStatic(Bag $b): void {} + +} + +/** @param Box $b */ +function testFunctionCall(Box $b): void +{ + assertCat($b); +} + +/** @param Box $b */ +function testMethodCall(Box $b, Asserter $a): void +{ + $a->assertCat($b); +} + +/** @param Box $b */ +function testStaticMethodCall(Box $b): void +{ + Asserter::assertCatStatic($b); +} + +/** @param Bag $b */ +function testDisjointFunctionCall(Bag $b): void +{ + assertStringBag($b); +} + +/** @param Bag $b */ +function testDisjointMethodCall(Bag $b, Asserter $a): void +{ + $a->assertStringBag($b); +} + +/** @param Bag $b */ +function testDisjointStaticMethodCall(Bag $b): void +{ + Asserter::assertStringBagStatic($b); +} diff --git a/tests/PHPStan/Rules/Comparison/data/invariant-generic-comparison.php b/tests/PHPStan/Rules/Comparison/data/invariant-generic-comparison.php new file mode 100644 index 00000000000..e95968e5910 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/invariant-generic-comparison.php @@ -0,0 +1,46 @@ + $b + * @param Box $c + */ +function compareTemplate(Box $b, Box $c): void +{ + if ($b === $c) { + echo 'same'; + } +} + +/** + * @param Box $b + * @param Box $c + */ +function compareRelated(Box $b, Box $c): void +{ + if ($b === $c) { + echo 'same'; + } +} + +/** + * @param Bag $b + * @param Bag $c + */ +function compareDisjoint(Bag $b, Bag $c): void +{ + if ($b === $c) { + echo 'same'; + } +} diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 109b85fd3c9..bb3522539a2 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -330,7 +330,7 @@ public function testBug4707(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4707.php'], [ [ - 'Return type (list) of method Bug4707\Block2::getChildren() should be compatible with return type (list>) of method Bug4707\ParentNodeInterface::getChildren()', + 'Return type (list) of method Bug4707\Block2::getChildren() should be covariant with return type (list>) of method Bug4707\ParentNodeInterface::getChildren()', 38, ], ]); diff --git a/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php b/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php index 349175cf336..77db8147154 100644 --- a/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php @@ -82,4 +82,14 @@ public function testRule(): void ]); } + public function testInvariantGenericAssert(): void + { + $this->analyse([__DIR__ . '/data/invariant-generic-assert.php'], [ + [ + 'Asserted type InvariantGenericAssertPhpDoc\Bag for $b with type InvariantGenericAssertPhpDoc\Bag can never happen.', + 31, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php index e8e2abe2bef..4606b5aa057 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php @@ -163,4 +163,14 @@ public function testBugIncompatibleAssertTypeWithMethodReturnType(): void ]); } + public function testInvariantGenericAssert(): void + { + $this->analyse([__DIR__ . '/data/invariant-generic-assert.php'], [ + [ + 'Asserted type InvariantGenericAssertPhpDoc\Bag for $b with type InvariantGenericAssertPhpDoc\Bag can never happen.', + 53, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/invariant-generic-assert.php b/tests/PHPStan/Rules/PhpDoc/data/invariant-generic-assert.php new file mode 100644 index 00000000000..ff935edaf87 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invariant-generic-assert.php @@ -0,0 +1,55 @@ + $b + * @phpstan-assert Box $b + */ +function assertCat(Box $b): void {} + +/** + * @param Box $b + * @phpstan-assert Box $b + */ +function assertCatOnAnimalBox(Box $b): void {} + +/** + * @param Bag $b + * @phpstan-assert Bag $b + */ +function assertStringBagOnIntBag(Bag $b): void {} + +final class Asserter +{ + + /** + * @template T of object + * @param Box $b + * @phpstan-assert Box $b + */ + public function assertCat(Box $b): void {} + + /** + * @param Box $b + * @phpstan-assert Box $b + */ + public function assertCatOnAnimalBox(Box $b): void {} + + /** + * @param Bag $b + * @phpstan-assert Bag $b + */ + public static function assertStringBagOnIntBag(Bag $b): void {} + +} diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index faca30db63a..30136e5fc78 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -55,9 +55,16 @@ public static function dataIsSuperTypeOf(): array new GenericObjectType(A\SubA::class, [new ObjectType('DateTime')]), TrinaryLogic::createYes(), ], + // Invariance is about assignability - it does not make the two types disjoint, + // so isSuperTypeOf() has to stay at maybe. dataAccepts() still says no. 'same class, different type args' => [ new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(A\A::class, [new ObjectType('DateTime')]), + TrinaryLogic::createMaybe(), + ], + 'same class, disjoint type args' => [ + new GenericObjectType(A\A::class, [new ObjectType('DateTime')]), + new GenericObjectType(A\A::class, [new IntegerType()]), TrinaryLogic::createNo(), ], // https://github.com/phpstan/phpstan/issues/11935 - `mixed` as a type @@ -87,7 +94,7 @@ public static function dataIsSuperTypeOf(): array 'implementation with @extends with different type args' => [ new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], 'invariant with equals types' => [ new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')]), @@ -97,11 +104,16 @@ public static function dataIsSuperTypeOf(): array 'invariant with sub type' => [ new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')]), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], 'invariant with super type' => [ new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')]), new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')]), + TrinaryLogic::createMaybe(), + ], + 'invariant with disjoint type' => [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')]), + new GenericObjectType(C\Invariant::class, [new ObjectType(Exception::class)]), TrinaryLogic::createNo(), ], 'covariant with equals types' => [ @@ -155,7 +167,7 @@ public static function dataIsSuperTypeOf(): array new GenericObjectType(ReflectionClass::class, [ new ObjectType(stdClass::class), ]), - PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createYes(), + PHP_VERSION_ID >= 80400 ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(), ], [ new GenericObjectType(ReflectionClass::class, [ @@ -164,7 +176,7 @@ public static function dataIsSuperTypeOf(): array new GenericObjectType(ReflectionClass::class, [ new ObjectWithoutClassType(), ]), - PHP_VERSION_ID >= 80400 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(), + TrinaryLogic::createMaybe(), ], [ new GenericObjectType(ReflectionClass::class, [ @@ -218,6 +230,7 @@ public static function dataIsSuperTypeOf(): array ]; } + /** @return list */ public static function dataTypeProjections(): array { $invariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], variances: [TemplateTypeVariance::createInvariant()]); @@ -235,9 +248,9 @@ public static function dataTypeProjections(): array $bivariant = new GenericObjectType(E\Foo::class, [new MixedType(true)], variances: [TemplateTypeVariance::createBivariant()]); return [ - [$invariantB, $invariantA, TrinaryLogic::createNo()], + [$invariantB, $invariantA, TrinaryLogic::createMaybe(), TrinaryLogic::createNo()], [$invariantB, $invariantB, TrinaryLogic::createYes()], - [$invariantB, $invariantC, TrinaryLogic::createNo()], + [$invariantB, $invariantC, TrinaryLogic::createMaybe(), TrinaryLogic::createNo()], [$invariantB, $covariantA, TrinaryLogic::createNo()], [$invariantB, $covariantB, TrinaryLogic::createNo()], [$invariantB, $covariantC, TrinaryLogic::createNo()], @@ -281,8 +294,26 @@ public static function dataTypeProjections(): array ]; } + /** @return list */ + public static function dataTypeProjectionsIsSuperTypeOf(): array + { + return array_map( + static fn (array $data): array => [$data[0], $data[1], $data[2]], + self::dataTypeProjections(), + ); + } + + /** @return list */ + public static function dataTypeProjectionsAccepts(): array + { + return array_map( + static fn (array $data): array => [$data[0], $data[1], $data[3] ?? $data[2]], + self::dataTypeProjections(), + ); + } + #[DataProvider('dataIsSuperTypeOf')] - #[DataProvider('dataTypeProjections')] + #[DataProvider('dataTypeProjectionsIsSuperTypeOf')] public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { $actualResult = $type->isSuperTypeOf($otherType); @@ -361,7 +392,7 @@ public static function dataAccepts(): array } #[DataProvider('dataAccepts')] - #[DataProvider('dataTypeProjections')] + #[DataProvider('dataTypeProjectionsAccepts')] public function testAccepts( Type $acceptingType, Type $acceptedType,