Skip to content

Commit bd8fca8

Browse files
committed
Add dynamic return type extension for Psl\Type\union()
1 parent 138b048 commit bd8fca8

4 files changed

Lines changed: 101 additions & 0 deletions

File tree

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ services:
1414
tags:
1515
- phpstan.broker.dynamicFunctionReturnTypeExtension
1616

17+
-
18+
class: Psl\PHPStan\Type\TypeUnionReturnTypeExtension
19+
tags:
20+
- phpstan.broker.dynamicFunctionReturnTypeExtension
21+
1722
-
1823
class: Psl\PHPStan\Type\AssertTypeSpecifyingExtension
1924
tags:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Psl\PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
9+
use PHPStan\Type\ErrorType;
10+
use PHPStan\Type\Generic\GenericObjectType;
11+
use PHPStan\Type\Type;
12+
use PHPStan\Type\TypeCombinator;
13+
use Psl\Type\TypeInterface;
14+
15+
class TypeUnionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
16+
{
17+
18+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
19+
{
20+
return $functionReflection->getName() === 'Psl\Type\union';
21+
}
22+
23+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
24+
{
25+
$args = $functionCall->getArgs();
26+
if ($args === []) {
27+
return null;
28+
}
29+
30+
$innerTypes = [];
31+
foreach ($args as $arg) {
32+
$argType = $scope->getType($arg->value);
33+
$inner = $argType->getTemplateType(TypeInterface::class, 'T');
34+
if ($inner instanceof ErrorType) {
35+
return null;
36+
}
37+
$innerTypes[] = $inner;
38+
}
39+
40+
return new GenericObjectType(
41+
TypeInterface::class,
42+
[
43+
TypeCombinator::union(...$innerTypes),
44+
]
45+
);
46+
}
47+
48+
}

tests/Type/PslTypeSpecifyingExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function dataFileAsserts(): iterable
2828
yield from $this->gatherAssertTypes(__DIR__ . '/data/complexTypev1.php');
2929
} else {
3030
yield from $this->gatherAssertTypes(__DIR__ . '/data/complexTypev2.php');
31+
yield from $this->gatherAssertTypes(__DIR__ . '/data/unionV2.php');
3132
}
3233
}
3334

tests/Type/data/unionV2.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PslUnionV2Test;
4+
5+
use Psl\Type;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* For PSL >= 2.0.0 (literal_scalar)
11+
*/
12+
class UnionTypes
13+
{
14+
public function literalScalarUnion($input): void
15+
{
16+
$ab = Type\union(Type\literal_scalar('a'), Type\literal_scalar('b'));
17+
$out = $ab->coerce($input);
18+
assertType("'a'|'b'", $out);
19+
}
20+
21+
public function mixedUnion($input): void
22+
{
23+
$intOrString = Type\union(Type\int(), Type\string());
24+
$out = $intOrString->coerce($input);
25+
assertType('int|string', $out);
26+
}
27+
28+
public function shapeWithLiteralUnion($input): void
29+
{
30+
$shape = Type\shape([
31+
'kind' => Type\union(
32+
Type\literal_scalar('a'),
33+
Type\literal_scalar('b'),
34+
Type\literal_scalar('c'),
35+
),
36+
]);
37+
$out = $shape->coerce($input);
38+
assertType("array{kind: 'a'|'b'|'c'}", $out);
39+
}
40+
41+
public function singleArgUnion($input): void
42+
{
43+
$single = Type\union(Type\int());
44+
$out = $single->coerce($input);
45+
assertType('int', $out);
46+
}
47+
}

0 commit comments

Comments
 (0)