From f0762db0d30d32aca519a0fd51894db6b454619e Mon Sep 17 00:00:00 2001 From: schlndh Date: Fri, 28 Mar 2025 14:01:00 +0100 Subject: [PATCH 1/2] fix union/intersect involving enum case --- phpstan-baseline.neon | 6 +++ src/Type/TypeCombinator.php | 5 +- .../Analyser/nsrt/enum-vs-in-array.php | 43 ++++++++++++++++ tests/PHPStan/Type/TypeCombinatorTest.php | 51 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/enum-vs-in-array.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d252aaef59..ffd34b40f3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1692,6 +1692,12 @@ parameters: count: 5 path: src/Type/TypeCombinator.php + - + message: '#^Doing instanceof PHPStan\\Type\\Enum\\EnumCaseObjectType is error\-prone and deprecated\. Use Type\:\:getEnumCases\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/TypeCombinator.php + - message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' identifier: phpstanApi.instanceofType diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 37ae13258f..ae2d46101a 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -18,6 +18,7 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateArrayType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; @@ -539,7 +540,7 @@ private static function unionWithSubtractedType( return $type; } - if ($type instanceof SubtractableType) { + if ($type instanceof SubtractableType && ! $type instanceof EnumCaseObjectType) { $subtractedType = $type->getSubtractedType() === null ? $subtractedType : self::union($type->getSubtractedType(), $subtractedType); @@ -595,7 +596,7 @@ private static function intersectWithSubtractedType( } $subtractedType = self::union(...$subtractedTypes); - } elseif ($b instanceof SubtractableType) { + } elseif ($b instanceof SubtractableType && ! $b instanceof EnumCaseObjectType) { $subtractedType = $b->getSubtractedType(); if ($subtractedType === null) { return $a->getTypeWithoutSubtractedType(); 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..0b92126f56 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -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()), From 0c8fd6a4b4c339d20de3c3c6eb5e5b1b3b89709f Mon Sep 17 00:00:00 2001 From: schlndh Date: Sat, 29 Mar 2025 11:16:23 +0100 Subject: [PATCH 2/2] improve EnumCaseObjectType subtracting --- phpstan-baseline.neon | 6 ---- src/Type/Enum/EnumCaseObjectType.php | 9 +++-- src/Type/TypeCombinator.php | 35 +++++++++++++------ .../Analyser/LegacyNodeScopeResolverTest.php | 4 +-- tests/PHPStan/Type/TypeCombinatorTest.php | 4 +-- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ffd34b40f3..d252aaef59 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1692,12 +1692,6 @@ parameters: count: 5 path: src/Type/TypeCombinator.php - - - message: '#^Doing instanceof PHPStan\\Type\\Enum\\EnumCaseObjectType is error\-prone and deprecated\. Use Type\:\:getEnumCases\(\) instead\.$#' - identifier: phpstanApi.instanceofType - count: 2 - path: src/Type/TypeCombinator.php - - message: '#^Doing instanceof PHPStan\\Type\\FloatType is error\-prone and deprecated\. Use Type\:\:isFloat\(\) instead\.$#' identifier: phpstanApi.instanceofType 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 ae2d46101a..29b57c1593 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -18,7 +18,6 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateArrayType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; @@ -540,7 +539,7 @@ private static function unionWithSubtractedType( return $type; } - if ($type instanceof SubtractableType && ! $type instanceof EnumCaseObjectType) { + if ($type instanceof SubtractableType) { $subtractedType = $type->getSubtractedType() === null ? $subtractedType : self::union($type->getSubtractedType(), $subtractedType); @@ -596,17 +595,31 @@ private static function intersectWithSubtractedType( } $subtractedType = self::union(...$subtractedTypes); - } elseif ($b instanceof SubtractableType && ! $b instanceof EnumCaseObjectType) { - $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/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 0b92126f56..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', ], [ [