Skip to content

Commit dd75630

Browse files
committed
EnumCaseObjectType
1 parent 779320e commit dd75630

File tree

14 files changed

+731
-18
lines changed

14 files changed

+731
-18
lines changed

composer.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Analyser/MutatingScope.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
use PHPStan\Type\ConstantType;
7474
use PHPStan\Type\ConstantTypeHelper;
7575
use PHPStan\Type\DynamicReturnTypeExtensionRegistry;
76+
use PHPStan\Type\Enum\EnumCaseObjectType;
7677
use PHPStan\Type\ErrorType;
7778
use PHPStan\Type\FloatType;
7879
use PHPStan\Type\GeneralizePrecision;
@@ -2145,7 +2146,7 @@ private function resolveType(Expr $node): Type
21452146
}
21462147

21472148
if ($constantClassReflection->isEnum() && $constantClassReflection->hasEnumCase($constantName)) {
2148-
$types[] = new ObjectType($constantClassReflection->getName());
2149+
$types[] = new EnumCaseObjectType($constantClassReflection->getName(), $constantName, $constantClassReflection);
21492150
continue;
21502151
}
21512152

src/Analyser/TypeSpecifier.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use PHPStan\Type\Constant\ConstantStringType;
3535
use PHPStan\Type\ConstantScalarType;
3636
use PHPStan\Type\ConstantType;
37+
use PHPStan\Type\Enum\EnumCaseObjectType;
3738
use PHPStan\Type\FunctionTypeSpecifyingExtension;
3839
use PHPStan\Type\Generic\GenericClassStringType;
3940
use PHPStan\Type\IntegerRangeType;
@@ -294,8 +295,10 @@ public function specifyTypesInCondition(
294295
$types = null;
295296

296297
if (
297-
$exprLeftType instanceof ConstantType
298-
&& !$expr->right instanceof Node\Scalar
298+
(
299+
$exprLeftType instanceof ConstantType
300+
&& !$expr->right instanceof Node\Scalar
301+
) || $exprLeftType instanceof EnumCaseObjectType
299302
) {
300303
$types = $this->create(
301304
$expr->right,
@@ -306,8 +309,10 @@ public function specifyTypesInCondition(
306309
);
307310
}
308311
if (
309-
$exprRightType instanceof ConstantType
310-
&& !$expr->left instanceof Node\Scalar
312+
(
313+
$exprRightType instanceof ConstantType
314+
&& !$expr->left instanceof Node\Scalar
315+
) || $exprRightType instanceof EnumCaseObjectType
311316
) {
312317
$leftType = $this->create(
313318
$expr->left,

src/PhpDoc/TypeNodeResolver.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
use PHPStan\Type\Constant\ConstantIntegerType;
4949
use PHPStan\Type\Constant\ConstantStringType;
5050
use PHPStan\Type\ConstantTypeHelper;
51+
use PHPStan\Type\Enum\EnumCaseObjectType;
5152
use PHPStan\Type\ErrorType;
5253
use PHPStan\Type\FloatType;
5354
use PHPStan\Type\Generic\GenericClassStringType;
@@ -738,6 +739,11 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
738739
continue;
739740
}
740741

742+
if ($classReflection->isEnum() && $classReflection->hasEnumCase($classConstantName)) {
743+
$constantTypes[] = new EnumCaseObjectType($classReflection->getName(), $classConstantName, $classReflection);
744+
continue;
745+
}
746+
741747
$constantTypes[] = ConstantTypeHelper::getTypeFromValue($constantValue);
742748
}
743749

@@ -752,6 +758,10 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
752758
return new ErrorType();
753759
}
754760

761+
if ($classReflection->isEnum() && $classReflection->hasEnumCase($constantName)) {
762+
return new EnumCaseObjectType($classReflection->getName(), $constantName, $classReflection);
763+
}
764+
755765
return ConstantTypeHelper::getTypeFromValue($classReflection->getConstant($constantName)->getValue());
756766
}
757767

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Enum;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use PHPStan\TrinaryLogic;
7+
use PHPStan\Type\CompoundType;
8+
use PHPStan\Type\ObjectType;
9+
use PHPStan\Type\Type;
10+
use PHPStan\Type\VerbosityLevel;
11+
use function sprintf;
12+
13+
/** @api */
14+
class EnumCaseObjectType extends ObjectType
15+
{
16+
17+
public function __construct(
18+
string $className,
19+
private string $enumCaseName,
20+
?ClassReflection $classReflection = null,
21+
)
22+
{
23+
parent::__construct($className, null, $classReflection);
24+
}
25+
26+
public function getEnumCaseName(): string
27+
{
28+
return $this->enumCaseName;
29+
}
30+
31+
public function describe(VerbosityLevel $level): string
32+
{
33+
$parent = parent::describe($level);
34+
35+
return sprintf('%s::%s', $parent, $this->enumCaseName);
36+
}
37+
38+
public function equals(Type $type): bool
39+
{
40+
if (!$type instanceof self) {
41+
return false;
42+
}
43+
44+
return $this->getClassName() === $type->getClassName()
45+
&& $this->enumCaseName === $type->enumCaseName;
46+
}
47+
48+
public function accepts(Type $type, bool $strictTypes): TrinaryLogic
49+
{
50+
return $this->isSuperTypeOf($type);
51+
}
52+
53+
public function isSuperTypeOf(Type $type): TrinaryLogic
54+
{
55+
if ($type instanceof self) {
56+
return TrinaryLogic::createFromBoolean(
57+
$this->getClassName() === $type->getClassName()
58+
&& $this->enumCaseName === $type->enumCaseName,
59+
);
60+
}
61+
62+
if ($type instanceof CompoundType) {
63+
return $type->isSubTypeOf($this);
64+
}
65+
66+
return $type->isSuperTypeOf($this)->yes() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo();
67+
}
68+
69+
public function subtract(Type $type): Type
70+
{
71+
return $this;
72+
}
73+
74+
public function getTypeWithoutSubtractedType(): Type
75+
{
76+
return $this;
77+
}
78+
79+
public function changeSubtractedType(?Type $subtractedType): Type
80+
{
81+
return $this;
82+
}
83+
84+
public function getSubtractedType(): ?Type
85+
{
86+
return null;
87+
}
88+
89+
/**
90+
* @param mixed[] $properties
91+
*/
92+
public static function __set_state(array $properties): Type
93+
{
94+
return new self($properties['className'], $properties['enumCaseName'], null);
95+
}
96+
97+
}

