Skip to content

Commit ee7e0fd

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
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<array{ok: string}> accepted for OkResponse<array<string, string>>) - Updated existing tests to reflect the more lenient accepts behavior
1 parent 4c6ef6e commit ee7e0fd

File tree

7 files changed

+92
-70
lines changed

7 files changed

+92
-70
lines changed

src/Type/Generic/GenericObjectType.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe
190190
$ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant();
191191
if (!$thisVariance->invariant()) {
192192
$results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]);
193+
} elseif ($acceptsContext && $templateType->getVariance()->invariant() && $this->types[$i]->isSuperTypeOf($ancestor->types[$i])->yes()) {
194+
$results[] = IsSuperTypeOfResult::createYes();
193195
} else {
194196
$results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]);
195197
}

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,17 @@ public function testBug12397(): void
411411
$this->analyse([__DIR__ . '/data/bug-12397.php'], []);
412412
}
413413

414+
#[RequiresPhp('>= 8.2')]
415+
public function testBug10290(): void
416+
{
417+
$this->checkNullables = true;
418+
$this->checkExplicitMixed = false;
419+
$this->analyse([__DIR__ . '/data/bug-10290.php'], [
420+
[
421+
'Function Bug10290\g() should return Bug10290\Err<string>|Bug10290\Ok<true> but returns Bug10290\Ok<bool>.',
422+
56,
423+
],
424+
]);
425+
}
426+
414427
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug10290;
6+
7+
/** @template T */
8+
abstract class Result
9+
{
10+
}
11+
12+
/** @template T */
13+
final readonly class Ok extends Result
14+
{
15+
/** @param T $data */
16+
public function __construct(public mixed $data)
17+
{
18+
}
19+
}
20+
21+
/** @template E */
22+
final readonly class Err extends Result
23+
{
24+
/** @param E $data */
25+
public function __construct(public mixed $data)
26+
{
27+
}
28+
}
29+
30+
/**
31+
* @return Ok<non-empty-string>|Err<array<mixed>>
32+
*/
33+
function f(string $json): Result
34+
{
35+
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
36+
assert(is_array($data));
37+
38+
if (isset($data['has_error']) && $data['has_error']) {
39+
return new Err($data);
40+
}
41+
42+
$email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
43+
if ($email === false) {
44+
return new Err($data);
45+
}
46+
47+
return new Ok($email);
48+
}
49+
50+
/**
51+
* @return Ok<true>|Err<string>
52+
*/
53+
function g(): Result
54+
{
55+
if (rand() === 1) {
56+
return new Ok(true);
57+
}
58+
59+
return new Err('error');
60+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,13 +2220,7 @@ public function testGenericObjectLowerBound(): void
22202220
$this->checkThisOnly = false;
22212221
$this->checkNullables = true;
22222222
$this->checkUnionTypes = true;
2223-
$this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], [
2224-
[
2225-
'Parameter #1 $c of method GenericObjectLowerBound\Foo::doFoo() expects GenericObjectLowerBound\Collection<GenericObjectLowerBound\Cat|GenericObjectLowerBound\Dog>, GenericObjectLowerBound\Collection<GenericObjectLowerBound\Dog> given.',
2226-
48,
2227-
'Template type T on class GenericObjectLowerBound\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2228-
],
2229-
]);
2223+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], []);
22302224
}
22312225

22322226
public function testNonEmptyStringVerbosity(): void
@@ -2255,33 +2249,7 @@ public function testBug5372(): void
22552249
$this->checkThisOnly = false;
22562250
$this->checkNullables = true;
22572251
$this->checkUnionTypes = true;
2258-
$this->analyse([__DIR__ . '/data/bug-5372.php'], [
2259-
[
2260-
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, non-falsy-string> given.',
2261-
64,
2262-
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2263-
],
2264-
[
2265-
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, class-string> given.',
2266-
68,
2267-
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2268-
],
2269-
[
2270-
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, class-string> given.',
2271-
72,
2272-
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2273-
],
2274-
[
2275-
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, literal-string> given.',
2276-
81,
2277-
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2278-
],
2279-
[
2280-
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, literal-string> given.',
2281-
85,
2282-
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2283-
],
2284-
]);
2252+
$this->analyse([__DIR__ . '/data/bug-5372.php'], []);
22852253
}
22862254

