From c861f483a4662728e92e6f364fbb004093231931 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sun, 5 Dec 2021 09:07:07 +0100 Subject: [PATCH] Ensure that simple union types like `A|null` yield a nullable `ReflectionNamedType` Fixes #901 ## Context Currently, when running reflection on an `A|null` type, BetterReflection produces a `Roave\BetterReflection\Reflection\ReflectionUnionType`: ```php var_dump( get_class( (new DefaultReflector(new StringSourceLocator( <<<'PHP' astLocator() ))) ->reflectClass('AClass') ->getProperty('typed') ->getType() ) ); ``` produces ``` string(53) "Roave\BetterReflection\Reflection\ReflectionUnionType" ``` In PHP-SRC, this behavior is different: https://3v4l.org/gMA4T#v8.1rc3 ```php getType()); var_dump((new ReflectionParameter([Implementation::class, 'bar'], 0))->getType()); ``` produces: ``` object(ReflectionNamedType)#2 (0) { } object(ReflectionUnionType)#1 (0) { } ``` This means that a `UnionType` AST node composed of just `null` plus another type should be converted into a `ReflectionNamedType`, for the sake of compatibility with upstream (this patch does that). This is ugly, but will (for now) avoid some bad issues in downstream handling (presently blocking https://github.com/Roave/BackwardCompatibilityCheck/pull/324 ) --- src/Reflection/ReflectionType.php | 13 +++++++++++++ test/unit/Reflection/ReflectionTypeTest.php | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/Reflection/ReflectionType.php b/src/Reflection/ReflectionType.php index 55c84b9b9..df32d74e6 100644 --- a/src/Reflection/ReflectionType.php +++ b/src/Reflection/ReflectionType.php @@ -11,6 +11,10 @@ use PhpParser\Node\UnionType; use Roave\BetterReflection\Reflector\Reflector; +use function array_filter; +use function array_values; +use function count; + abstract class ReflectionType { protected function __construct( @@ -43,6 +47,15 @@ public static function createFromNode( return new ReflectionIntersectionType($reflector, $owner, $type); } + $nonNullTypes = array_values(array_filter( + $type->types, + static fn (Identifier|Name $type): bool => $type->toString() !== 'null', + )); + + if (count($nonNullTypes) === 1) { + return self::createFromNode($reflector, $owner, $nonNullTypes[0], true); + } + return new ReflectionUnionType($reflector, $owner, $type, $allowsNull); } diff --git a/test/unit/Reflection/ReflectionTypeTest.php b/test/unit/Reflection/ReflectionTypeTest.php index 3642cedbf..bf40c3b5d 100644 --- a/test/unit/Reflection/ReflectionTypeTest.php +++ b/test/unit/Reflection/ReflectionTypeTest.php @@ -38,6 +38,18 @@ public function dataProvider(): array [new Node\NullableType(new Node\Identifier('string')), false, ReflectionNamedType::class, true], [new Node\IntersectionType([new Node\Name('A'), new Node\Name('B')]), false, ReflectionIntersectionType::class, false], [new Node\UnionType([new Node\Name('A'), new Node\Name('B')]), false, ReflectionUnionType::class, false], + 'Union types composed of just `null` and a type are simplified into a ReflectionNamedType' => [ + new Node\UnionType([new Node\Name('A'), new Node\Name('null')]), + false, + ReflectionNamedType::class, + true, + ], + 'Union types composed of `null` and more than one type are kept as ReflectionUnionType' => [ + new Node\UnionType([new Node\Name('A'), new Node\Name('B'), new Node\Name('null')]), + false, + ReflectionUnionType::class, + false, + ], ]; }