From 14d3d5abe1afd7cf1a19a366567a7e83a050c63d Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sun, 5 Dec 2021 11:07:33 +0100 Subject: [PATCH] Rewrote #901 so that `ReflectionUnionType` is kept internally for union types When an union type of `T|null` or `?T` is met, BetterReflection will (from now on) keep a `ReflectionUnionType` internally, while exposing a `ReflectionNamedType` with `ReflectionNamedType#allowsNull() === true` only at adapter level. While this is a BC break, it leads to a much cleaner API around handling `null` types, and inspecting types for type analysis. Ref: https://github.com/Roave/BetterReflection/pull/902#discussion_r762529382 Ref: https://github.com/Roave/BackwardCompatibilityCheck/pull/324 --- src/Reflection/Adapter/ReflectionEnum.php | 2 +- .../Adapter/ReflectionNamedType.php | 7 +- src/Reflection/Adapter/ReflectionType.php | 20 +++++- src/Reflection/ReflectionIntersectionType.php | 7 +- src/Reflection/ReflectionNamedType.php | 15 ++-- src/Reflection/ReflectionType.php | 36 ++++------ src/Reflection/ReflectionUnionType.php | 14 +++- .../ReflectionFunctionStringCast.php | 8 ++- .../StringCast/ReflectionMethodStringCast.php | 8 ++- .../ReflectionParameterStringCast.php | 6 +- .../ReflectionPropertyStringCast.php | 2 +- .../StringCast/ReflectionTypeStringCast.php | 42 +++++++++++ .../Reflection/Adapter/ReflectionEnumTest.php | 4 +- .../Adapter/ReflectionNamedTypeTest.php | 22 +++++- .../Reflection/Adapter/ReflectionTypeTest.php | 54 ++++++--------- test/unit/Reflection/ReflectionClassTest.php | 4 +- .../ReflectionFunctionAbstractTest.php | 6 +- .../ReflectionIntersectionTypeTest.php | 1 + .../Reflection/ReflectionNamedTypeTest.php | 9 +-- .../Reflection/ReflectionPropertyTest.php | 4 +- test/unit/Reflection/ReflectionTypeTest.php | 25 +++++-- .../Reflection/ReflectionUnionTypeTest.php | 9 +-- .../ReflectionTypeStringCastTest.php | 69 +++++++++++++++++++ .../PhpStormStubsSourceStubberTest.php | 16 ++--- .../ReflectionSourceStubberTest.php | 17 ++++- 25 files changed, 292 insertions(+), 115 deletions(-) create mode 100644 src/Reflection/StringCast/ReflectionTypeStringCast.php create mode 100644 test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php diff --git a/src/Reflection/Adapter/ReflectionEnum.php b/src/Reflection/Adapter/ReflectionEnum.php index 0bd109b50..da58d4f39 100644 --- a/src/Reflection/Adapter/ReflectionEnum.php +++ b/src/Reflection/Adapter/ReflectionEnum.php @@ -509,7 +509,7 @@ public function isBacked(): bool public function getBackingType(): ?ReflectionNamedType { if ($this->betterReflectionEnum->isBacked()) { - return new ReflectionNamedType($this->betterReflectionEnum->getBackingType()); + return new ReflectionNamedType($this->betterReflectionEnum->getBackingType(), false); } return null; diff --git a/src/Reflection/Adapter/ReflectionNamedType.php b/src/Reflection/Adapter/ReflectionNamedType.php index 313f87262..62c9dc4d2 100644 --- a/src/Reflection/Adapter/ReflectionNamedType.php +++ b/src/Reflection/Adapter/ReflectionNamedType.php @@ -12,7 +12,7 @@ */ final class ReflectionNamedType extends CoreReflectionNamedType { - public function __construct(private BetterReflectionNamedType $betterReflectionType) + public function __construct(private BetterReflectionNamedType $betterReflectionType, private bool $allowsNull) { } @@ -23,12 +23,13 @@ public function getName(): string public function __toString(): string { - return $this->betterReflectionType->__toString(); + return ($this->allowsNull ? '?' : '') + . $this->betterReflectionType->__toString(); } public function allowsNull(): bool { - return $this->betterReflectionType->allowsNull(); + return $this->allowsNull; } public function isBuiltin(): bool diff --git a/src/Reflection/Adapter/ReflectionType.php b/src/Reflection/Adapter/ReflectionType.php index d37e35e2a..12d39f411 100644 --- a/src/Reflection/Adapter/ReflectionType.php +++ b/src/Reflection/Adapter/ReflectionType.php @@ -9,6 +9,10 @@ use Roave\BetterReflection\Reflection\ReflectionNamedType as BetterReflectionNamedType; use Roave\BetterReflection\Reflection\ReflectionUnionType as BetterReflectionUnionType; +use function array_filter; +use function array_values; +use function count; + abstract class ReflectionType extends CoreReflectionType { public static function fromTypeOrNull(BetterReflectionNamedType|BetterReflectionUnionType|BetterReflectionIntersectionType|null $betterReflectionType): ReflectionUnionType|ReflectionNamedType|ReflectionIntersectionType|null @@ -18,6 +22,20 @@ public static function fromTypeOrNull(BetterReflectionNamedType|BetterReflection } if ($betterReflectionType instanceof BetterReflectionUnionType) { + // php-src has this weird behavior where a union type composed of a single type `T` + // together with `null` means that a `ReflectionNamedType` for `?T` is produced, + // rather than `T|null`. This is done to keep BC compatibility with PHP 7.1 (which + // introduced nullable types), but at reflection level, this is mostly a nuisance. + // In order to keep parity with core, we stashed this weird behavior in here. + $nonNullTypes = array_values(array_filter( + $betterReflectionType->getTypes(), + static fn (BetterReflectionNamedType $type): bool => $type->getName() !== 'null', + )); + + if ($betterReflectionType->allowsNull() && count($nonNullTypes) === 1) { + return new ReflectionNamedType($nonNullTypes[0], true); + } + return new ReflectionUnionType($betterReflectionType); } @@ -25,6 +43,6 @@ public static function fromTypeOrNull(BetterReflectionNamedType|BetterReflection return new ReflectionIntersectionType($betterReflectionType); } - return new ReflectionNamedType($betterReflectionType); + return new ReflectionNamedType($betterReflectionType, false); } } diff --git a/src/Reflection/ReflectionIntersectionType.php b/src/Reflection/ReflectionIntersectionType.php index a2fd467f8..b51f7aaca 100644 --- a/src/Reflection/ReflectionIntersectionType.php +++ b/src/Reflection/ReflectionIntersectionType.php @@ -22,7 +22,7 @@ public function __construct( ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner, IntersectionType $type, ) { - parent::__construct($reflector, $owner, false); + parent::__construct($reflector, $owner); $this->types = array_filter( array_map(static fn (Node\Identifier|Node\Name $type): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType => ReflectionType::createFromNode($reflector, $owner, $type), $type->types), @@ -38,6 +38,11 @@ public function getTypes(): array return $this->types; } + public function allowsNull(): bool + { + return false; + } + public function __toString(): string { return implode('&', array_map(static fn (ReflectionNamedType $type): string => $type->__toString(), $this->types)); diff --git a/src/Reflection/ReflectionNamedType.php b/src/Reflection/ReflectionNamedType.php index 9bfdf2ec1..276cbf033 100644 --- a/src/Reflection/ReflectionNamedType.php +++ b/src/Reflection/ReflectionNamedType.php @@ -40,9 +40,8 @@ public function __construct( Reflector $reflector, ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner, Identifier|Name $type, - bool $allowsNull, ) { - parent::__construct($reflector, $owner, $allowsNull); + parent::__construct($reflector, $owner); $this->name = $type->toString(); } @@ -102,13 +101,13 @@ public function getClass(): ReflectionClass throw new LogicException(sprintf('The type %s cannot be resolved to class', $this->name)); } - public function __toString(): string + public function allowsNull(): bool { - $name = ''; - if ($this->allowsNull()) { - $name .= '?'; - } + return false; + } - return $name . $this->getName(); + public function __toString(): string + { + return $this->getName(); } } diff --git a/src/Reflection/ReflectionType.php b/src/Reflection/ReflectionType.php index df32d74e6..781f3a5a9 100644 --- a/src/Reflection/ReflectionType.php +++ b/src/Reflection/ReflectionType.php @@ -11,16 +11,11 @@ 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( protected Reflector $reflector, protected ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner, - private bool $allowsNull, ) { } @@ -31,41 +26,36 @@ public static function createFromNode( Reflector $reflector, ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner, Identifier|Name|NullableType|UnionType|IntersectionType $type, - bool $forceAllowsNull = false, + bool $allowsNull = false, ): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType { - $allowsNull = $forceAllowsNull; if ($type instanceof NullableType) { $type = $type->type; $allowsNull = true; } if ($type instanceof Identifier || $type instanceof Name) { - return new ReflectionNamedType($reflector, $owner, $type, $allowsNull); + if ($allowsNull) { + return new ReflectionUnionType( + $reflector, + $owner, + new UnionType([$type, new Identifier('null')]), + ); + } + + return new ReflectionNamedType($reflector, $owner, $type); } if ($type instanceof IntersectionType) { 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); + return new ReflectionUnionType($reflector, $owner, $type); } /** - * Does the parameter allow null? + * Does the type allow null? */ - public function allowsNull(): bool - { - return $this->allowsNull; - } + abstract public function allowsNull(): bool; /** * Convert this string type to a string diff --git a/src/Reflection/ReflectionUnionType.php b/src/Reflection/ReflectionUnionType.php index e170f63d5..0c1589406 100644 --- a/src/Reflection/ReflectionUnionType.php +++ b/src/Reflection/ReflectionUnionType.php @@ -21,9 +21,8 @@ public function __construct( Reflector $reflector, ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner, UnionType $type, - bool $allowsNull, ) { - parent::__construct($reflector, $owner, $allowsNull); + parent::__construct($reflector, $owner); $this->types = array_filter( array_map(static fn (Node\Identifier|Node\Name $type): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType => ReflectionType::createFromNode($reflector, $owner, $type), $type->types), @@ -39,6 +38,17 @@ public function getTypes(): array return $this->types; } + public function allowsNull(): bool + { + foreach ($this->types as $type) { + if ($type->getName() === 'null') { + return true; + } + } + + return false; + } + public function __toString(): string { return implode('|', array_map(static fn (ReflectionType $type): string => $type->__toString(), $this->types)); diff --git a/src/Reflection/StringCast/ReflectionFunctionStringCast.php b/src/Reflection/StringCast/ReflectionFunctionStringCast.php index c2e706d5b..efc82e487 100644 --- a/src/Reflection/StringCast/ReflectionFunctionStringCast.php +++ b/src/Reflection/StringCast/ReflectionFunctionStringCast.php @@ -72,6 +72,12 @@ private static function parametersToString(ReflectionFunction $functionReflectio private static function returnTypeToString(ReflectionFunction $methodReflection): string { - return $methodReflection->getReturnType()?->__toString() ?? ''; + $type = $methodReflection->getReturnType(); + + if ($type === null) { + return ''; + } + + return ReflectionTypeStringCast::toString($type); } } diff --git a/src/Reflection/StringCast/ReflectionMethodStringCast.php b/src/Reflection/StringCast/ReflectionMethodStringCast.php index db6d11971..d2669a3e8 100644 --- a/src/Reflection/StringCast/ReflectionMethodStringCast.php +++ b/src/Reflection/StringCast/ReflectionMethodStringCast.php @@ -126,6 +126,12 @@ private static function parametersToString(ReflectionMethod $methodReflection): private static function returnTypeToString(ReflectionMethod $methodReflection): string { - return $methodReflection->getReturnType()?->__toString() ?? ''; + $type = $methodReflection->getReturnType(); + + if ($type === null) { + return ''; + } + + return ReflectionTypeStringCast::toString($type); } } diff --git a/src/Reflection/StringCast/ReflectionParameterStringCast.php b/src/Reflection/StringCast/ReflectionParameterStringCast.php index e69187f0b..1e1f696c2 100644 --- a/src/Reflection/StringCast/ReflectionParameterStringCast.php +++ b/src/Reflection/StringCast/ReflectionParameterStringCast.php @@ -34,11 +34,13 @@ public static function toString(ReflectionParameter $parameterReflection): strin private static function typeToString(ReflectionParameter $parameterReflection): string { - if (! $parameterReflection->hasType()) { + $type = $parameterReflection->getType(); + + if ($type === null) { return ''; } - return (string) $parameterReflection->getType() . ' '; + return ReflectionTypeStringCast::toString($type) . ' '; } private static function valueToString(ReflectionParameter $parameterReflection): string diff --git a/src/Reflection/StringCast/ReflectionPropertyStringCast.php b/src/Reflection/StringCast/ReflectionPropertyStringCast.php index 125f42e87..68a25583a 100644 --- a/src/Reflection/StringCast/ReflectionPropertyStringCast.php +++ b/src/Reflection/StringCast/ReflectionPropertyStringCast.php @@ -29,7 +29,7 @@ public static function toString(ReflectionProperty $propertyReflection): string self::visibilityToString($propertyReflection), $propertyReflection->isStatic() ? ' static' : '', $propertyReflection->isReadOnly() ? ' readonly' : '', - $type !== null ? sprintf(' %s', $type->__toString()) : '', + $type !== null ? sprintf(' %s', ReflectionTypeStringCast::toString($type)) : '', $propertyReflection->getName(), ); } diff --git a/src/Reflection/StringCast/ReflectionTypeStringCast.php b/src/Reflection/StringCast/ReflectionTypeStringCast.php new file mode 100644 index 000000000..3ad37465b --- /dev/null +++ b/src/Reflection/StringCast/ReflectionTypeStringCast.php @@ -0,0 +1,42 @@ +getTypes(), + static fn (ReflectionNamedType $type): bool => $type->getName() !== 'null', + )); + + if ($type->allowsNull() && count($nonNullTypes) === 1) { + return '?' . $nonNullTypes[0]->__toString(); + } + } + + return $type->__toString(); + } +} diff --git a/test/unit/Reflection/Adapter/ReflectionEnumTest.php b/test/unit/Reflection/Adapter/ReflectionEnumTest.php index a1bcac607..8a65b557e 100644 --- a/test/unit/Reflection/Adapter/ReflectionEnumTest.php +++ b/test/unit/Reflection/Adapter/ReflectionEnumTest.php @@ -648,8 +648,10 @@ public function testGetBackingTypeForBackedEnum(): void ->willReturn($betterReflectionNamedType); $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $backingType = $reflectionEnumAdapter->getBackingType(); - self::assertInstanceOf(ReflectionNamedTypeAdapter::class, $reflectionEnumAdapter->getBackingType()); + self::assertInstanceOf(ReflectionNamedTypeAdapter::class, $backingType); + self::assertFalse($backingType->allowsNull()); } public function testHasConstantWithEnumCase(): void diff --git a/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php b/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php index 79f08f041..b54beaeda 100644 --- a/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php +++ b/test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php @@ -37,11 +37,27 @@ public function testCoreReflectionMethods(string $methodName): void self::assertSame(ReflectionNamedTypeAdapter::class, $reflectionTypeAdapterReflection->getMethod($methodName)->getDeclaringClass()->getName()); } + public function testWillRenderNullabilityMarkerWhenGiven(): void + { + $reflectionStub = $this->createMock(BetterReflectionNamedType::class); + $reflectionStub->method('__toString') + ->willReturn('foo'); + + self::assertSame('foo', (new ReflectionNamedTypeAdapter($reflectionStub, false))->__toString()); + self::assertSame('?foo', (new ReflectionNamedTypeAdapter($reflectionStub, true))->__toString()); + } + + public function testWillReportThatItAcceptsOrRejectsNull(): void + { + $reflectionStub = $this->createMock(BetterReflectionNamedType::class); + + self::assertFalse((new ReflectionNamedTypeAdapter($reflectionStub, false))->allowsNull()); + self::assertTrue((new ReflectionNamedTypeAdapter($reflectionStub, true))->allowsNull()); + } + public function methodExpectationProvider(): array { return [ - ['__toString', null, 'int', []], - ['allowsNull', null, true, []], ['isBuiltin', null, true, []], ['getName', null, 'int', []], ]; @@ -67,7 +83,7 @@ public function testAdapterMethods(string $methodName, ?string $expectedExceptio $this->expectException($expectedException); } - $adapter = new ReflectionNamedTypeAdapter($reflectionStub); + $adapter = new ReflectionNamedTypeAdapter($reflectionStub, false); $adapter->{$methodName}(...$args); } } diff --git a/test/unit/Reflection/Adapter/ReflectionTypeTest.php b/test/unit/Reflection/Adapter/ReflectionTypeTest.php index 40cc869bf..af9cf2783 100644 --- a/test/unit/Reflection/Adapter/ReflectionTypeTest.php +++ b/test/unit/Reflection/Adapter/ReflectionTypeTest.php @@ -42,38 +42,6 @@ public function testCoreReflectionMethods(string $methodName): void self::assertSame(ReflectionNamedTypeAdapter::class, $reflectionTypeAdapterReflection->getMethod($methodName)->getDeclaringClass()->getName()); } - public function methodExpectationProvider(): array - { - return [ - ['__toString', null, '', []], - ['allowsNull', null, true, []], - ]; - } - - /** - * @param list $args - * - * @dataProvider methodExpectationProvider - */ - public function testAdapterMethods(string $methodName, ?string $expectedException, mixed $returnValue, array $args): void - { - $reflectionStub = $this->createMock(BetterReflectionNamedType::class); - - if ($expectedException === null) { - $reflectionStub->expects($this->once()) - ->method($methodName) - ->with(...$args) - ->will($this->returnValue($returnValue)); - } - - if ($expectedException !== null) { - $this->expectException($expectedException); - } - - $adapter = ReflectionTypeAdapter::fromTypeOrNull($reflectionStub); - $adapter->{$methodName}(...$args); - } - public function testFromTypeOrNullWithNull(): void { self::assertNull(ReflectionTypeAdapter::fromTypeOrNull(null)); @@ -84,6 +52,28 @@ public function testFromTypeOrNullWithNamedType(): void self::assertInstanceOf(ReflectionNamedTypeAdapter::class, ReflectionTypeAdapter::fromTypeOrNull($this->createMock(BetterReflectionNamedType::class))); } + public function testWillMakeNullableNamedTypeOutOfNullableUnionWithOnlyOneType(): void + { + $unionType = $this->createMock(BetterReflectionUnionType::class); + $fooType = $this->createMock(BetterReflectionNamedType::class); + $nullType = $this->createMock(BetterReflectionNamedType::class); + + $fooType->method('getName') + ->willReturn('foo'); + $nullType->method('getName') + ->willReturn('null'); + $unionType->method('getTypes') + ->willReturn([$fooType, $nullType]); + $unionType->method('allowsNull') + ->willReturn(true); + + $type = ReflectionTypeAdapter::fromTypeOrNull($unionType); + + self::assertInstanceOf(ReflectionNamedTypeAdapter::class, $type); + self::assertTrue($type->allowsNull()); + self::assertSame('foo', $type->getName()); + } + public function testFromTypeOrNullWithUnionType(): void { self::assertInstanceOf(ReflectionUnionTypeAdapter::class, ReflectionTypeAdapter::fromTypeOrNull($this->createMock(BetterReflectionUnionType::class))); diff --git a/test/unit/Reflection/ReflectionClassTest.php b/test/unit/Reflection/ReflectionClassTest.php index 9c1409f2a..0f723ec1b 100644 --- a/test/unit/Reflection/ReflectionClassTest.php +++ b/test/unit/Reflection/ReflectionClassTest.php @@ -240,8 +240,8 @@ public function dataMethodsOfBackedEnum(): array [ 'tryFrom', ['value' => [ReflectionUnionType::class, 'string|int']], - ReflectionNamedType::class, - '?static', + ReflectionUnionType::class, + 'static|null', ], ]; } diff --git a/test/unit/Reflection/ReflectionFunctionAbstractTest.php b/test/unit/Reflection/ReflectionFunctionAbstractTest.php index b34a7d658..34286e17d 100644 --- a/test/unit/Reflection/ReflectionFunctionAbstractTest.php +++ b/test/unit/Reflection/ReflectionFunctionAbstractTest.php @@ -456,9 +456,9 @@ public function testHasReturnTypeWhenTypeIsNotDeclared(): void public function nullableReturnTypeFunctionProvider(): array { return [ - ['returnsNullableInt', '?int'], - ['returnsNullableString', '?string'], - ['returnsNullableObject', '?' . stdClass::class], + ['returnsNullableInt', 'int|null'], + ['returnsNullableString', 'string|null'], + ['returnsNullableObject', stdClass::class . '|null'], ]; } diff --git a/test/unit/Reflection/ReflectionIntersectionTypeTest.php b/test/unit/Reflection/ReflectionIntersectionTypeTest.php index d902405c8..fb4df0f07 100644 --- a/test/unit/Reflection/ReflectionIntersectionTypeTest.php +++ b/test/unit/Reflection/ReflectionIntersectionTypeTest.php @@ -44,5 +44,6 @@ public function test(Node\IntersectionType $intersectionType, string $expectedSt self::assertContainsOnlyInstancesOf(ReflectionNamedType::class, $typeReflection->getTypes()); self::assertSame($expectedString, $typeReflection->__toString()); + self::assertFalse($typeReflection->allowsNull()); } } diff --git a/test/unit/Reflection/ReflectionNamedTypeTest.php b/test/unit/Reflection/ReflectionNamedTypeTest.php index b0f45b0cc..0227d43f3 100644 --- a/test/unit/Reflection/ReflectionNamedTypeTest.php +++ b/test/unit/Reflection/ReflectionNamedTypeTest.php @@ -49,9 +49,6 @@ public function testAllowsNull(): void { $noNullType = $this->createType('string'); self::assertFalse($noNullType->allowsNull()); - - $allowsNullType = $this->createType('string', true); - self::assertTrue($allowsNullType->allowsNull()); } public function isBuildinProvider(): Generator @@ -96,7 +93,6 @@ public function testIsNotBuiltin(string $type): void public function testImplicitCastToString(): void { self::assertSame('int', (string) $this->createType('int')); - self::assertSame('?int', (string) $this->createType('int', true)); self::assertSame('string', (string) $this->createType('string')); self::assertSame('array', (string) $this->createType('array')); self::assertSame('callable', (string) $this->createType('callable')); @@ -110,12 +106,11 @@ public function testImplicitCastToString(): void self::assertSame('Foo\Bar\Baz', (string) $this->createType('Foo\Bar\Baz')); self::assertSame('\Foo\Bar\Baz', (string) $this->createType('\Foo\Bar\Baz')); - self::assertSame('?\Foo\Bar\Baz', (string) $this->createType('\Foo\Bar\Baz', true)); } - private function createType(string $type, bool $allowsNull = false): ReflectionNamedType + private function createType(string $type): ReflectionNamedType { - return new ReflectionNamedType($this->reflector, $this->owner, new Identifier($type), $allowsNull); + return new ReflectionNamedType($this->reflector, $this->owner, new Identifier($type)); } public function testGetClassFromPropertyType(): void diff --git a/test/unit/Reflection/ReflectionPropertyTest.php b/test/unit/Reflection/ReflectionPropertyTest.php index 6b7b0152b..d3c03c96d 100644 --- a/test/unit/Reflection/ReflectionPropertyTest.php +++ b/test/unit/Reflection/ReflectionPropertyTest.php @@ -195,7 +195,7 @@ public function testIsPromoted(): void self::assertTrue($promotedProperty->isPromoted()); self::assertTrue($promotedProperty->isPrivate()); self::assertTrue($promotedProperty->hasType()); - self::assertSame('?int', $promotedProperty->getType()->__toString()); + self::assertSame('int|null', $promotedProperty->getType()->__toString()); self::assertFalse($promotedProperty->hasDefaultValue()); self::assertNull($promotedProperty->getDefaultValue()); self::assertSame(46, $promotedProperty->getStartLine()); @@ -675,7 +675,7 @@ public function getTypeProvider(): array ['integerProperty', 'int'], ['classProperty', 'stdClass'], ['noTypeProperty', ''], - ['nullableStringProperty', '?string'], + ['nullableStringProperty', 'string|null'], ['arrayProperty', 'array'], ]; } diff --git a/test/unit/Reflection/ReflectionTypeTest.php b/test/unit/Reflection/ReflectionTypeTest.php index bf40c3b5d..78bb6d3cc 100644 --- a/test/unit/Reflection/ReflectionTypeTest.php +++ b/test/unit/Reflection/ReflectionTypeTest.php @@ -15,6 +15,9 @@ /** * @covers \Roave\BetterReflection\Reflection\ReflectionType + * @covers \Roave\BetterReflection\Reflection\ReflectionNamedType + * @covers \Roave\BetterReflection\Reflection\ReflectionIntersectionType + * @covers \Roave\BetterReflection\Reflection\ReflectionUnionType */ class ReflectionTypeTest extends TestCase { @@ -34,21 +37,31 @@ public function dataProvider(): array return [ [new Node\Name('A'), false, ReflectionNamedType::class, false], [new Node\Identifier('string'), false, ReflectionNamedType::class, false], - [new Node\Identifier('string'), true, ReflectionNamedType::class, true], - [new Node\NullableType(new Node\Identifier('string')), false, ReflectionNamedType::class, true], + 'Forcing a type to be nullable turns it into a `T|null` ReflectionUnionType' => [ + new Node\Identifier('string'), + true, + ReflectionUnionType::class, + true, + ], + 'Nullable types are converted into `T|null` ReflectionUnionType instances' => [ + new Node\NullableType(new Node\Identifier('string')), + false, + ReflectionUnionType::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' => [ + 'Union types composed of just `null` and a type are kept as `T|null` ReflectionUnionType' => [ new Node\UnionType([new Node\Name('A'), new Node\Name('null')]), false, - ReflectionNamedType::class, + ReflectionUnionType::class, true, ], - 'Union types composed of `null` and more than one type are kept as ReflectionUnionType' => [ + 'Union types composed of `null` and more than one type are kept as `T|U|null` ReflectionUnionType' => [ new Node\UnionType([new Node\Name('A'), new Node\Name('B'), new Node\Name('null')]), false, ReflectionUnionType::class, - false, + true, ], ]; } diff --git a/test/unit/Reflection/ReflectionUnionTypeTest.php b/test/unit/Reflection/ReflectionUnionTypeTest.php index 1d30b75f9..294006a13 100644 --- a/test/unit/Reflection/ReflectionUnionTypeTest.php +++ b/test/unit/Reflection/ReflectionUnionTypeTest.php @@ -30,19 +30,20 @@ protected function setUp(): void public function dataProvider(): array { return [ - [new Node\UnionType([new Node\Name('\A\Foo'), new Node\Name('Boo')]), '\A\Foo|Boo'], - [new Node\UnionType([new Node\Name('A'), new Node\Name('B'), new Node\Identifier('null')]), 'A|B|null'], + [new Node\UnionType([new Node\Name('\A\Foo'), new Node\Name('Boo')]), '\A\Foo|Boo', false], + [new Node\UnionType([new Node\Name('A'), new Node\Name('B'), new Node\Identifier('null')]), 'A|B|null', true], ]; } /** * @dataProvider dataProvider */ - public function test(Node\UnionType $unionType, string $expectedString): void + public function test(Node\UnionType $unionType, string $expectedString, bool $expectedNullable): void { - $typeReflection = new ReflectionUnionType($this->reflector, $this->owner, $unionType, false); + $typeReflection = new ReflectionUnionType($this->reflector, $this->owner, $unionType); self::assertContainsOnlyInstancesOf(ReflectionNamedType::class, $typeReflection->getTypes()); self::assertSame($expectedString, $typeReflection->__toString()); + self::assertSame($expectedNullable, $typeReflection->allowsNull()); } } diff --git a/test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php b/test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php new file mode 100644 index 000000000..7f67bccf1 --- /dev/null +++ b/test/unit/Reflection/StringCast/ReflectionTypeStringCastTest.php @@ -0,0 +1,69 @@ +astLocator(), + )); + + $returnTypeForFunction = static function (string $function) use ($reflector): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType { + $type = $reflector->reflectFunction($function) + ->getReturnType(); + + assert($type !== null); + + return $type; + }; + + return [ + [$returnTypeForFunction('a'), 'int|string|null'], + [$returnTypeForFunction('b'), '?int'], + [$returnTypeForFunction('c'), '?int'], + [$returnTypeForFunction('d'), 'A&B'], + [$returnTypeForFunction('e'), 'int'], + ]; + } + + /** + * @dataProvider toStringProvider + */ + public function testToString( + ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType $type, + string $expectedString, + ): void { + self::assertSame($expectedString, ReflectionTypeStringCast::toString($type)); + } +} diff --git a/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php b/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php index cce2e25df..e38cb0e89 100644 --- a/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php +++ b/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php @@ -761,12 +761,12 @@ public function dataMethodInPhpVersion(): array [CoreReflectionProperty::class, 'getType', 70300, false], [CoreReflectionProperty::class, 'getType', 70400, true], [CoreReflectionProperty::class, 'getType', 80000, true], - [CoreReflectionProperty::class, 'getType', 80100, true, null, '?ReflectionType'], + [CoreReflectionProperty::class, 'getType', 80100, true, null, 'ReflectionType|null'], [CoreReflectionClass::class, 'export', 70400, true], [CoreReflectionClass::class, 'export', 80000, false], [DatePeriod::class, 'getRecurrences', 70216, false], [DatePeriod::class, 'getRecurrences', 70217, true], - [DatePeriod::class, 'getRecurrences', 80100, true, null, '?int'], + [DatePeriod::class, 'getRecurrences', 80100, true, null, 'int|null'], [DateTimeInterface::class, 'getOffset', 79999, true], [DateTimeInterface::class, 'getOffset', 80000, true], [DateTimeInterface::class, 'getOffset', 80100, true, null, 'int'], @@ -816,7 +816,7 @@ public function dataMethodParameterInPhpVersion(): array { return [ ['mysqli_stmt', 'execute', 'params', 80099, false], - ['mysqli_stmt', 'execute', 'params', 80100, true, '?array', true], + ['mysqli_stmt', 'execute', 'params', 80100, true, 'array|null', true], ['PDOStatement', 'fetchAll', 'fetch_argument', 50299, false], ['PDOStatement', 'fetchAll', 'fetch_argument', 50300, true, null, true], ['PDOStatement', 'fetchAll', 'fetch_argument', 70499, true, null, true], @@ -862,11 +862,11 @@ public function dataPropertyInPhpVersion(): array [DateInterval::class, 'f', 70099, false], [DateInterval::class, 'f', 70100, true], [PDOException::class, 'errorInfo', 80099, true], - [PDOException::class, 'errorInfo', 80100, true, '?array'], + [PDOException::class, 'errorInfo', 80100, true, 'array|null'], [DOMNode::class, 'nodeType', 80099, true], [DOMNode::class, 'nodeType', 80100, true, 'int'], [DOMNode::class, 'parentNode', 80099, true], - [DOMNode::class, 'parentNode', 80100, true, '?DOMNode'], + [DOMNode::class, 'parentNode', 80100, true, 'DOMNode|null'], ]; } @@ -910,7 +910,7 @@ public function dataFunctionInPhpVersion(): array ['read_exif_data', 80000, false], ['spl_autoload_functions', 79999, true, 'array|false'], ['spl_autoload_functions', 80000, true, 'array'], - ['dom_import_simplexml', 70000, true, '?DOMElement'], + ['dom_import_simplexml', 70000, true, 'DOMElement|null'], ['dom_import_simplexml', 80000, true, 'DOMElement'], // Not core functions ['newrelic_add_custom_parameter', 40000, true, 'bool'], @@ -943,8 +943,8 @@ public function dataFunctionParameterInPhpVersion(): array return [ ['bcscale', 'scale', 70200, true, 'int', false], ['bcscale', 'scale', 70299, true, 'int', false], - ['bcscale', 'scale', 70300, true, '?int', true], - ['bcscale', 'scale', 80000, true, '?int', true], + ['bcscale', 'scale', 70300, true, 'int|null', true], + ['bcscale', 'scale', 80000, true, 'int|null', true], ['easter_date', 'mode', 79999, false], ['easter_date', 'mode', 80000, true, 'int', false], ['curl_version', 'age', 50200, false], diff --git a/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php b/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php index f7b5b5232..78e691c64 100644 --- a/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php +++ b/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php @@ -11,6 +11,7 @@ use ReflectionFunction as CoreReflectionFunction; use ReflectionMethod as CoreReflectionMethod; use ReflectionParameter as CoreReflectionParameter; +use Roave\BetterReflection\Reflection\Adapter\ReflectionType; use Roave\BetterReflection\Reflection\ReflectionClass; use Roave\BetterReflection\Reflection\ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionParameter; @@ -370,7 +371,11 @@ private function assertSameMethodAttributes(CoreReflectionMethod $original, Refl } self::assertSame($original->hasReturnType(), $stubbed->hasReturnType(), $original->getName()); - self::assertSame((string) $original->getReturnType(), (string) $stubbed->getReturnType(), $original->getName()); + self::assertSame( + (string) $original->getReturnType(), + (string) ReflectionType::fromTypeOrNull($stubbed->getReturnType()), + $original->getName(), + ); } private function assertSameParameterAttributes( @@ -442,10 +447,16 @@ public function testInternalFunctionsReturnType(string $functionName): void if (method_exists($originalReflection, 'hasTentativeReturnType') && $originalReflection->hasTentativeReturnType()) { self::assertSame($originalReflection->hasTentativeReturnType(), $stubbedReflection->hasTentativeReturnType()); - self::assertSame((string) $originalReflection->getTentativeReturnType(), (string) $stubbedReflection->getTentativeReturnType()); + self::assertSame( + (string) $originalReflection->getTentativeReturnType(), + (string) ReflectionType::fromTypeOrNull($stubbedReflection->getTentativeReturnType()), + ); } else { self::assertSame($originalReflection->hasReturnType(), $stubbedReflection->hasReturnType()); - self::assertSame((string) $originalReflection->getReturnType(), (string) $stubbedReflection->getReturnType()); + self::assertSame( + (string) $originalReflection->getReturnType(), + (string) ReflectionType::fromTypeOrNull($stubbedReflection->getReturnType()), + ); } }