src/Type/ObjectType.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
use PHPStan\Type\Constant\ConstantArrayType;
2929
use PHPStan\Type\Constant\ConstantBooleanType;
3030
use PHPStan\Type\Constant\ConstantStringType;
31+
use PHPStan\Type\Enum\EnumCaseObjectType;
3132
use PHPStan\Type\Generic\GenericObjectType;
3233
use PHPStan\Type\Traits\NonGenericTypeTrait;
3334
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
3435
use Traversable;
36+
use function array_keys;
3537
use function array_map;
3638
use function array_values;
3739
use function count;
@@ -981,6 +983,42 @@ public function getTypeWithoutSubtractedType(): Type
981983

982984
public function changeSubtractedType(?Type $subtractedType): Type
983985
{
986+
$classReflection = $this->getClassReflection();
987+
if ($classReflection !== null && $classReflection->isEnum() && $subtractedType !== null) {
988+
$constants = $classReflection->getNativeReflection()->getConstants();
989+
$cases = [];
990+
foreach (array_keys($constants) as $constantName) {
991+
if (!$classReflection->hasEnumCase($constantName)) {
992+
continue;
993+
}
994+
995+
$cases[$constantName] = new EnumCaseObjectType($classReflection->getName(), $constantName);
996+
}
997+
998+
foreach (TypeUtils::flattenTypes($subtractedType) as $subType) {
999+
if (!$subType instanceof EnumCaseObjectType) {
1000+
return new self($this->className, $subtractedType);
1001+
}
1002+
1003+
if ($subType->getClassName() !== $this->getClassName()) {
1004+
return new self($this->className, $subtractedType);
1005+
}
1006+
1007+
unset($cases[$subType->getEnumCaseName()]);
1008+
}
1009+
1010+
$cases = array_values($cases);
1011+
if (count($cases) === 0) {
1012+
return new NeverType();
1013+
}
1014+
1015+
if (count($cases) === 1) {
1016+
return $cases[0];
1017+
}
1018+
1019+
return new UnionType(array_values($cases));
1020+
}
1021+
9841022
return new self($this->className, $subtractedType);
9851023
}
9861024

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,11 +474,13 @@ public function testEnums(): void
474474
}
475475

