diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index 4a44b32865b..d93273f3a8e 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -232,11 +232,11 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bo } if ($this->covariant()) { - return $a->isSuperTypeOf($b); + return self::compareTypeArguments($a, $b); } if ($this->contravariant()) { - return $b->isSuperTypeOf($a); + return self::compareTypeArguments($b, $a); } if ($this->bivariant()) { @@ -246,6 +246,19 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bo throw new ShouldNotHappenException(); } + /** + * The "exactly this class" flavour of a `new Foo()` value cannot be written in a + * PHPDoc type argument, and `Foo` with the flavour is only a maybe-supertype of a + * plain `Foo`. Comparing the two directly would make a type argument carrying the + * flavour unmatchable against the very same written type - the invariant branch + * above does not have the problem because equals() ignores the flavour. + */ + private static function compareTypeArguments(Type $super, Type $sub): IsSuperTypeOfResult + { + return TemplateTypeHelper::removeFinalByKeywordOverrides($super) + ->isSuperTypeOf(TemplateTypeHelper::removeFinalByKeywordOverrides($sub)); + } + public function equals(self $other): bool { return $other->value === $this->value; diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 84849931a8a..81c5aaa2f3f 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4452,6 +4452,14 @@ public function testBug8441(): void } #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-15235.php'], []); + } + public function testBug15166(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-15235.php b/tests/PHPStan/Rules/Methods/data/bug-15235.php new file mode 100644 index 00000000000..02fa51c6891 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-15235.php @@ -0,0 +1,173 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug15235; + +/** + * @template T of object + */ +class A +{ + /** + * @param T $object + */ + public function __construct(public object $object) {} +} + +class B +{ + /** + * @param A $a + */ + public function method(A $a): void {} + + /** + * @param A $a + */ + public function invariant(A $a): void {} + + /** + * @param A $a + */ + public function contravariant(A $a): void {} +} + +class D +{ + public function inconsistent(B $b): void + { + $b->method(new A($b)); + } +} + +function fromNew(): void +{ + $b = new B(); + $b->method(new A($b)); + $b->invariant(new A($b)); + $b->contravariant(new A($b)); +} + +function fromGetClass(B $b): void +{ + if (get_class($b) !== B::class) { + return; + } + + $b->method(new A($b)); +} + +/** @template-covariant T of object */ +class Covariant +{ + /** @param T $object */ + public function __construct(public object $object) {} +} + +/** @template-contravariant T of object */ +class Contravariant +{ + /** @param T $object */ + public function set(object $object): void {} +} + +class E +{ + /** @param Covariant $c */ + public function declaredCovariant(Covariant $c): void {} + + /** @param Contravariant $c */ + public function declaredContravariant(Contravariant $c): void {} + + /** @param \Traversable $it */ + public function traversable(\Traversable $it): void {} + + /** @return Covariant */ + public function makeCovariant(): Covariant + { + return new Covariant($this); + } + + /** @return Wrapper */ + public function makeWrapper(): Wrapper + { + return new Wrapper($this); + } +} + +class Consumer +{ + /** @param Covariant $c */ + public function takesCovariant(Covariant $c): void {} + + /** @param Wrapper $wrapper */ + public function takesContravariantWrapper(Wrapper $wrapper): void {} + + /** @param Wrapper> $wrapper */ + public function takesNested(Wrapper $wrapper): void {} +} + +/** + * @param Covariant $covariant + * @param Contravariant $contravariant + * @param \Traversable $traversable + */ +function declaredVariances(Covariant $covariant, Contravariant $contravariant, \Traversable $traversable): void +{ + $e = new E(); + $e->declaredCovariant($covariant); + $e->declaredContravariant($contravariant); + $e->traversable($traversable); +} + +function flavourOnTheArgumentSide(Consumer $consumer): void +{ + $e = new E(); + $consumer->takesCovariant($e->makeCovariant()); + $consumer->takesContravariantWrapper($e->makeWrapper()); +} + +/** @template T of object */ +class Wrapper +{ + /** @param T $object */ + public function __construct(public object $object) {} +} + +function nestedInGenericType(Consumer $consumer): void +{ + $e = new E(); + $consumer->takesNested(new Wrapper($e->makeCovariant())); +} + +class Composite +{ + /** @param Covariant|null $c */ + public function inUnion(?Covariant $c): void {} + + /** @param array> $c */ + public function inArray(array $c): void {} + + /** @param Covariant> $c */ + public function nested(Covariant $c): void {} + + /** @param Wrapper> $c */ + public function nestedInvariant(Wrapper $c): void {} +} + +/** + * @param Covariant $covariant + * @param array> $array + * @param Covariant> $nested + * @param Wrapper> $nestedInvariant + */ +function compositeShapes(Covariant $covariant, array $array, Covariant $nested, Wrapper $nestedInvariant): void +{ + $composite = new Composite(); + $composite->inUnion($covariant); + $composite->inUnion(null); + $composite->inArray($array); + $composite->nested($nested); + $composite->nestedInvariant($nestedInvariant); +} diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 8a874eb39a6..b43e510c5df 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1096,6 +1096,11 @@ public function testBug8441(): void } #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->analyse([__DIR__ . '/data/bug-15235.php'], []); + } + public function testBug15166(): void { $this->analyse([__DIR__ . '/data/bug-15166.php'], []); diff --git a/tests/PHPStan/Rules/Properties/data/bug-15235.php b/tests/PHPStan/Rules/Properties/data/bug-15235.php new file mode 100644 index 00000000000..612d23cb87f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-15235.php @@ -0,0 +1,45 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug15235Properties; + +/** + * @template T of object + */ +class A +{ + /** + * @param T $object + */ + public function __construct(public object $object) {} +} + +/** @template-covariant T of object */ +class Covariant +{ + /** @param T $object */ + public function __construct(public object $object) {} +} + +class B +{ + /** @var A|null */ + public ?A $callSiteVariance = null; + + /** @var Covariant|null */ + public ?Covariant $declaredVariance = null; +} + +function fromParameter(B $b): void +{ + $b->callSiteVariance = new A($b); + $b->declaredVariance = new Covariant($b); +} + +function fromNew(): void +{ + $b = new B(); + $b->callSiteVariance = new A($b); + $b->declaredVariance = new Covariant($b); +}