22872255
public function testLiteralString(): void
@@ -2641,11 +2609,6 @@ public function testGenericVariance(): void
26412609
'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant<GenericVarianceCall\B>, GenericVarianceCall\Invariant<GenericVarianceCall\A> given.',
26422610
45,
26432611
],
2644-
[
2645-
'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant<GenericVarianceCall\B>, GenericVarianceCall\Invariant<GenericVarianceCall\C> given.',
2646-
53,
2647-
'Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2648-
],
26492612
[
26502613
'Parameter #1 $param of method GenericVarianceCall\Foo::covariant() expects GenericVarianceCall\Covariant<GenericVarianceCall\B>, GenericVarianceCall\Covariant<GenericVarianceCall\A> given.',
26512614
60,
@@ -2654,11 +2617,6 @@ public function testGenericVariance(): void
26542617
'Parameter #1 $param of method GenericVarianceCall\Foo::contravariant() expects GenericVarianceCall\Contravariant<GenericVarianceCall\B>, GenericVarianceCall\Contravariant<GenericVarianceCall\C> given.',
26552618
83,
26562619
],
2657-
[
2658-
'Parameter #1 $param of method GenericVarianceCall\Foo::invariantArray() expects array{GenericVarianceCall\Invariant<GenericVarianceCall\B>}, array{GenericVarianceCall\Invariant<GenericVarianceCall\C>} given.',
2659-
97,
2660-
'Offset 0 (GenericVarianceCall\Invariant<GenericVarianceCall\B>) does not accept type GenericVarianceCall\Invariant<GenericVarianceCall\C>: Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
2661-
],
26622620
]);
26632621
}
26642622

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -469,28 +469,7 @@ public function testInferArrayKey(): void
469469

470470
public function testBug4590(): void
471471
{
472-
$this->analyse([__DIR__ . '/data/bug-4590.php'], [
473-
[
474-
'Method Bug4590\OkResponse::testGenericStatic() should return static(Bug4590\OkResponse<array<string, string>>) but returns static(Bug4590\OkResponse<array{ok: string}>).',
475-
36,
476-
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
477-
],
478-
[
479-
'Method Bug4590\\Controller::test1() should return Bug4590\\OkResponse<array<string, string>> but returns Bug4590\\OkResponse<array{ok: string}>.',
480-
47,
481-
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
482-
],
483-
[
484-
'Method Bug4590\\Controller::test2() should return Bug4590\\OkResponse<array<int, string>> but returns Bug4590\\OkResponse<array{string}>.',
485-
55,
486-
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
487-
],
488-
[
489-
'Method Bug4590\\Controller::test3() should return Bug4590\\OkResponse<array<string>> but returns Bug4590\\OkResponse<array{string}>.',
490-
63,
491-
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
492-
],
493-
]);
472+
$this->analyse([__DIR__ . '/data/bug-4590.php'], []);
494473
}
495474

496475
public function testTemplateStringBound(): void

tests/PHPStan/Type/Generic/GenericObjectTypeTest.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ public static function dataAccepts(): array
295295
'same class, different type args' => [
296296
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
297297
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
298-
TrinaryLogic::createNo(),
298+
TrinaryLogic::createYes(),
299299
],
300300
'same class, one naked' => [
301301
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
@@ -310,7 +310,7 @@ public static function dataAccepts(): array
310310
'implementation with @extends with different type args' => [
311311
new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]),
312312
new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]),
313-
TrinaryLogic::createNo(),
313+
TrinaryLogic::createYes(),
314314
],
315315
'generic object accepts normal object of same type' => [
316316
new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]),
@@ -330,8 +330,18 @@ public static function dataAccepts(): array
330330
];
331331
}
332332

333+
public static function dataAcceptsTypeProjections(): array
334+
{
335+
$data = self::dataTypeProjections();
336+
// In accepts context, invariant generics accept subtypes (covariant behavior)
337+
// Entry #2: [$invariantB, $invariantC, No] → Yes because C extends B
338+
$data[2][2] = TrinaryLogic::createYes();
339+
340+
return $data;
341+
}
342+
333343
#[DataProvider('dataAccepts')]
334-
#[DataProvider('dataTypeProjections')]
344+
#[DataProvider('dataAcceptsTypeProjections')]
335345
public function testAccepts(
336346
Type $acceptingType,
337347
Type $acceptedType,

tests/PHPStan/Type/StaticTypeTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ public static function dataAccepts(): iterable
391391
new StringType(),
392392
])], null, []),
393393
new GenericStaticType($c, [new IntegerType()], null, []),
394-
TrinaryLogic::createNo(),
394+
TrinaryLogic::createYes(),
395395
];
396396

397397
yield [

0 commit comments

Comments
 (0)