476476
$errors = $this->runAnalyse(__DIR__ . '/data/enums.php');
477-
$this->assertCount(2, $errors);
478-
$this->assertSame('Access to an undefined property EnumTypeAssertions\Foo::$value.', $errors[0]->getMessage());
477+
$this->assertCount(3, $errors);
478+
$this->assertSame('Access to an undefined property EnumTypeAssertions\Foo::TWO::$value.', $errors[0]->getMessage());
479479
$this->assertSame(23, $errors[0]->getLine());
480480
$this->assertSame('Access to undefined constant EnumTypeAssertions\Baz::NONEXISTENT.', $errors[1]->getMessage());
481481
$this->assertSame(78, $errors[1]->getLine());
482+
$this->assertSame('Strict comparison using === between EnumTypeAssertions\Foo::ONE and EnumTypeAssertions\Foo::TWO will always evaluate to false.', $errors[2]->getMessage());
483+
$this->assertSame(141, $errors[2]->getLine());
482484
}
483485

484486
/**

tests/PHPStan/Analyser/data/enums.php

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class FooClass
1818

1919
public function doFoo(): void
2020
{
21-
assertType(Foo::class, Foo::ONE);
22-
assertType(Foo::class, Foo::TWO);
21+
assertType(Foo::class . '::ONE' , Foo::ONE);
22+
assertType(Foo::class . '::TWO', Foo::TWO);
2323
assertType('*ERROR*', Foo::TWO->value);
2424
assertType('array<' . Foo::class . '>', Foo::cases());
2525
}
@@ -39,8 +39,8 @@ class BarClass
3939

4040
public function doFoo(string $s): void
4141
{
42-
assertType(Bar::class, Bar::ONE);
43-
assertType(Bar::class, Bar::TWO);
42+
assertType(Bar::class . '::ONE', Bar::ONE);
43+
assertType(Bar::class . '::TWO', Bar::TWO);
4444
assertType('string', Bar::TWO->value);
4545
assertType('array<' . Bar::class . '>', Bar::cases());
4646

@@ -65,8 +65,8 @@ class BazClass
6565

6666
public function doFoo(int $i): void
6767
{
68-
assertType(Baz::class, Baz::ONE);
69-
assertType(Baz::class, Baz::TWO);
68+
assertType(Baz::class . '::ONE', Baz::ONE);
69+
assertType(Baz::class . '::TWO', Baz::TWO);
7070
assertType('int', Baz::TWO->value);
7171
assertType('array<' . Baz::class . '>', Baz::cases());
7272

@@ -78,4 +78,74 @@ public function doFoo(int $i): void
7878
assertType('*ERROR*', Baz::NONEXISTENT);
7979
}
8080

81+
/**
82+
* @param Baz::ONE $enum
83+
* @param Baz::THREE $constant
84+
* @return void
85+
*/
86+
public function doBar($enum, $constant): void
87+
{
88+
assertType(Baz::class . '::ONE', $enum);
89+
assertType('3', $constant);
90+
}
91+
92+
/**
93+
* @param Baz::ONE $enum
94+
* @param Baz::THREE $constant
95+
* @return void
96+
*/
97+
public function doBaz(Baz $enum, $constant): void
98+
{
99+
assertType(Baz::class . '::ONE', $enum);
100+
assertType('3', $constant);
101+
}
102+
103+
/**
104+
* @param Foo::* $enums
105+
* @return void
106+
*/
107+
public function doLorem($enums): void
108+
{
109+
assertType(Foo::class . '::ONE|' . Foo::class . '::TWO', $enums);
110+
}
111+
112+
}
113+
114+
class Lorem
115+
{
116+
117+
public function doFoo(Foo $foo): void
118+
{
119+
if ($foo === Foo::ONE) {
120+
assertType(Foo::class . '::ONE', $foo);
121+
return;
122+
}
123+
124+
assertType(Foo::class . '::TWO', $foo);
125+
}
126+
127+
public function doBar(Foo $foo): void
128+
{
129+
if (Foo::ONE === $foo) {
130+
assertType(Foo::class . '::ONE', $foo);
131+
return;
132+
}
133+
134+
assertType(Foo::class . '::TWO', $foo);
135+
}
136+
137+
public function doBaz(Foo $foo): void
138+
{
139+
if ($foo === Foo::ONE) {
140+
assertType(Foo::class . '::ONE', $foo);
141+
if ($foo === Foo::TWO) {
142+
assertType('*NEVER*', $foo);
143+
} else {
144+
assertType(Foo::class . '::ONE', $foo);
145+
}
146+
147+
assertType(Foo::class . '::ONE', $foo);
148+
}
149+
}
150+
81151
}

0 commit comments

Comments
 (0)