Skip to content

Commit 9e9f82d

Browse files
github-actions[bot]staabm
authored andcommitted
Fix phpstan/phpstan#11430: Two unbounded generics in conditional return assumed same
- Don't subtract TemplateType from another TemplateType's bound in tryRemove - This prevented `T of mixed~S` from being created, which couldn't match the plain `T` in invariant generic argument checks - New regression test in tests/PHPStan/Rules/TooWideTypehints/data/bug-11430.php
1 parent 7b8a821 commit 9e9f82d

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

src/Type/Generic/TemplateTypeTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,10 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
368368

369369
public function tryRemove(Type $typeToRemove): ?Type
370370
{
371+
if ($typeToRemove instanceof TemplateType) {
372+
return null;
373+
}
374+
371375
$bound = TypeCombinator::remove($this->getBound(), $typeToRemove);
372376
if ($this->getBound() === $bound) {
373377
return null;

tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,10 @@ public function testBug13676(): void
309309
$this->analyse([__DIR__ . '/data/bug-13676.php'], []);
310310
}
311311

312+
#[RequiresPhp('>= 8.2')]
313+
public function testBug11430(): void
314+
{
315+
$this->analyse([__DIR__ . '/data/bug-11430.php'], []);
316+
}
317+
312318
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11430;
6+
7+
/**
8+
* @template T
9+
*/
10+
interface Option {}
11+
12+
/**
13+
* @template T
14+
* @implements Option<T>
15+
*/
16+
class Some implements Option
17+
{
18+
/**
19+
* @param T $value
20+
*/
21+
public function __construct(public mixed $value) {}
22+
}
23+
24+
/**
25+
* @implements Option<never>
26+
*/
27+
class None implements Option {}
28+
29+
/**
30+
* @internal
31+
*/
32+
final class Choice
33+
{
34+
/**
35+
* @template T
36+
* @template S
37+
*
38+
* @param T $value
39+
* @param S $none
40+
*
41+
* @return (T is S ? None : Some<T>)
42+
*/
43+
public static function from(mixed $value, mixed $none = null): Option
44+
{
45+
if ($value === $none) {
46+
return new None();
47+
}
48+
49+
return new Some($value);
50+
}
51+
}

0 commit comments

Comments
 (0)