diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index ad78a004ad..e20ed2038b 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -65,6 +65,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Helper\GetTemplateTypeType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; @@ -602,6 +603,21 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na { $mainTypeName = strtolower($typeNode->type->name); $genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope); + $variances = array_map( + static function (string $variance): TemplateTypeVariance { + switch ($variance) { + case GenericTypeNode::VARIANCE_INVARIANT: + return TemplateTypeVariance::createInvariant(); + case GenericTypeNode::VARIANCE_COVARIANT: + return TemplateTypeVariance::createCovariant(); + case GenericTypeNode::VARIANCE_CONTRAVARIANT: + return TemplateTypeVariance::createContravariant(); + case GenericTypeNode::VARIANCE_BIVARIANT: + return TemplateTypeVariance::createBivariant(); + } + }, + $typeNode->variances, + ); if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') { if (count($genericTypes) === 1) { // array @@ -748,7 +764,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if ($mainTypeClassName !== null) { if (!$this->getReflectionProvider()->hasClass($mainTypeClassName)) { - return new GenericObjectType($mainTypeClassName, $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } $classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName); @@ -762,6 +778,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new GenericObjectType($mainTypeClassName, [ new MixedType(true), $genericTypes[0], + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], ]); } @@ -769,6 +788,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], + ], null, null, [ + $variances[0], + $variances[1], ]); } } @@ -780,6 +802,11 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na $genericTypes[0], $mixed, $mixed, + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } @@ -790,19 +817,24 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na $genericTypes[1], $mixed, $mixed, + ], null, null, [ + $variances[0], + $variances[1], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } } if (!$mainType->isIterable()->yes()) { - return new GenericObjectType($mainTypeClassName, $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } if ( count($genericTypes) !== 1 || $classReflection->getTemplateTypeMap()->count() === 1 ) { - return new GenericObjectType($mainTypeClassName, $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } } } @@ -824,7 +856,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } if ($mainTypeClassName !== null) { - return new GenericObjectType($mainTypeClassName, $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } return new ErrorType(); diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index c1b220b5d5..3383fb0e9d 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -36,6 +36,9 @@ use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\TypeAlias; use PHPStan\Type\TypehintHelper; @@ -94,6 +97,10 @@ class ClassReflection private ?TemplateTypeMap $activeTemplateTypeMap = null; + private ?TemplateTypeVarianceMap $defaultCallSiteVarianceMap = null; + + private ?TemplateTypeVarianceMap $callSiteVarianceMap = null; + /** @var array|null */ private ?array $ancestors = null; @@ -140,6 +147,7 @@ public function __construct( private ?TemplateTypeMap $resolvedTemplateTypeMap, private ?ResolvedPhpDocBlock $stubPhpDocBlock, private ?string $extraCacheKey = null, + private ?TemplateTypeVarianceMap $resolvedCallSiteVarianceMap = null, ) { } @@ -241,7 +249,11 @@ public function getDisplayName(bool $withTemplateTypes = true): string return $name; } - return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>'; + return $name . '<' . implode(',', array_map( + static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::typeOnly()), + $this->getActiveTemplateTypeMap()->getTypes(), + $this->getCallSiteVarianceMap()->getVariances(), + )) . '>'; } public function getCacheKey(): string @@ -254,7 +266,11 @@ public function getCacheKey(): string $cacheKey = $this->displayName; if ($this->resolvedTemplateTypeMap !== null) { - $cacheKey .= '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::cache()), $this->resolvedTemplateTypeMap->getTypes())) . '>'; + $cacheKey .= '<' . implode(',', array_map( + static fn (Type $type, TemplateTypeVariance $variance): string => TypeProjectionHelper::describe($type, $variance, VerbosityLevel::cache()), + $this->getActiveTemplateTypeMap()->getTypes(), + $this->getCallSiteVarianceMap()->getVariances(), + )) . '>'; } if ($this->extraCacheKey !== null) { @@ -1239,6 +1255,32 @@ public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap(); } + private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap + { + if ($this->defaultCallSiteVarianceMap !== null) { + return $this->defaultCallSiteVarianceMap; + } + + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + $this->defaultCallSiteVarianceMap = TemplateTypeVarianceMap::createEmpty(); + return $this->defaultCallSiteVarianceMap; + } + + $map = []; + foreach ($this->getTemplateTags() as $templateTag) { + $map[$templateTag->getName()] = TemplateTypeVariance::createInvariant(); + } + + $this->defaultCallSiteVarianceMap = new TemplateTypeVarianceMap($map); + return $this->defaultCallSiteVarianceMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap ??= $this->resolvedCallSiteVarianceMap ?? $this->getDefaultCallSiteVarianceMap(); + } + public function isGeneric(): bool { if ($this->isGeneric === null) { @@ -1272,6 +1314,26 @@ public function typeMapFromList(array $types): TemplateTypeMap return new TemplateTypeMap($map); } + /** + * @param array $variances + */ + public function varianceMapFromList(array $variances): TemplateTypeVarianceMap + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return new TemplateTypeVarianceMap([]); + } + + $map = []; + $i = 0; + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $map[$tag->getName()] = $variances[$i] ?? TemplateTypeVariance::createInvariant(); + $i++; + } + + return new TemplateTypeVarianceMap($map); + } + /** @return array */ public function typeMapToList(TemplateTypeMap $typeMap): array { @@ -1288,6 +1350,22 @@ public function typeMapToList(TemplateTypeMap $typeMap): array return $list; } + /** @return array */ + public function varianceMapToList(TemplateTypeVarianceMap $varianceMap): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + $list = []; + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $list[] = $varianceMap->getVariance($tag->getName()) ?? TemplateTypeVariance::createInvariant(); + } + + return $list; + } + /** * @param array $types */ @@ -1308,6 +1386,33 @@ public function withTypes(array $types): self $this->anonymousFilename, $this->typeMapFromList($types), $this->stubPhpDocBlock, + null, + $this->resolvedCallSiteVarianceMap, + ); + } + + /** + * @param array $variances + */ + public function withVariances(array $variances): self + { + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, + $this->phpVersion, + $this->propertiesClassReflectionExtensions, + $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->displayName, + $this->reflection, + $this->anonymousFilename, + $this->resolvedTemplateTypeMap, + $this->stubPhpDocBlock, + null, + $this->varianceMapFromList($variances), ); } diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index aa9da9c690..775134aab6 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -35,12 +35,14 @@ class GenericObjectType extends ObjectType /** * @api * @param array $types + * @param array $variances */ public function __construct( string $mainType, private array $types, ?Type $subtractedType = null, private ?ClassReflection $classReflection = null, + private array $variances = [], ) { parent::__construct($mainType, $subtractedType, $classReflection); @@ -51,7 +53,11 @@ public function describe(VerbosityLevel $level): string return sprintf( '%s<%s>', parent::describe($level), - implode(', ', array_map(static fn (Type $type): string => $type->describe($level), $this->types)), + implode(', ', array_map( + static fn (Type $type, ?TemplateTypeVariance $variance = null): string => TypeProjectionHelper::describe($type, $variance, $level), + $this->types, + $this->variances, + )), ); } @@ -74,6 +80,12 @@ public function equals(Type $type): bool if (!$genericType->equals($otherGenericType)) { return false; } + + $variance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + $otherVariance = $type->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if (!$variance->equals($otherVariance)) { + return false; + } } return true; @@ -100,6 +112,12 @@ public function getTypes(): array return $this->types; } + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { return $this->acceptsWithReason($type, $strictTypes)->result; @@ -171,7 +189,15 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Accept throw new ShouldNotHappenException(); } - $results[] = $templateType->isValidVarianceWithReason($this->types[$i], $ancestor->types[$i]); + $thisVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if (!$thisVariance->invariant()) { + $results[] = $thisVariance->isValidVarianceWithReason($templateType, $this->types[$i], $ancestor->types[$i]); + } else { + $results[] = $templateType->isValidVarianceWithReason($this->types[$i], $ancestor->types[$i]); + } + + $results[] = AcceptsResult::createFromBoolean($thisVariance->validPosition($ancestorVariance)); } if (count($results) === 0) { @@ -197,7 +223,9 @@ public function getClassReflection(): ?ClassReflection return null; } - return $this->classReflection = $reflectionProvider->getClass($this->getClassName())->withTypes($this->types); + return $this->classReflection = $reflectionProvider->getClass($this->getClassName()) + ->withTypes($this->types) + ->withVariances($this->variances); } public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection @@ -267,11 +295,12 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc $references = []; foreach ($this->types as $i => $type) { - $variance = $positionVariance->compose( - isset($typeList[$i]) && $typeList[$i] instanceof TemplateType - ? $typeList[$i]->getVariance() - : TemplateTypeVariance::createInvariant(), - ); + $effectiveVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if ($effectiveVariance->invariant() && isset($typeList[$i]) && $typeList[$i] instanceof TemplateType) { + $effectiveVariance = $typeList[$i]->getVariance(); + } + + $variance = $positionVariance->compose($effectiveVariance); foreach ($type->getReferencedTemplateTypes($variance) as $reference) { $references[] = $reference; } @@ -297,7 +326,7 @@ public function traverse(callable $cb): Type } if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { - return $this->recreate($this->getClassName(), $types, $subtractedType); + return $this->recreate($this->getClassName(), $types, $subtractedType, $this->variances); } return $this; @@ -340,19 +369,22 @@ public function traverseSimultaneously(Type $right, callable $cb): Type /** * @param Type[] $types + * @param TemplateTypeVariance[] $variances */ - protected function recreate(string $className, array $types, ?Type $subtractedType): self + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): self { return new self( $className, $types, $subtractedType, + null, + $variances, ); } public function changeSubtractedType(?Type $subtractedType): Type { - return new self($this->getClassName(), $this->types, $subtractedType); + return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances); } public function toPhpDocNode(): TypeNode @@ -362,6 +394,7 @@ public function toPhpDocNode(): TypeNode return new GenericTypeNode( $parent, array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), ); } @@ -374,6 +407,8 @@ public static function __set_state(array $properties): Type $properties['className'], $properties['types'], $properties['subtractedType'] ?? null, + null, + $properties['variances'] ?? [], ); } diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index be8861ab06..485781aaca 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -21,7 +21,7 @@ public function __construct( GenericObjectType $bound, ) { - parent::__construct($bound->getClassName(), $bound->getTypes()); + parent::__construct($bound->getClassName(), $bound->getTypes(), null, null, $bound->getVariances()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; @@ -30,7 +30,7 @@ public function __construct( $this->bound = $bound; } - protected function recreate(string $className, array $types, ?Type $subtractedType): GenericObjectType + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType { return new self( $this->scope, diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index d05c1a8a4a..a3ab7a04bd 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; @@ -19,6 +20,7 @@ class TemplateTypeVariance private const COVARIANT = 2; private const CONTRAVARIANT = 3; private const STATIC = 4; + private const BIVARIANT = 5; /** @var self[] */ private static array $registry; @@ -55,6 +57,11 @@ public static function createStatic(): self return self::create(self::STATIC); } + public static function createBivariant(): self + { + return self::create(self::BIVARIANT); + } + public function invariant(): bool { return $this->value === self::INVARIANT; @@ -75,6 +82,11 @@ public function static(): bool return $this->value === self::STATIC; } + public function bivariant(): bool + { + return $this->value === self::BIVARIANT; + } + public function compose(self $other): self { if ($this->contravariant()) { @@ -84,6 +96,9 @@ public function compose(self $other): self if ($other->covariant()) { return self::createContravariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } @@ -94,6 +109,9 @@ public function compose(self $other): self if ($other->covariant()) { return self::createCovariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } @@ -101,6 +119,10 @@ public function compose(self $other): self return self::createInvariant(); } + if ($this->bivariant()) { + return self::createBivariant(); + } + return $other; } @@ -163,6 +185,10 @@ public function isValidVarianceWithReason(?TemplateType $templateType, Type $a, return new AcceptsResult($b->isSuperTypeOf($a), []); } + if ($this->bivariant()) { + return AcceptsResult::createYes(); + } + throw new ShouldNotHappenException(); } @@ -175,6 +201,7 @@ public function validPosition(self $other): bool { return $other->value === $this->value || $other->invariant() + || $this->bivariant() || $this->static(); } @@ -189,6 +216,27 @@ public function describe(): string return 'contravariant'; case self::STATIC: return 'static'; + case self::BIVARIANT: + return 'bivariant'; + } + + throw new ShouldNotHappenException(); + } + + /** + * @return GenericTypeNode::VARIANCE_* + */ + public function toPhpDocNodeVariance(): string + { + switch ($this->value) { + case self::INVARIANT: + return GenericTypeNode::VARIANCE_INVARIANT; + case self::COVARIANT: + return GenericTypeNode::VARIANCE_COVARIANT; + case self::CONTRAVARIANT: + return GenericTypeNode::VARIANCE_CONTRAVARIANT; + case self::BIVARIANT: + return GenericTypeNode::VARIANCE_BIVARIANT; } throw new ShouldNotHappenException(); diff --git a/src/Type/Generic/TemplateTypeVarianceMap.php b/src/Type/Generic/TemplateTypeVarianceMap.php new file mode 100644 index 0000000000..55d3a18aa3 --- /dev/null +++ b/src/Type/Generic/TemplateTypeVarianceMap.php @@ -0,0 +1,51 @@ + $variances + */ + public function __construct(private array $variances) + { + } + + public static function createEmpty(): self + { + $empty = self::$empty; + + if ($empty !== null) { + return $empty; + } + + $empty = new self([]); + self::$empty = $empty; + + return $empty; + } + + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + + public function hasVariance(string $name): bool + { + return array_key_exists($name, $this->getVariances()); + } + + public function getVariance(string $name): ?TemplateTypeVariance + { + return $this->getVariances()[$name] ?? null; + } + +} diff --git a/src/Type/Generic/TypeProjectionHelper.php b/src/Type/Generic/TypeProjectionHelper.php new file mode 100644 index 0000000000..217103bd09 --- /dev/null +++ b/src/Type/Generic/TypeProjectionHelper.php @@ -0,0 +1,31 @@ +describe($level); + + if ($variance === null || $variance->invariant()) { + return $describedType; + } + + if ($variance->bivariant()) { + return '*'; + } + + return sprintf('%s %s', $variance->describe(), $describedType); + } + +} diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index ea72bad5f5..7f18546d3e 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -124,6 +124,9 @@ private static function createFromReflection(ClassReflection $reflection): self return new GenericObjectType( $reflection->getName(), $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()), + null, + null, + $reflection->varianceMapToList($reflection->getCallSiteVarianceMap()), ); } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 3cc95197a3..80d324e3e4 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -85,10 +85,13 @@ public function getStaticObjectType(): ObjectType if ($this->staticObjectType === null) { if ($this->classReflection->isGeneric()) { $typeMap = $this->classReflection->getActiveTemplateTypeMap()->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::toArgument($type)); + $varianceMap = $this->classReflection->getCallSiteVarianceMap(); return $this->staticObjectType = new GenericObjectType( $this->classReflection->getName(), $this->classReflection->typeMapToList($typeMap), $this->subtractedType, + null, + $this->classReflection->varianceMapToList($varianceMap), ); } diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index 75c9c0d7fe..70207aad08 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -17,6 +17,7 @@ use PHPStan\Type\Test\B; use PHPStan\Type\Test\C; use PHPStan\Type\Test\D; +use PHPStan\Type\Test\E; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -153,11 +154,115 @@ public function dataIsSuperTypeOf(): array ]), TrinaryLogic::createNo(), ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], + ]; + } + + public function dataTypeProjections(): array + { + $invariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createInvariant()]); + $invariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createInvariant()]); + $invariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createInvariant()]); + + $covariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createCovariant()]); + $covariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createCovariant()]); + $covariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createCovariant()]); + + $contravariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createContravariant()]); + $contravariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createContravariant()]); + $contravariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createContravariant()]); + + $bivariant = new GenericObjectType(E\Foo::class, [new MixedType(true)], null, null, [TemplateTypeVariance::createBivariant()]); + + return [ + [$invariantB, $invariantA, TrinaryLogic::createNo()], + [$invariantB, $invariantB, TrinaryLogic::createYes()], + [$invariantB, $invariantC, TrinaryLogic::createNo()], + [$invariantB, $covariantA, TrinaryLogic::createNo()], + [$invariantB, $covariantB, TrinaryLogic::createNo()], + [$invariantB, $covariantC, TrinaryLogic::createNo()], + [$invariantB, $contravariantA, TrinaryLogic::createNo()], + [$invariantB, $contravariantB, TrinaryLogic::createNo()], + [$invariantB, $contravariantC, TrinaryLogic::createNo()], + [$invariantB, $bivariant, TrinaryLogic::createNo()], + + [$covariantB, $invariantA, TrinaryLogic::createMaybe()], + [$covariantB, $invariantB, TrinaryLogic::createYes()], + [$covariantB, $invariantC, TrinaryLogic::createYes()], + [$covariantB, $covariantA, TrinaryLogic::createMaybe()], + [$covariantB, $covariantB, TrinaryLogic::createYes()], + [$covariantB, $covariantC, TrinaryLogic::createYes()], + [$covariantB, $contravariantA, TrinaryLogic::createNo()], + [$covariantB, $contravariantB, TrinaryLogic::createNo()], + [$covariantB, $contravariantC, TrinaryLogic::createNo()], + [$covariantB, $bivariant, TrinaryLogic::createNo()], + + [$contravariantB, $invariantA, TrinaryLogic::createYes()], + [$contravariantB, $invariantB, TrinaryLogic::createYes()], + [$contravariantB, $invariantC, TrinaryLogic::createMaybe()], + [$contravariantB, $covariantA, TrinaryLogic::createNo()], + [$contravariantB, $covariantB, TrinaryLogic::createNo()], + [$contravariantB, $covariantC, TrinaryLogic::createNo()], + [$contravariantB, $contravariantA, TrinaryLogic::createYes()], + [$contravariantB, $contravariantB, TrinaryLogic::createYes()], + [$contravariantB, $contravariantC, TrinaryLogic::createMaybe()], + [$contravariantB, $bivariant, TrinaryLogic::createNo()], + + [$bivariant, $invariantA, TrinaryLogic::createYes()], + [$bivariant, $invariantB, TrinaryLogic::createYes()], + [$bivariant, $invariantC, TrinaryLogic::createYes()], + [$bivariant, $covariantA, TrinaryLogic::createYes()], + [$bivariant, $covariantB, TrinaryLogic::createYes()], + [$bivariant, $covariantC, TrinaryLogic::createYes()], + [$bivariant, $contravariantA, TrinaryLogic::createYes()], + [$bivariant, $contravariantB, TrinaryLogic::createYes()], + [$bivariant, $contravariantC, TrinaryLogic::createYes()], + [$bivariant, $bivariant, TrinaryLogic::createYes()], ]; } /** * @dataProvider dataIsSuperTypeOf + * @dataProvider dataTypeProjections */ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -227,6 +332,7 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts + * @dataProvider dataTypeProjections */ public function testAccepts( Type $acceptingType, @@ -379,6 +485,36 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'param: Invariant' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Invariant' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], 'param: Out' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Out::class, [ @@ -606,6 +742,36 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'return: Invariant' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Invariant' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], 'return: Out' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Out::class, [ @@ -914,6 +1080,36 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'param: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], 'return: Out> (with invariance composition)' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Out::class, [ @@ -1008,6 +1204,36 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'return: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], ]; } diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-e.php b/tests/PHPStan/Type/Generic/data/generic-classes-e.php new file mode 100644 index 0000000000..ed60625429 --- /dev/null +++ b/tests/PHPStan/Type/Generic/data/generic-classes-e.php @@ -0,0 +1,10 @@ +', ]; + yield [ + new GenericObjectType('stdClass', [ + new StringType(), + new IntegerType(), + new MixedType(), + ], null, null, [ + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createContravariant(), + TemplateTypeVariance::createBivariant(), + ]), + 'stdClass', + ]; + yield [ new IterableType(new MixedType(), new MixedType()), 'iterable',