From ee7e0fd78229c2ff334ba69a5a42c6903fa9d8df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:22:09 +0000 Subject: [PATCH] Fix phpstan/phpstan#10290: Accept subtypes in invariant generic template positions in accepts() context - Modified GenericObjectType::isSuperTypeOfInternal() to accept subtypes for invariant template parameters when in the accepts context (used for error reporting) - When the declared template variance is invariant and the type argument is a strict subtype, accepts() now returns Yes instead of No - isSuperTypeOf() behavior remains unchanged (strict invariant equality) - New regression test in tests/PHPStan/Rules/Functions/data/bug-10290.php - Also fixes phpstan/phpstan#4590 (OkResponse accepted for OkResponse>) - Updated existing tests to reflect the more lenient accepts behavior --- src/Type/Generic/GenericObjectType.php | 2 + .../Rules/Functions/ReturnTypeRuleTest.php | 13 ++++ .../Rules/Functions/data/bug-10290.php | 60 +++++++++++++++++++ .../Rules/Methods/CallMethodsRuleTest.php | 46 +------------- .../Rules/Methods/ReturnTypeRuleTest.php | 23 +------ .../Type/Generic/GenericObjectTypeTest.php | 16 ++++- tests/PHPStan/Type/StaticTypeTest.php | 2 +- 7 files changed, 92 insertions(+), 70 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-10290.php diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 10b516355d0..58493125467 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -190,6 +190,8 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant(); if (!$thisVariance->invariant()) { $results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]); + } elseif ($acceptsContext && $templateType->getVariance()->invariant() && $this->types[$i]->isSuperTypeOf($ancestor->types[$i])->yes()) { + $results[] = IsSuperTypeOfResult::createYes(); } else { $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]); } diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 6b63c91b88f..4ff6fa9021a 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -411,4 +411,17 @@ public function testBug12397(): void $this->analyse([__DIR__ . '/data/bug-12397.php'], []); } + #[RequiresPhp('>= 8.2')] + public function testBug10290(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-10290.php'], [ + [ + 'Function Bug10290\g() should return Bug10290\Err|Bug10290\Ok but returns Bug10290\Ok.', + 56, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-10290.php b/tests/PHPStan/Rules/Functions/data/bug-10290.php new file mode 100644 index 00000000000..0e78b0782e9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10290.php @@ -0,0 +1,60 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug10290; + +/** @template T */ +abstract class Result +{ +} + +/** @template T */ +final readonly class Ok extends Result +{ + /** @param T $data */ + public function __construct(public mixed $data) + { + } +} + +/** @template E */ +final readonly class Err extends Result +{ + /** @param E $data */ + public function __construct(public mixed $data) + { + } +} + +/** + * @return Ok|Err> + */ +function f(string $json): Result +{ + $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + assert(is_array($data)); + + if (isset($data['has_error']) && $data['has_error']) { + return new Err($data); + } + + $email = filter_var($data['email'], FILTER_VALIDATE_EMAIL); + if ($email === false) { + return new Err($data); + } + + return new Ok($email); +} + +/** + * @return Ok|Err + */ +function g(): Result +{ + if (rand() === 1) { + return new Ok(true); + } + + return new Err('error'); +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 26ffbe0fc9e..9461a6b0da8 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2220,13 +2220,7 @@ public function testGenericObjectLowerBound(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], [ - [ - 'Parameter #1 $c of method GenericObjectLowerBound\Foo::doFoo() expects GenericObjectLowerBound\Collection, GenericObjectLowerBound\Collection given.', - 48, - 'Template type T on class GenericObjectLowerBound\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - ]); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], []); } public function testNonEmptyStringVerbosity(): void @@ -2255,33 +2249,7 @@ public function testBug5372(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/data/bug-5372.php'], [ - [ - 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', - 64, - 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - [ - 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', - 68, - 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - [ - 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', - 72, - 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - [ - 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', - 81, - 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - [ - 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', - 85, - 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - ]); + $this->analyse([__DIR__ . '/data/bug-5372.php'], []); } public function testLiteralString(): void @@ -2641,11 +2609,6 @@ public function testGenericVariance(): void 'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant, GenericVarianceCall\Invariant given.', 45, ], - [ - 'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant, GenericVarianceCall\Invariant given.', - 53, - 'Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], [ 'Parameter #1 $param of method GenericVarianceCall\Foo::covariant() expects GenericVarianceCall\Covariant, GenericVarianceCall\Covariant given.', 60, @@ -2654,11 +2617,6 @@ public function testGenericVariance(): void 'Parameter #1 $param of method GenericVarianceCall\Foo::contravariant() expects GenericVarianceCall\Contravariant, GenericVarianceCall\Contravariant given.', 83, ], - [ - 'Parameter #1 $param of method GenericVarianceCall\Foo::invariantArray() expects array{GenericVarianceCall\Invariant}, array{GenericVarianceCall\Invariant} given.', - 97, - 'Offset 0 (GenericVarianceCall\Invariant) does not accept type GenericVarianceCall\Invariant: Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], ]); } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 40e42953aab..52d3d1ad0bc 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -469,28 +469,7 @@ public function testInferArrayKey(): void public function testBug4590(): void { - $this->analyse([__DIR__ . '/data/bug-4590.php'], [ - [ - 'Method Bug4590\OkResponse::testGenericStatic() should return static(Bug4590\OkResponse>) but returns static(Bug4590\OkResponse).', - 36, - 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - [ - 'Method Bug4590\\Controller::test1() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 47, - 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - [ - 'Method Bug4590\\Controller::test2() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 55, - 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - [ - 'Method Bug4590\\Controller::test3() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', - 63, - 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', - ], - ]); + $this->analyse([__DIR__ . '/data/bug-4590.php'], []); } public function testTemplateStringBound(): void diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index 2f3733c363b..a492d660fee 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -295,7 +295,7 @@ public static function dataAccepts(): array 'same class, different type args' => [ new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(A\A::class, [new ObjectType('DateTime')]), - TrinaryLogic::createNo(), + TrinaryLogic::createYes(), ], 'same class, one naked' => [ new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]), @@ -310,7 +310,7 @@ public static function dataAccepts(): array 'implementation with @extends with different type args' => [ new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]), - TrinaryLogic::createNo(), + TrinaryLogic::createYes(), ], 'generic object accepts normal object of same type' => [ new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]), @@ -330,8 +330,18 @@ public static function dataAccepts(): array ]; } + public static function dataAcceptsTypeProjections(): array + { + $data = self::dataTypeProjections(); + // In accepts context, invariant generics accept subtypes (covariant behavior) + // Entry #2: [$invariantB, $invariantC, No] → Yes because C extends B + $data[2][2] = TrinaryLogic::createYes(); + + return $data; + } + #[DataProvider('dataAccepts')] - #[DataProvider('dataTypeProjections')] + #[DataProvider('dataAcceptsTypeProjections')] public function testAccepts( Type $acceptingType, Type $acceptedType, diff --git a/tests/PHPStan/Type/StaticTypeTest.php b/tests/PHPStan/Type/StaticTypeTest.php index 980b5f60042..fabfa1e86f9 100644 --- a/tests/PHPStan/Type/StaticTypeTest.php +++ b/tests/PHPStan/Type/StaticTypeTest.php @@ -391,7 +391,7 @@ public static function dataAccepts(): iterable new StringType(), ])], null, []), new GenericStaticType($c, [new IntegerType()], null, []), - TrinaryLogic::createNo(), + TrinaryLogic::createYes(), ]; yield [