Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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‎

Lines changed: 7 additions & 2 deletions
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‎

Lines changed: 23 additions & 9 deletions
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‎

Lines changed: 2 additions & 2 deletions
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
[
Lines changed: 43 additions & 0 deletions
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‎

Lines changed: 53 additions & 2 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /