Skip to content

Commit

Permalink
Rewrote #901 so that ReflectionUnionType is kept internally for uni…
Browse files Browse the repository at this point in the history
…on 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: #902 (comment)
Ref: Roave/BackwardCompatibilityCheck#324
  • Loading branch information
Ocramius committed Dec 5, 2021
1 parent 0d78168 commit 14d3d5a
Show file tree
Hide file tree
Showing 25 changed files with 292 additions and 115 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();
}
}
36 changes: 13 additions & 23 deletions src/Reflection/ReflectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}

Expand All @@ -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
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();
}
}
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 14d3d5a

Please sign in to comment.