diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index 5e803a6af9..e3ae5e23f5 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -18,6 +18,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; @@ -94,7 +95,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult public function subtract(Type $type): Type { - return $this; + return $this->changeSubtractedType($type); } public function getTypeWithoutSubtractedType(): Type @@ -104,7 +105,11 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { - return $this; + if ($subtractedType === null || ! $this->equals($subtractedType)) { + return $this; + } + + return new NeverType(); } public function getSubtractedType(): ?Type diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 37ae13258f..29b57c1593 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -595,17 +595,31 @@ private static function intersectWithSubtractedType( } $subtractedType = self::union(...$subtractedTypes); - } elseif ($b instanceof SubtractableType) { - $subtractedType = $b->getSubtractedType(); - if ($subtractedType === null) { - return $a->getTypeWithoutSubtractedType(); - } } else { - $subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType()); - if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) { - return $a->getTypeWithoutSubtractedType(); + $isBAlreadySubtracted = $a->getSubtractedType()->isSuperTypeOf($b); + + if ($isBAlreadySubtracted->no()) { + return $a; + } elseif ($isBAlreadySubtracted->yes()) { + $subtractedType = self::remove($a->getSubtractedType(), $b); + + if ($subtractedType instanceof NeverType) { + $subtractedType = null; + } + + return $a->changeSubtractedType($subtractedType); + } elseif ($b instanceof SubtractableType) { + $subtractedType = $b->getSubtractedType(); + if ($subtractedType === null) { + return $a->getTypeWithoutSubtractedType(); + } + } else { + $subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType()); + if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) { + return $a->getTypeWithoutSubtractedType(); + } + $subtractedType = new MixedType(false, $b); } - $subtractedType = new MixedType(false, $b); } $subtractedType = self::intersect( diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 8b89e0935e..167cad5f8e 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8105,11 +8105,11 @@ public function dataArrayKeysInBranches(): array '$array', ], [ - 'non-empty-array&hasOffsetValue(\'key\', mixed)', + 'non-empty-array&hasOffsetValue(\'key\', mixed~null)', '$generalArray', ], [ - 'mixed', + 'mixed~null', '$generalArray[\'key\']', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php b/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php new file mode 100644 index 0000000000..4a9a22e4c5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php @@ -0,0 +1,43 @@ += 8.1 + +declare(strict_types = 1); + +namespace EnumVsInArray; + +use function PHPStan\Testing\assertType; + +enum FooEnum +{ + case A; + case B; + case C; + case D; + case E; + case F; + case G; + case H; + case I; + case J; +} + +function foo(FooEnum $e): int +{ + if (in_array($e, [FooEnum::A, FooEnum::B, FooEnum::C], true)) { + throw new \Exception('a'); + } + + assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e); + + if (rand(0, 10) === 1) { + if (!in_array($e, [FooEnum::D, FooEnum::E], true)) { + throw new \Exception('d'); + } + } + + assertType('EnumVsInArray\FooEnum~(EnumVsInArray\FooEnum::A|EnumVsInArray\FooEnum::B|EnumVsInArray\FooEnum::C)', $e); + + return match ($e) { + FooEnum::D, FooEnum::E, FooEnum::F, FooEnum::G, FooEnum::H, FooEnum::I => 2, + FooEnum::J => 3, + }; +} diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 7dd88c8e69..a62073a40c 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -1069,7 +1069,7 @@ public function dataUnion(): iterable new ObjectWithoutClassType(new ObjectType('A')), ], MixedType::class, - 'mixed=implicit', + 'mixed~int=implicit', ], [ [ @@ -1125,7 +1125,7 @@ public function dataUnion(): iterable new ObjectType('InvalidArgumentException'), ], MixedType::class, - 'mixed=implicit', // should be MixedType~Exception+InvalidArgumentException + 'mixed~Exception~InvalidArgumentException=implicit', ], [ [ @@ -2262,6 +2262,36 @@ public function dataUnion(): iterable 'PHPStan\Fixture\ManyCasesTestEnum~PHPStan\Fixture\ManyCasesTestEnum::A', ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ])), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'C'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + ]), + ], + ObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum~(PHPStan\Fixture\ManyCasesTestEnum::A|PHPStan\Fixture\ManyCasesTestEnum::B)', + ]; + + yield [ + [ + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ])), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + ]), + ], + ObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum~PHPStan\Fixture\ManyCasesTestEnum::B', + ]; + yield [ [ new ThisType( @@ -4224,6 +4254,27 @@ public function dataIntersect(): iterable '$this(stdClass)&stdClass::foo', ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new MixedType(false, new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ + [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ]), + new MixedType(false, new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum::B', + ]; + yield [ [ TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()),