Skip to content

Commit

Permalink
Tagged unions
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jul 26, 2022
1 parent 3034ae5 commit 33ff2b2
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 1 deletion.
8 changes: 7 additions & 1 deletion src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1096,12 +1096,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;
}
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-1.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-2.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-3.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/tagged-unions.php');
}

/**
Expand Down
181 changes: 181 additions & 0 deletions tests/PHPStan/Analyser/data/tagged-unions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

namespace TaggedUnions;

use function PHPStan\Testing\assertType;

class Foo
{

/** @param array{A: int}|array{A: string} $foo */
public function doFoo(array $foo)
{
assertType('array{A: int}|array{A: string}', $foo);
if (is_int($foo['A'])) {
assertType("array{A: int}", $foo);
$foo['B'] = 'yo';
assertType("array{A: int, B: 'yo'}", $foo);
} else {
assertType('array{A: string}', $foo);
}

assertType("array{A: int, B: 'yo'}|array{A: string}", $foo);
}

/** @param array{A: int, B: 1}|array{A: string, B: 2} $foo */
public function doFoo2(array $foo)
{
assertType('array{A: int, B: 1}|array{A: string, B: 2}', $foo);
if (is_int($foo['A'])) {
assertType("array{A: int, B: 1}", $foo);
} else {
assertType("array{A: string, B: 2}", $foo);
}

assertType('array{A: int, B: 1}|array{A: string, B: 2}', $foo);
}

/** @param array{A: int, B: 1}|array{A: string, C: 1} $foo */
public function doFoo3(array $foo)
{
assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
if (is_int($foo['A'])) {
assertType("array{A: int, B: 1}", $foo);
} else {
assertType("array{A: string, C: 1}", $foo);
}

assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
}

/** @param array{A: int, B: 1}|array{A: string, C: 1} $foo */
public function doFoo4(array $foo)
{
assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
if (isset($foo['C'])) {
assertType("array{A: string, C: 1}", $foo);
} else {
assertType("array{A: int, B: 1}|array{A: string, C: 1}", $foo); // could be array{A: int, B: 1}
}

assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo);
}

/**
* @param array{A: int}|array{A: int|string} $foo
* @return void
*/
public function doBar(array $foo)
{
assertType('array{A: int|string}', $foo);
}

}

/**
* @phpstan-type Topping string
* @phpstan-type Salsa string
*
* @phpstan-type Pizza array{type: 'pizza', toppings: Topping[]}
* @phpstan-type Pasta array{type: 'pasta', salsa: Salsa}
* @phpstan-type Meal Pizza|Pasta
*/
class Test
{
/**
* @param Meal $meal
*/
function test($meal): void {
assertType("array{type: 'pasta', salsa: string}|array{type: 'pizza', toppings: array<string>}", $meal);
if ($meal['type'] === 'pizza') {
assertType("array{type: 'pizza', toppings: array<string>}", $meal);
} else {
assertType("array{type: 'pasta', salsa: string}", $meal);
}
assertType("array{type: 'pasta', salsa: string}|array{type: 'pizza', toppings: array<string>}", $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);
}

}
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -712,4 +712,15 @@ public function testBug4117(): void
$this->analyse([__DIR__ . '/data/bug-4117.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,
],
]);
}

}
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/Methods/data/tagged-unions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace TaggedUnionReturnCheck;

class HelloWorld
{
/**
* @return array{updated: true, id: int}|array{updated: false, id: null}
*/
public function sayHello(): array
{
return ['updated' => false, 'id' => 5];
}
}

0 comments on commit 33ff2b2

Please sign in to comment.