Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/Type/Generic/GenericObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
92 changes: 92 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12444.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types = 1);

namespace Bug12444;

use function PHPStan\Testing\assertType;

/**
* @template-covariant T
*/
interface Covariant {}

/**
* @template T of object
* @param class-string<T> $class
* @return Covariant<T>
*/
function covariant(string $class): Covariant
{
throw new \Exception();
}

/**
* @template-contravariant T
*/
interface Contravariant {}

/**
* @template T of object
* @param class-string<T> $class
* @return Contravariant<T>
*/
function contravariant(string $class): Contravariant
{
throw new \Exception();
}

/**
* @template T
* @extends Covariant<T>
* @extends Contravariant<T>
*/
interface Invariant extends Covariant, Contravariant {}

/**
* @template T of object
* @param class-string<T> $class
* @return Invariant<T>
*/
function invariant(string $class): Invariant
{
throw new \Exception();
}

/**
* @template T
* @param T $value
* @param Covariant<T> ...$covariants
* @return T
*/
function testCovariant(mixed $value, Covariant ...$covariants): mixed
{
return $value;
}

/**
* @template T
* @param T $value
* @param Contravariant<T> ...$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);
158 changes: 158 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12444b.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

declare(strict_types = 1);

namespace Bug12444b;

use function PHPStan\Testing\assertType;

/**
* @template-contravariant T
*/
interface Contra {}

/**
* @template T
* @extends Contra<T>
*/
interface Inv extends Contra {}

/**
* @template T of object
* @param class-string<T> $class
* @return Contra<T>
*/
function contra(string $class): Contra
{
throw new \Exception();
}

/**
* @template T of object
* @param class-string<T> $class
* @return Inv<T>
*/
function inv(string $class): Inv
{
throw new \Exception();
}

// Non-variadic: two separate contravariant params
/**
* @template T
* @param T $value
* @param Contra<T> $a
* @param Contra<T> $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<Out, In>
*/
interface InvFunc extends Func {}

/**
* @template T
* @param Func<T, T> $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<T> ...$contras
* @return T
*/
public function test(mixed $value, Contra ...$contras): mixed
{
return $value;
}

/**
* @template T
* @param T $value
* @param Contra<T> ...$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);
}
Loading