Skip to content

Commit 69a7cf6

Browse files
phpstan-botstaabmclaude
authored
Preserve template bound for enum-case, integer-range and constant-bool subtypes in TemplateTypeFactory::create() (#5905)
Co-authored-by: staabm <120441+staabm@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d6abe7f commit 69a7cf6

3 files changed

Lines changed: 62 additions & 8 deletions

File tree

src/Type/Generic/TemplateTypeFactory.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Type\Constant\ConstantIntegerType;
1111
use PHPStan\Type\Constant\ConstantStringType;
1212
use PHPStan\Type\FloatType;
13+
use PHPStan\Type\IntegerRangeType;
1314
use PHPStan\Type\IntegerType;
1415
use PHPStan\Type\IntersectionType;
1516
use PHPStan\Type\IterableType;
@@ -39,14 +40,17 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
3940
}
4041

4142
$boundClass = get_class($bound);
42-
if ($bound instanceof ObjectType && ($boundClass === ObjectType::class || $bound instanceof TemplateType)) {
43-
return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default);
44-
}
45-
4643
if ($bound instanceof GenericObjectType && ($boundClass === GenericObjectType::class || $bound instanceof TemplateType)) {
4744
return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound, $default);
4845
}
4946

47+
// Catches plain ObjectType and any other object subtype without a dedicated
48+
// Template* class (e.g. enum-case object types), preserving the precise bound
49+
// instead of widening it to TemplateMixedType.
50+
if ($bound instanceof ObjectType) {
51+
return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default);
52+
}
53+
5054
if ($bound instanceof ObjectWithoutClassType && ($boundClass === ObjectWithoutClassType::class || $bound instanceof TemplateType)) {
5155
return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound, $default);
5256
}
@@ -71,7 +75,7 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
7175
return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound, $default);
7276
}
7377

74-
if ($bound instanceof IntegerType && ($boundClass === IntegerType::class || $bound instanceof TemplateType)) {
78+
if ($bound instanceof IntegerType && ($boundClass === IntegerType::class || $bound instanceof IntegerRangeType || $bound instanceof TemplateType)) {
7579
return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound, $default);
7680
}
7781

@@ -83,7 +87,7 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
8387
return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default);
8488
}
8589

86-
if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound instanceof TemplateType)) {
90+
if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound->isTrue()->yes() || $bound->isFalse()->yes() || $bound instanceof TemplateType)) {
8791
return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound, $default);
8892
}
8993

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug10083;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
enum Foo
8+
{
9+
10+
case Abc;
11+
case Bcd;
12+
13+
}
14+
15+
/**
16+
* @template TFoo of Foo::*
17+
* @param TFoo $foo
18+
*/
19+
function checkFoo($foo): void
20+
{
21+
}
22+
23+
/**
24+
* @template TFoo of Foo::*
25+
* @param TFoo $foo
26+
*/
27+
function narrowEnum($foo): void
28+
{
29+
if (Foo::Abc === $foo) {
30+
assertType('TFoo of Bug10083\Foo::Abc (function Bug10083\narrowEnum(), argument)', $foo);
31+
} else {
32+
assertType('TFoo of Bug10083\Foo::Bcd (function Bug10083\narrowEnum(), argument)', $foo);
33+
}
34+
35+
$filter = Foo::Abc === $foo ? 'Abc' : 'Bcd';
36+
assertType('TFoo of Bug10083\Foo::Abc (function Bug10083\narrowEnum(), argument)|TFoo of Bug10083\Foo::Bcd (function Bug10083\narrowEnum(), argument)', $foo);
37+
checkFoo($foo);
38+
}
39+
40+
/**
41+
* @template TInt of int
42+
* @param TInt $int
43+
*/
44+
function narrowIntRange($int): void
45+
{
46+
if ($int >= 0 && $int <= 5) {
47+
assertType('TInt of int<0, 5> (function Bug10083\narrowIntRange(), argument)', $int);
48+
}
49+
}

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
use PHPStan\Type\Generic\GenericObjectType;
4646
use PHPStan\Type\Generic\GenericStaticType;
4747
use PHPStan\Type\Generic\TemplateBenevolentUnionType;
48+
use PHPStan\Type\Generic\TemplateBooleanType;
4849
use PHPStan\Type\Generic\TemplateIntersectionType;
4950
use PHPStan\Type\Generic\TemplateMixedType;
5051
use PHPStan\Type\Generic\TemplateObjectType;
@@ -6354,8 +6355,8 @@ public static function dataRemove(): array
63546355
TemplateTypeVariance::createInvariant(),
63556356
),
63566357
new ConstantBooleanType(false),
6357-
TemplateMixedType::class, // should be TemplateConstantBooleanType
6358-
'T (class Foo, parameter)', // should be T of true
6358+
TemplateBooleanType::class,
6359+
'T of true (class Foo, parameter)',
63596360
],
63606361
[
63616362
new ObjectShapeType(['foo' => new IntegerType()], []),

0 commit comments

Comments
 (0)