Skip to content

Commit b225f74

Browse files
authored
Fix union/intersect involving enum case
1 parent 17beb01 commit b225f74

File tree

5 files changed

+128
-15
lines changed

5 files changed

+128
-15
lines changed

src/Type/Enum/EnumCaseObjectType.php

+7-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use PHPStan\Type\Constant\ConstantStringType;
1919
use PHPStan\Type\GeneralizePrecision;
2020
use PHPStan\Type\IsSuperTypeOfResult;
21+
use PHPStan\Type\NeverType;
2122
use PHPStan\Type\ObjectType;
2223
use PHPStan\Type\SubtractableType;
2324
use PHPStan\Type\Type;
@@ -94,7 +95,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
9495

9596
public function subtract(Type $type): Type
9697
{
97-
return $this;
98+
return $this->changeSubtractedType($type);
9899
}
99100

100101
public function getTypeWithoutSubtractedType(): Type
@@ -104,7 +105,11 @@ public function getTypeWithoutSubtractedType(): Type
104105

105106
public function changeSubtractedType(?Type $subtractedType): Type
106107
{
107-
return $this;
108+
if ($subtractedType === null || ! $this->equals($subtractedType)) {
109+
return $this;
110+
}
111+
112+
return new NeverType();
108113
}
109114

110115
public function getSubtractedType(): ?Type

src/Type/TypeCombinator.php

+23-9
Original file line numberDiff line numberDiff line change
@@ -595,17 +595,31 @@ private static function intersectWithSubtractedType(
595595
}
596596

597597
$subtractedType = self::union(...$subtractedTypes);
598-
} elseif ($b instanceof SubtractableType) {
599-
$subtractedType = $b->getSubtractedType();
600-
if ($subtractedType === null) {
601-
return $a->getTypeWithoutSubtractedType();
602-
}
603598
} else {
604-
$subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType());
605-
if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) {
606-
return $a->getTypeWithoutSubtractedType();
599+
$isBAlreadySubtracted = $a->getSubtractedType()->isSuperTypeOf($b);
600+
601+
if ($isBAlreadySubtracted->no()) {
602+
return $a;
603+
} elseif ($isBAlreadySubtracted->yes()) {
604+
$subtractedType = self::remove($a->getSubtractedType(), $b);
605+
606+
if ($subtractedType instanceof NeverType) {
607+
$subtractedType = null;
608+
}
609+
610+
return $a->changeSubtractedType($subtractedType);
611+
} elseif ($b instanceof SubtractableType) {
612+
$subtractedType = $b->getSubtractedType();
613+
if ($subtractedType === null) {
614+
return $a->getTypeWithoutSubtractedType();
615+
}
616+
} else {
617+
$subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType());
618+
if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) {
619+
return $a->getTypeWithoutSubtractedType();
620+
}
621+
$subtractedType = new MixedType(false, $b);
607622
}
608-
$subtractedType = new MixedType(false, $b);
609623
}
610624

611625
$subtractedType = self::intersect(

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -8105,11 +8105,11 @@ public function dataArrayKeysInBranches(): array
81058105
'$array',
81068106
],
81078107
[
8108-
'non-empty-array&hasOffsetValue(\'key\', mixed)',
8108+
'non-empty-array&hasOffsetValue(\'key\', mixed~null)',
81098109
'$generalArray',
81108110
],
81118111
[
8112-
'mixed',
8112+
'mixed~null',
81138113
'$generalArray[\'key\']',
81148114
],
81158115
[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace EnumVsInArray;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
enum FooEnum
10+
{
11+
case A;
12+
case B;
13+
case C;
14+
case D;
15+
case E;
16+
case F;
17+
case G;
18+
case H;
19+
case I;
20+
case J;
21+
}
22+
23+
function foo(FooEnum $e): int
24+
{
25+
if (in_array($e, [FooEnum::A, FooEnum::B, FooEnum::C], true)) {
26+
throw new \Exception('a');
27+
}
28+
29+
assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e);
30+
31+
if (rand(0, 10) === 1) {
32+
if (!in_array($e, [FooEnum::D, FooEnum::E], true)) {
33+
throw new \Exception('d');
34+
}
35+
}
36+
37+
assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e);
38+
39+
return match ($e) {
40+
FooEnum::D, FooEnum::E, FooEnum::F, FooEnum::G, FooEnum::H, FooEnum::I => 2,
41+
FooEnum::J => 3,
42+
};
43+
}

tests/PHPStan/Type/TypeCombinatorTest.php

+53-2
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ public function dataUnion(): iterable
10691069
new ObjectWithoutClassType(new ObjectType('A')),
10701070
],
10711071
MixedType::class,
1072-
'mixed=implicit',
1072+
'mixed~int=implicit',
10731073
],
10741074
[
10751075
[
@@ -1125,7 +1125,7 @@ public function dataUnion(): iterable
11251125
new ObjectType('InvalidArgumentException'),
11261126
],
11271127
MixedType::class,
1128-
'mixed=implicit', // should be MixedType~Exception+InvalidArgumentException
1128+
'mixed~Exception~InvalidArgumentException=implicit',
11291129
],
11301130
[
11311131
[
@@ -2262,6 +2262,36 @@ public function dataUnion(): iterable
22622262
'PHPStan\Fixture\ManyCasesTestEnum~PHPStan\Fixture\ManyCasesTestEnum::A',
22632263
];
22642264

2265+
yield [
2266+
[
2267+
new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([
2268+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'),
2269+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'),
2270+
])),
2271+
new UnionType([
2272+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'C'),
2273+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'),
2274+
]),
2275+
],
2276+
ObjectType::class,
2277+
'PHPStan\Fixture\ManyCasesTestEnum~(PHPStan\Fixture\ManyCasesTestEnum::A|PHPStan\Fixture\ManyCasesTestEnum::B)',
2278+
];
2279+
2280+
yield [
2281+
[
2282+
new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([
2283+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'),
2284+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'),
2285+
])),
2286+
new UnionType([
2287+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'),
2288+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'),
2289+
]),
2290+
],
2291+
ObjectType::class,
2292+
'PHPStan\Fixture\ManyCasesTestEnum~PHPStan\Fixture\ManyCasesTestEnum::B',
2293+
];
2294+
22652295
yield [
22662296
[
22672297
new ThisType(
@@ -4224,6 +4254,27 @@ public function dataIntersect(): iterable
42244254
'$this(stdClass)&stdClass::foo',
42254255
];
42264256

4257+
yield [
4258+
[
4259+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'),
4260+
new MixedType(false, new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')),
4261+
],
4262+
NeverType::class,
4263+
'*NEVER*=implicit',
4264+
];
4265+
4266+
yield [
4267+
[
4268+
new UnionType([
4269+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'),
4270+
new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'),
4271+
]),
4272+
new MixedType(false, new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')),
4273+
],
4274+
EnumCaseObjectType::class,
4275+
'PHPStan\Fixture\ManyCasesTestEnum::B',
4276+
];
4277+
42274278
yield [
42284279
[
42294280
TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()),

0 commit comments

Comments
 (0)