Skip to content

Commit

Permalink
Merge pull request #902 from Roave/feature/#901-ensure-that-simple-un…
Browse files Browse the repository at this point in the history
…ion-types-are-turned-into-nullable-reflection-named-types

Ensure that simple union types like `A|null` yield a nullable `ReflectionNamedType`
  • Loading branch information
Ocramius authored Dec 5, 2021
2 parents ff18794 + 33a5645 commit 2b64de3
Show file tree
Hide file tree
Showing 26 changed files with 305 additions and 107 deletions.
2 changes: 1 addition & 1 deletion src/Reflection/Adapter/ReflectionEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/Reflection/Adapter/ReflectionNamedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/
final class ReflectionNamedType extends CoreReflectionNamedType
{
public function __construct(private BetterReflectionNamedType $betterReflectionType)
public function __construct(private BetterReflectionNamedType $betterReflectionType, private bool $allowsNull)
{
}

Expand All @@ -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
Expand Down
20 changes: 19 additions & 1 deletion src/Reflection/Adapter/ReflectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,13 +22,27 @@ 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);
}

if ($betterReflectionType instanceof BetterReflectionIntersectionType) {
return new ReflectionIntersectionType($betterReflectionType);
}

return new ReflectionNamedType($betterReflectionType);
return new ReflectionNamedType($betterReflectionType, false);
}
}
7 changes: 6 additions & 1 deletion src/Reflection/ReflectionIntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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));
Expand Down
15 changes: 7 additions & 8 deletions src/Reflection/ReflectionNamedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
}
23 changes: 13 additions & 10 deletions src/Reflection/ReflectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ abstract class ReflectionType
protected function __construct(
protected Reflector $reflector,
protected ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner,
private bool $allowsNull,
) {
}

Expand All @@ -27,32 +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);
}

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
Expand Down
14 changes: 12 additions & 2 deletions src/Reflection/ReflectionUnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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));
Expand Down
8 changes: 7 additions & 1 deletion src/Reflection/StringCast/ReflectionFunctionStringCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
8 changes: 7 additions & 1 deletion src/Reflection/StringCast/ReflectionMethodStringCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
6 changes: 4 additions & 2 deletions src/Reflection/StringCast/ReflectionParameterStringCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/StringCast/ReflectionPropertyStringCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
}
Expand Down
42 changes: 42 additions & 0 deletions src/Reflection/StringCast/ReflectionTypeStringCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\Reflection\StringCast;

use Roave\BetterReflection\Reflection\ReflectionIntersectionType;
use Roave\BetterReflection\Reflection\ReflectionNamedType;
use Roave\BetterReflection\Reflection\ReflectionUnionType;

use function array_filter;
use function array_values;
use function count;

/**
* @internal
*/
final class ReflectionTypeStringCast
{
public static function toString(
ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType $type,
): string {
if ($type instanceof ReflectionUnionType) {
// 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 `Reflector#__toString()` behavior, we stashed
// this weird behavior in here.
$nonNullTypes = array_values(array_filter(
$type->getTypes(),
static fn (ReflectionNamedType $type): bool => $type->getName() !== 'null',
));

if ($type->allowsNull() && count($nonNullTypes) === 1) {
return '?' . $nonNullTypes[0]->__toString();
}
}

return $type->__toString();
}
}
16 changes: 4 additions & 12 deletions test/compat/ResolvePHP7Types.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,28 @@ $functionInfo = $reflector->reflectFunction('myFunction');
$returnType = $functionInfo->getReturnType();

var_dump([
'builtIn' => $returnType->isBuiltin(),
'type' => $returnType->__toString(),
]);

array_map(function (\Roave\BetterReflection\Reflection\ReflectionParameter $param) {
$type = $param->getType();

var_dump([
'builtIn' => $type->isBuiltin(),
'type' => $type->__toString(),
]);
}, $functionInfo->getParameters());

?>
--EXPECTF--
array(2) {
["builtIn"]=>
bool(true)
array(1) {
["type"]=>
string(4) "bool"
}
array(2) {
["builtIn"]=>
bool(true)
array(1) {
["type"]=>
string(3) "int"
}
array(2) {
["builtIn"]=>
bool(true)
array(1) {
["type"]=>
string(7) "?string"
string(11) "string|null"
}
4 changes: 3 additions & 1 deletion test/unit/Reflection/Adapter/ReflectionEnumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions test/unit/Reflection/Adapter/ReflectionNamedTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', []],
];
Expand All @@ -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);
}
}
Loading

0 comments on commit 2b64de3

Please sign in to comment.