Skip to content

Commit b8fd9a0

Browse files
Resolve template with value of BackedEnum
1 parent ee071c0 commit b8fd9a0

8 files changed

Lines changed: 216 additions & 1 deletion

File tree

src/Type/ValueOfType.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
66
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
77
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8+
use PHPStan\Type\Generic\TemplateType;
89
use PHPStan\Type\Generic\TemplateTypeVariance;
910
use PHPStan\Type\Traits\LateResolvableTypeTrait;
1011
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
@@ -51,8 +52,17 @@ public function isResolvable(): bool
5152
protected function getResult(): Type
5253
{
5354
if ($this->type->isEnum()->yes()) {
55+
$enumCases = $this->type->getEnumCases();
56+
if (
57+
$enumCases === []
58+
&& $this->type instanceof TemplateType
59+
&& (new ObjectType('BackedEnum'))->isSuperTypeOf($this->type->getBound())->yes()
60+
) {
61+
return new UnionType([new IntegerType(), new StringType()]);
62+
}
63+
5464
$valueTypes = [];
55-
foreach ($this->type->getEnumCases() as $enumCase) {
65+
foreach ($enumCases as $enumCase) {
5666
$valueType = $enumCase->getBackingValueType();
5767
if ($valueType === null) {
5868
continue;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug13283;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
enum Test: string
8+
{
9+
case NAME = 'name';
10+
case VALUE = 'value';
11+
}
12+
13+
/**
14+
* @template T of \BackedEnum
15+
* @param class-string<T> $enum
16+
* @phpstan-assert null|value-of<T> $value
17+
*/
18+
function assertValue(mixed $value, string $enum): void
19+
{
20+
if (null === $value) {
21+
return;
22+
}
23+
24+
if (! is_int($value) && ! is_string($value)) {
25+
throw new \Exception();
26+
}
27+
28+
if (null === $enum::tryFrom($value)) {
29+
throw new \Exception();
30+
}
31+
}
32+
33+
function getFromRequest(): mixed
34+
{
35+
return 'name';
36+
}
37+
38+
$v = getFromRequest();
39+
40+
assertType('mixed', $v);
41+
42+
assertValue($v, Test::class);
43+
44+
assertType("'name'|'value'|null", $v);
45+
46+
$a = null !== $v ? Test::from($v) : null;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug13782;
4+
5+
use BackedEnum;
6+
use function PHPStan\Testing\assertType;
7+
8+
enum IntEnum : int
9+
{
10+
case A = 1;
11+
case B = 2;
12+
}
13+
14+
class EnumMethods
15+
{
16+
/**
17+
* @template TEnum of BackedEnum
18+
* @param TEnum $enum
19+
* @return value-of<TEnum>
20+
*/
21+
public static function getValue(BackedEnum $enum): int|string
22+
{
23+
return $enum->value;
24+
}
25+
26+
/**
27+
* @template TEnum of BackedEnum
28+
* @param TEnum|null $enum
29+
* @return ($enum is TEnum ? value-of<TEnum> : null)
30+
*/
31+
public static function getNullableValue(?BackedEnum $enum): int|string|null
32+
{
33+
return $enum === null ? null : $enum->value;
34+
}
35+
}
36+
37+
assertType("2", EnumMethods::getValue(IntEnum::B));
38+
assertType("2", EnumMethods::getNullableValue(IntEnum::B));

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,12 @@ public function testBug13208(): void
10341034
$this->analyse([__DIR__ . '/data/bug-13208.php'], []);
10351035
}
10361036

1037+
#[RequiresPhp('>= 8.1')]
1038+
public function testBug13282(): void
1039+
{
1040+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13282.php'], []);
1041+
}
1042+
10371043
public function testBug11609(): void
10381044
{
10391045
$this->analyse([__DIR__ . '/data/bug-11609.php'], [

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,14 @@ public function testBug12274(): void
357357
]);
358358
}
359359

360+
#[RequiresPhp('>= 8.1')]
361+
public function testBug13638(): void
362+
{
363+
$this->checkNullables = true;
364+
$this->checkExplicitMixed = true;
365+
$this->analyse([__DIR__ . '/data/bug-13638.php'], []);
366+
}
367+
360368
public function testBug13484(): void
361369
{
362370
$this->checkExplicitMixed = true;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug13638;
4+
5+
use BackedEnum;
6+
7+
/**
8+
* @template T of BackedEnum
9+
* @param ?value-of<T> $a
10+
* @return ($a is null ? list<?value-of<T>> : list<value-of<T>>)
11+
*/
12+
function test1(int | string | null $a): array
13+
{
14+
return [$a];
15+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3684,6 +3684,21 @@ public function testBug13881(): void
36843684
$this->analyse([__DIR__ . '/data/bug-13881.php'], []);
36853685
}
36863686

3687+
#[RequiresPhp('>= 8.1')]
3688+
public function testBug12219(): void
3689+
{
3690+
$this->checkThisOnly = false;
3691+
$this->checkNullables = true;
3692+
$this->checkUnionTypes = true;
3693+
$this->checkExplicitMixed = true;
3694+
$this->analyse([__DIR__ . '/data/bug-12219.php'], [
3695+
[
3696+
'Parameter #2 $value of method Bug12219\Bug3::foo() contains unresolvable type.',
3697+
75,
3698+
],
3699+
]);
3700+
}
3701+
36873702
public function testBug13511(): void
36883703
{
36893704
$this->checkThisOnly = false;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug12219;
4+
5+
class Bug
6+
{
7+
/**
8+
* @template T of \BackedEnum
9+
* @param class-string<T> $class
10+
* @param value-of<T> $value
11+
*/
12+
public function foo(string $class, mixed $value): void
13+
{
14+
}
15+
16+
/**
17+
* @template Q of \BackedEnum
18+
* @param class-string<Q> $class
19+
* @param value-of<Q> $value
20+
*/
21+
public function bar(string $class, mixed $value): void
22+
{
23+
// Parameter #2 $value of static method Bug::foo() contains unresolvable type.
24+
$this->foo($class, $value);
25+
}
26+
}
27+
28+
interface SuperBackedEnum extends \BackedEnum
29+
{
30+
public function customMethod(): void;
31+
}
32+
33+
class Bug2
34+
{
35+
/**
36+
* @template T of SuperBackedEnum
37+
* @param class-string<T> $class
38+
* @param value-of<T> $value
39+
*/
40+
public function foo(string $class, mixed $value): void
41+
{
42+
}
43+
44+
/**
45+
* @template Q of SuperBackedEnum
46+
* @param class-string<Q> $class
47+
* @param value-of<Q> $value
48+
*/
49+
public function bar(string $class, mixed $value): void
50+
{
51+
// Parameter #2 $value of static method Bug::foo() contains unresolvable type.
52+
$this->foo($class, $value);
53+
}
54+
}
55+
56+
class Bug3
57+
{
58+
/**
59+
* @template T of \UnitEnum
60+
* @param class-string<T> $class
61+
* @param value-of<T> $value
62+
*/
63+
public function foo(string $class, mixed $value): void
64+
{
65+
}
66+
67+
/**
68+
* @template Q of \UnitEnum
69+
* @param class-string<Q> $class
70+
* @param value-of<Q> $value
71+
*/
72+
public function bar(string $class, mixed $value): void
73+
{
74+
// Parameter #2 $value of static method Bug::foo() contains unresolvable type.
75+
$this->foo($class, $value);
76+
}
77+
}

0 commit comments

Comments
 (0)