diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index f0e478df7b9..55dfa3f4d43 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -233,12 +233,13 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bo return new IsSuperTypeOfResult(TrinaryLogic::createFromBoolean($result), $reasons); } - if ($this->covariant()) { - return $a->isSuperTypeOf($b); - } + if ($this->covariant() || $this->contravariant()) { + // The "exactly this class" flavour of a `new Foo()` value cannot be written + // in a PHPDoc type argument - ignore it like equals() does for invariance. + $a = TemplateTypeHelper::removeFinalByKeywordOverrides($a); + $b = TemplateTypeHelper::removeFinalByKeywordOverrides($b); - if ($this->contravariant()) { - return $b->isSuperTypeOf($a); + return $this->covariant() ? $a->isSuperTypeOf($b) : $b->isSuperTypeOf($a); } if ($this->bivariant()) { diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index a4ae024b9df..510efc6e821 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -468,6 +468,12 @@ public function testBug15442(): void $this->analyse([__DIR__ . '/data/bug-15442.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->analyse([__DIR__ . '/../Methods/data/bug-15235.php'], []); + } + #[RequiresPhp('>= 8.0.0')] public function testBug11935WithCheckExplicitMixed(): void { diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 62761326049..62658809769 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4537,6 +4537,15 @@ public function testBug15166(): void $this->analyse([__DIR__ . '/data/bug-15166.php'], []); } + #[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 testBug6732(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index f499119a6f0..6da5e402076 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1158,4 +1158,11 @@ public function testBug11421(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-15235.php'], []); + } + } 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..3d024e8c136 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-15235.php @@ -0,0 +1,182 @@ += 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 static function staticMethod(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)); + $b::staticMethod(new A($b)); + + $callable = $b->method(...); + $callable(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 885902a72b7..774ab8e5f26 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1101,6 +1101,12 @@ public function testBug15166(): void $this->analyse([__DIR__ . '/data/bug-15166.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->analyse([__DIR__ . '/data/bug-15235.php'], []); + } + public function testBug6732(): void { $this->analyse([__DIR__ . '/data/bug-6732.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); +} diff --git a/turbo-ext/src/TemplateTypeHelper.cpp b/turbo-ext/src/TemplateTypeHelper.cpp index 94f40d42b31..2cf5e45c1a1 100644 --- a/turbo-ext/src/TemplateTypeHelper.cpp +++ b/turbo-ext/src/TemplateTypeHelper.cpp @@ -558,6 +558,11 @@ zv::Val pt_type_template_type_helper_to_argument(zval *type) return TemplateTypeHelper::toArgument(type); } +zv::Val pt_type_template_type_helper_remove_final_by_keyword_overrides(zval *type) +{ + return TemplateTypeHelper::removeFinalByKeywordOverrides(type); +} + /* }}} */ /* {{{ engine ABI glue: parameter parsing + registration */ diff --git a/turbo-ext/src/TemplateTypeVariance.cpp b/turbo-ext/src/TemplateTypeVariance.cpp index 3029efe8c59..73436893d96 100644 --- a/turbo-ext/src/TemplateTypeVariance.cpp +++ b/turbo-ext/src/TemplateTypeVariance.cpp @@ -436,9 +436,17 @@ zv::Val TemplateTypeVariance::isValidVariance(zval *templateType, zval *a, zval return pt_type_new_ce(pt_ce_is_super_type_of_result, 2, args); } - if (thisValue == COVARIANT) return pt_type_op(Z_OBJ_P(a), PT_OP_IS_SUPER_TYPE_OF, 1, b); - - if (thisValue == CONTRAVARIANT) return pt_type_op(Z_OBJ_P(b), PT_OP_IS_SUPER_TYPE_OF, 1, a); + if (thisValue == COVARIANT || thisValue == CONTRAVARIANT) { + /* the "exactly this class" flavour of a `new Foo()` value cannot be + * written in a PHPDoc type argument - ignore it like equals() does + * for invariance */ + zv::Val strippedA = pt_type_template_type_helper_remove_final_by_keyword_overrides(a); + if (UNEXPECTED(strippedA.isUndef())) return zv::Val(); + zv::Val strippedB = pt_type_template_type_helper_remove_final_by_keyword_overrides(b); + if (UNEXPECTED(strippedB.isUndef())) return zv::Val(); + if (thisValue == COVARIANT) return pt_type_op(Z_OBJ_P(strippedA.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, strippedB.raw()); + return pt_type_op(Z_OBJ_P(strippedB.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, strippedA.raw()); + } if (thisValue == BIVARIANT) return pt_type_is_super_type_of_result(PT_TRI_YES); diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index 183c84abc1e..faf10c006ce 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -946,12 +946,13 @@ zv::Val pt_type_template_type_variance_map_new(zval *variances); zv::Val pt_type_template_type_map_empty(); zv::Val pt_type_template_type_map_new(zval *types, zval *lowerBoundTypes = NULL); /* TemplateTypeHelper::resolveTemplateTypes() / resolveToDefaults() / - * resolveToBounds() / toArgument() (the arguments borrowed); UNDEF = - * pending exception */ + * resolveToBounds() / toArgument() / removeFinalByKeywordOverrides() (the + * arguments borrowed); UNDEF = pending exception */ zv::Val pt_type_template_type_helper_resolve_template_types(zval *type, zval *standins, zval *callSiteVariances, zval *positionVariance, bool keepErrorTypes); zv::Val pt_type_template_type_helper_resolve_to_defaults(zval *type); zv::Val pt_type_template_type_helper_resolve_to_bounds(zval *type); zv::Val pt_type_template_type_helper_to_argument(zval *type); +zv::Val pt_type_template_type_helper_remove_final_by_keyword_overrides(zval *type); /* $reference->getType() / ->getPositionVariance() of a reference instance * (TemplateTypeReference.cpp): the slots of a native instance, the methods * of anything else (the PHP twin declared next to the native class in the