diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index 2a71b8647c4..41c5035c198 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -29,6 +29,16 @@ public function filterTypes(callable $filterCb): Type return $result; } + public function tryRemove(Type $typeToRemove): ?Type + { + $result = parent::tryRemove($typeToRemove); + if ($result === null) { + return null; + } + + return TypeUtils::toBenevolentUnion($result); + } + public function describe(VerbosityLevel $level): string { return '(' . parent::describe($level) . ')'; diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 694dd1d61af..e98669868e7 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -105,12 +105,12 @@ public function isArgument(): bool public function toArgument(): TemplateType { - return new self( + return TemplateTypeFactory::create( $this->scope, - new TemplateTypeArgumentStrategy(), - $this->variance, $this->name, TemplateTypeHelper::toArgument($this->getBound()), + $this->variance, + new TemplateTypeArgumentStrategy(), $this->default !== null ? TemplateTypeHelper::toArgument($this->default) : null, ); } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 7c1bdaecbcd..ee20b9a8494 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -1317,7 +1317,38 @@ public function traverseSimultaneously(Type $right, callable $cb): Type public function tryRemove(Type $typeToRemove): ?Type { - return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); + $innerTypes = []; + $changed = false; + foreach ($this->types as $innerType) { + $removed = TypeCombinator::remove($innerType, $typeToRemove); + if (!$removed->equals($innerType)) { + $changed = true; + } + if ($removed instanceof NeverType) { + continue; + } + if ($removed instanceof self && !$removed instanceof TemplateType) { + foreach ($removed->getTypes() as $removedInnerType) { + $innerTypes[] = $removedInnerType; + } + } else { + $innerTypes[] = $removed; + } + } + + if (!$changed) { + return null; + } + + if (count($innerTypes) === 0) { + return new NeverType(); + } + + if (count($innerTypes) === 1) { + return $innerTypes[0]; + } + + return new UnionType($innerTypes); } public function exponentiate(Type $exponent): Type diff --git a/tests/PHPStan/Analyser/nsrt/finite-types.php b/tests/PHPStan/Analyser/nsrt/finite-types.php index c3e7e719a4e..bbcac17de8a 100644 --- a/tests/PHPStan/Analyser/nsrt/finite-types.php +++ b/tests/PHPStan/Analyser/nsrt/finite-types.php @@ -21,7 +21,7 @@ public function doFoo(bool $a, bool $b): void return; } - assertType('array{bool, true}', $array); + assertType('array{false, true}|array{true, true}', $array); if ($array === [true, true]) { assertType('array{true, true}', $array); return; diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 7ab3586dfec..4bf279f1433 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -470,6 +470,18 @@ public function testBug12790(): void $this->analyse([__DIR__ . '/data/bug-12790.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testBug10128(): void + { + $this->analyse([__DIR__ . '/data/bug-10128.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug11453(): void + { + $this->analyse([__DIR__ . '/data/bug-11453.php'], []); + } + #[RequiresPhp('>= 8.0.0')] public function testBug11310(): void { diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10128.php b/tests/PHPStan/Rules/Comparison/data/bug-10128.php new file mode 100644 index 00000000000..2b3402ae56f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10128.php @@ -0,0 +1,84 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug10128; + +enum FooBar { + case Bar; + case Baz; + case Foo; +} + +function getFoo(): FooBar { + return FooBar::Bar; +} + +function test(): void { + $first = getFoo(); + $second = getFoo(); + + $type = match ([$first, $second]) { + [FooBar::Bar, FooBar::Bar] => 1, + [FooBar::Bar, FooBar::Baz] => 2, + [FooBar::Bar, FooBar::Foo] => 3, + + [FooBar::Baz, FooBar::Bar] => 1, + [FooBar::Baz, FooBar::Baz] => 2, + [FooBar::Baz, FooBar::Foo] => 3, + + [FooBar::Foo, FooBar::Bar] => 1, + [FooBar::Foo, FooBar::Baz] => 2, + [FooBar::Foo, FooBar::Foo] => 3, + }; + + $type2 = match ([$first, $second]) { + [FooBar::Bar, FooBar::Bar], + [FooBar::Bar, FooBar::Baz], + [FooBar::Bar, FooBar::Foo] => 1, + + [FooBar::Baz, FooBar::Bar] => 1, + [FooBar::Baz, FooBar::Baz] => 2, + [FooBar::Baz, FooBar::Foo] => 3, + + [FooBar::Foo, FooBar::Bar] => 1, + [FooBar::Foo, FooBar::Baz] => 2, + [FooBar::Foo, FooBar::Foo] => 3, + }; + + $type3 = match ([$first, $second]) { + [FooBar::Bar, FooBar::Bar], + [FooBar::Baz, FooBar::Baz], + [FooBar::Foo, FooBar::Foo], + [FooBar::Foo, FooBar::Baz] => 1, + + [FooBar::Bar, FooBar::Baz], + [FooBar::Baz, FooBar::Bar], + [FooBar::Bar, FooBar::Foo], + [FooBar::Foo, FooBar::Bar] => 2, + + [FooBar::Baz, FooBar::Foo] => 3, + }; +} + +enum TwoCases { + case Foo; + case Bar; +} + +function getTwoCases(): TwoCases { + return TwoCases::Foo; +} + +function testTwoCases(): void { + $first = getTwoCases(); + $second = getTwoCases(); + + $type = match ([$first, $second]) { + [TwoCases::Foo, TwoCases::Foo] => 1, + [TwoCases::Bar, TwoCases::Bar] => 1, + + [TwoCases::Bar, TwoCases::Foo] => 2, + [TwoCases::Foo, TwoCases::Bar] => 2, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11453.php b/tests/PHPStan/Rules/Comparison/data/bug-11453.php new file mode 100644 index 00000000000..0c9cbfdeee0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11453.php @@ -0,0 +1,34 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11453; + +/** @return 1|2|3 */ +function getInt(): int { + return 1; +} + +/** @return 'a'|'b'|'c' */ +function getString(): string { + return 'a'; +} + +function test(): void { + $int = getInt(); + $string = getString(); + + $type = match ([$int, $string]) { + [1, 'a'] => 'one-a', + [1, 'b'] => 'one-b', + [1, 'c'] => 'one-c', + + [2, 'a'] => 'two-a', + [2, 'b'] => 'two-b', + [2, 'c'] => 'two-c', + + [3, 'a'] => 'three-a', + [3, 'b'] => 'three-b', + [3, 'c'] => 'three-c', + }; +}