Skip to content

Commit 39f8909

Browse files
committed
Support for native union types
1 parent b9c29a5 commit 39f8909

7 files changed

Lines changed: 132 additions & 1 deletion

File tree

build/phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ parameters:
1717
- %rootDir%/tests/PHPStan/Analyser/traits/*
1818
- %rootDir%/tests/notAutoloaded/*
1919
- %rootDir%/tests/PHPStan/Generics/functions.php
20+
- %rootDir%/tests/PHPStan/Reflection/UnionTypesTest.php
2021
ignoreErrors:
2122
- '#^Dynamic call to static method PHPUnit\\Framework\\\S+\(\)\.$#'
2223
- '#should be contravariant with parameter \$node \(PhpParser\\Node\) of method PHPStan\\Rules\\Rule<PhpParser\\Node>::processNode\(\)$#'

conf/config.neon

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
parameters:
22
bootstrap: null
3-
bootstrapFiles: []
3+
bootstrapFiles:
4+
- ../stubs/runtime/ReflectionUnionType.php
45
excludes_analyse: []
56
autoload_directories: []
67
autoload_files: []

src/Type/TypehintHelper.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use PHPStan\Broker\Broker;
66
use PHPStan\Type\Generic\TemplateTypeHelper;
77
use ReflectionNamedType;
8+
use ReflectionType;
9+
use ReflectionUnionType;
810

911
class TypehintHelper
1012
{
@@ -62,6 +64,14 @@ public static function decideTypeFromReflection(
6264
return $phpDocType ?? new MixedType();
6365
}
6466

67+
if ($reflectionType instanceof ReflectionUnionType) {
68+
$type = TypeCombinator::union(...array_map(static function (ReflectionType $type) use ($selfClass): Type {
69+
return self::decideTypeFromReflection($type, null, $selfClass, false);
70+
}, $reflectionType->getTypes()));
71+
72+
return self::decideType($type, $phpDocType);
73+
}
74+
6575
if (!$reflectionType instanceof ReflectionNamedType) {
6676
throw new \PHPStan\ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType)));
6777
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
if (class_exists('ReflectionUnionType', false)) {
4+
return;
5+
}
6+
7+
class ReflectionUnionType extends ReflectionType
8+
{
9+
10+
/** @return ReflectionType[] */
11+
public function getTypes()
12+
{
13+
return [];
14+
}
15+
16+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9989,6 +9989,17 @@ public function dataGraphicsDrawReturnTypes(): array
99899989
return $this->gatherAssertTypes(__DIR__ . '/data/graphics-draw-return-types.php');
99909990
}
99919991

9992+
public function dataNativeUnionTypes(): array
9993+
{
9994+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
9995+
return [];
9996+
}
9997+
9998+
require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php';
9999+
10000+
return $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/unionTypes.php');
10001+
}
10002+
999210003
/**
999310004
* @dataProvider dataBug2574
999410005
* @dataProvider dataBug2577
@@ -10049,6 +10060,7 @@ public function dataGraphicsDrawReturnTypes(): array
1004910060
* @dataProvider dataOverrideVariableCertaintyInRootScope
1005010061
* @dataProvider dataBitwiseNot
1005110062
* @dataProvider dataGraphicsDrawReturnTypes
10063+
* @dataProvider dataNativeUnionTypes
1005210064
* @param string $assertType
1005310065
* @param string $file
1005410066
* @param mixed ...$args
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection;
4+
5+
use NativeUnionTypes\Foo;
6+
use PhpParser\Node\Name;
7+
use PHPStan\Testing\TestCase;
8+
use PHPStan\Type\UnionType;
9+
use PHPStan\Type\VerbosityLevel;
10+
11+
class UnionTypesTest extends TestCase
12+
{
13+
14+
public function testUnionTypes(): void
15+
{
16+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
17+
$this->markTestSkipped('Test requires PHP 8.0');
18+
}
19+
20+
require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php';
21+
22+
$reflectionProvider = $this->createBroker();
23+
$class = $reflectionProvider->getClass(Foo::class);
24+
$propertyType = $class->getNativeProperty('fooProp')->getNativeType();
25+
$this->assertInstanceOf(UnionType::class, $propertyType);
26+
$this->assertSame('bool|int', $propertyType->describe(VerbosityLevel::precise()));
27+
28+
$method = $class->getNativeMethod('doFoo');
29+
$methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants());
30+
$methodReturnType = $methodVariant->getReturnType();
31+
$this->assertInstanceOf(UnionType::class, $methodReturnType);
32+
$this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $methodReturnType->describe(VerbosityLevel::precise()));
33+
34+
$methodParameterType = $methodVariant->getParameters()[0]->getType();
35+
$this->assertInstanceOf(UnionType::class, $methodParameterType);
36+
$this->assertSame('bool|int', $methodParameterType->describe(VerbosityLevel::precise()));
37+
38+
$function = $reflectionProvider->getFunction(new Name('NativeUnionTypes\doFoo'), null);
39+
$functionVariant = ParametersAcceptorSelector::selectSingle($function->getVariants());
40+
$functionReturnType = $functionVariant->getReturnType();
41+
$this->assertInstanceOf(UnionType::class, $functionReturnType);
42+
$this->assertSame('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $functionReturnType->describe(VerbosityLevel::precise()));
43+
44+
$functionParameterType = $functionVariant->getParameters()[0]->getType();
45+
$this->assertInstanceOf(UnionType::class, $functionParameterType);
46+
$this->assertSame('bool|int', $functionParameterType->describe(VerbosityLevel::precise()));
47+
}
48+
49+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php // lint >= 8.0
2+
3+
namespace NativeUnionTypes;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
class Foo
8+
{
9+
10+
public int|bool $fooProp;
11+
12+
public function doFoo(int|bool $foo): self|Bar
13+
{
14+
assertType('bool|int', $foo);
15+
assertType('bool|int', $this->fooProp);
16+
}
17+
18+
}
19+
20+
class Bar
21+
{
22+
23+
}
24+
25+
function doFoo(int|bool $foo): Foo|Bar
26+
{
27+
assertType('bool|int', $foo);
28+
}
29+
30+
function (Foo $foo): void {
31+
assertType('bool|int', $foo->fooProp);
32+
assertType('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $foo->doFoo(1));
33+
assertType('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', doFoo(1));
34+
};
35+
36+
function (): void {
37+
$f = function (int|bool $foo): Foo|Bar {
38+
assertType('bool|int', $foo);
39+
};
40+
41+
assertType('NativeUnionTypes\\Bar|NativeUnionTypes\\Foo', $f(1));
42+
};

0 commit comments

Comments
 (0)