diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 7ee37d6d41..d3f729a1e5 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -296,9 +296,26 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $otherTypes = $ancestorClassReflection->typeMapToList($ancestorClassReflection->getActiveTemplateTypeMap()); $typeMap = TemplateTypeMap::createEmpty(); + $classReflection = $this->getClassReflection(); + $typeList = []; + if ($classReflection !== null) { + $typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); + } + foreach ($this->getTypes() as $i => $type) { $other = $otherTypes[$i] ?? new ErrorType(); - $typeMap = $typeMap->union($type->inferTemplateTypes($other)); + $map = $type->inferTemplateTypes($other); + + $effectiveVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if ($effectiveVariance->invariant() && isset($typeList[$i]) && $typeList[$i] instanceof TemplateType) { + $effectiveVariance = $typeList[$i]->getVariance(); + } + + if ($effectiveVariance->contravariant()) { + $map = $map->convertToLowerBoundTypes(); + } + + $typeMap = $typeMap->union($map); } return $typeMap; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12444.php b/tests/PHPStan/Analyser/nsrt/bug-12444.php new file mode 100644 index 0000000000..5f447e8d8e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12444.php @@ -0,0 +1,92 @@ + $class + * @return Covariant + */ +function covariant(string $class): Covariant +{ + throw new \Exception(); +} + +/** + * @template-contravariant T + */ +interface Contravariant {} + +/** + * @template T of object + * @param class-string $class + * @return Contravariant + */ +function contravariant(string $class): Contravariant +{ + throw new \Exception(); +} + +/** + * @template T + * @extends Covariant + * @extends Contravariant + */ +interface Invariant extends Covariant, Contravariant {} + +/** + * @template T of object + * @param class-string $class + * @return Invariant + */ +function invariant(string $class): Invariant +{ + throw new \Exception(); +} + +/** + * @template T + * @param T $value + * @param Covariant ...$covariants + * @return T + */ +function testCovariant(mixed $value, Covariant ...$covariants): mixed +{ + return $value; +} + +/** + * @template T + * @param T $value + * @param Contravariant ...$contravariants + * @return T + */ +function testContravariant(mixed $value, Contravariant ...$contravariants): mixed +{ + return $value; +} + +// Contravariant with direct Contravariant args +$r3 = testContravariant( + new \RuntimeException(), + contravariant(\Throwable::class), + contravariant(\Exception::class), +); +assertType('RuntimeException', $r3); + +// Contravariant with Invariant args (extending Contravariant) - this is the reported bug +$r4 = testContravariant( + new \RuntimeException(), + invariant(\Throwable::class), + invariant(\Exception::class), +); +assertType('RuntimeException', $r4); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12444b.php b/tests/PHPStan/Analyser/nsrt/bug-12444b.php new file mode 100644 index 0000000000..fef26bf5d1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12444b.php @@ -0,0 +1,158 @@ + + */ +interface Inv extends Contra {} + +/** + * @template T of object + * @param class-string $class + * @return Contra + */ +function contra(string $class): Contra +{ + throw new \Exception(); +} + +/** + * @template T of object + * @param class-string $class + * @return Inv + */ +function inv(string $class): Inv +{ + throw new \Exception(); +} + +// Non-variadic: two separate contravariant params +/** + * @template T + * @param T $value + * @param Contra $a + * @param Contra $b + * @return T + */ +function testTwoParams(mixed $value, Contra $a, Contra $b): mixed +{ + return $value; +} + +// Non-variadic with direct Contra +$r1 = testTwoParams( + new \RuntimeException(), + contra(\Throwable::class), + contra(\Exception::class), +); +assertType('RuntimeException', $r1); + +// Non-variadic with Inv (extending Contra) +$r2 = testTwoParams( + new \RuntimeException(), + inv(\Throwable::class), + inv(\Exception::class), +); +assertType('RuntimeException', $r2); + +// Mixed variance: function with both covariant and contravariant template params +/** + * @template-covariant Out + * @template-contravariant In + */ +interface Func +{ +} + +/** + * @template Out + * @template In + * @extends Func + */ +interface InvFunc extends Func {} + +/** + * @template T + * @param Func $fn + * @param T $value + * @return T + */ +function applyFunc(Func $fn, mixed $value): mixed +{ + return $value; +} + +/** + * @param Func<\Exception, \Throwable> $fn + */ +function testMixedVariance(Func $fn): void +{ + $r = applyFunc($fn, new \RuntimeException()); + assertType('Exception', $r); +} + +/** + * @param InvFunc<\Exception, \Throwable> $fn + */ +function testMixedVarianceWithInv(InvFunc $fn): void +{ + $r = applyFunc($fn, new \RuntimeException()); + assertType('Exception', $r); +} + +// Method on a class (vs function) +class Container +{ + /** + * @template T + * @param T $value + * @param Contra ...$contras + * @return T + */ + public function test(mixed $value, Contra ...$contras): mixed + { + return $value; + } + + /** + * @template T + * @param T $value + * @param Contra ...$contras + * @return T + */ + public static function testStatic(mixed $value, Contra ...$contras): mixed + { + return $value; + } +} + +function testMethod(): void +{ + $c = new Container(); + // Method with Inv args + $r = $c->test( + new \RuntimeException(), + inv(\Throwable::class), + inv(\Exception::class), + ); + assertType('RuntimeException', $r); + + // Static method with Inv args + $r2 = Container::testStatic( + new \RuntimeException(), + inv(\Throwable::class), + inv(\Exception::class), + ); + assertType('RuntimeException', $r2); +}