diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index f862bcbcfe..b1ab4e7507 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -1115,12 +1115,18 @@ public function isKeysSupersetOf(self $otherArray): bool } $otherKeys = $otherArray->keyTypes; - foreach ($this->keyTypes as $keyType) { + foreach ($this->keyTypes as $i => $keyType) { foreach ($otherArray->keyTypes as $j => $otherKeyType) { if (!$keyType->equals($otherKeyType)) { continue; } + $valueType = $this->valueTypes[$i]; + $otherValueType = $otherArray->valueTypes[$j]; + if ($valueType->isSuperTypeOf($otherValueType)->no()) { + continue; + } + unset($otherKeys[$j]); continue 2; } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 85d802ca25..f4c3b72642 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -962,6 +962,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5758.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-3931.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5223.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/tagged-unions.php'); } /** diff --git a/tests/PHPStan/Analyser/data/tagged-unions.php b/tests/PHPStan/Analyser/data/tagged-unions.php new file mode 100644 index 0000000000..49a86a7828 --- /dev/null +++ b/tests/PHPStan/Analyser/data/tagged-unions.php @@ -0,0 +1,181 @@ +}", $meal); + if ($meal['type'] === 'pizza') { + assertType("array{type: 'pizza', toppings: array}", $meal); + } else { + assertType("array{type: 'pasta', salsa: string}", $meal); + } + assertType("array{type: 'pasta', salsa: string}|array{type: 'pizza', toppings: array}", $meal); + } +} + +class HelloWorld +{ + /** + * @return array{updated: true, id: int}|array{updated: false, id: null} + */ + public function sayHello(): array + { + return ['updated' => false, 'id' => 5]; + } + + public function doFoo() + { + $x = $this->sayHello(); + assertType("array{updated: false, id: null}|array{updated: true, id: int}", $x); + if ($x['updated']) { + assertType('array{updated: true, id: int}', $x); + } + } +} + +/** + * @psalm-type A array{tag: 'A', foo: bool} + * @psalm-type B array{tag: 'B'} + */ +class X { + /** @psalm-param A|B $arr */ + public function ooo(array $arr): void { + assertType("array{tag: 'A', foo: bool}|array{tag: 'B'}", $arr); + if ($arr['tag'] === 'A') { + assertType("array{tag: 'A', foo: bool}", $arr); + } else { + assertType("array{tag: 'B'}", $arr); + } + assertType("array{tag: 'A', foo: bool}|array{tag: 'B'}", $arr); + } +} + +class TipsFromArnaud +{ + + // https://github.com/phpstan/phpstan/issues/7666#issuecomment-1191563801 + + /** + * @param array{a: int}|array{a: int} $a + */ + public function doFoo(array $a): void + { + assertType('array{a: int}', $a); + } + + /** + * @param array{a: int}|array{a: string} $a + */ + public function doFoo2(array $a): void + { + assertType('array{a: int|string}', $a); + } + + /** + * @param array{a: int, b: int}|array{a: string, b: string} $a + */ + public function doFoo3(array $a): void + { + assertType('array{a: int, b: int}|array{a: string, b: string}', $a); + } + + /** + * @param array{a: int, b: string}|array{a: string, b:string} $a + */ + public function doFoo4(array $a): void + { + assertType('array{a: int|string, b: string}', $a); + } + + /** + * @param array{a: int, b: string, c: string}|array{a: string, b: string, c: string} $a + */ + public function doFoo5(array $a): void + { + assertType('array{a: int|string, b: string, c: string}', $a); + } + +} diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index e9bd12226d..7c821bb087 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -724,4 +724,15 @@ public function testBug7511(): void $this->analyse([__DIR__ . '/data/bug-7511.php'], []); } + public function testTaggedUnions(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/tagged-unions.php'], [ + [ + 'Method TaggedUnionReturnCheck\HelloWorld::sayHello() should return array{updated: false, id: null}|array{updated: true, id: int} but returns array{updated: false, id: 5}.', + 12, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/tagged-unions.php b/tests/PHPStan/Rules/Methods/data/tagged-unions.php new file mode 100644 index 0000000000..1f76221d5a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/tagged-unions.php @@ -0,0 +1,14 @@ + false, 'id' => 5]; + } +}