Skip to content

Commit

Permalink
Type, Reflection: supports intersection types
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Sep 20, 2021
1 parent f168ed9 commit 9cd8039
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 24 deletions.
5 changes: 5 additions & 0 deletions ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
$parameters->set('skip', [
'fixtures*/*',

'tests/Utils/Reflection.getParameterType.81.phpt',
'tests/Utils/Reflection.getPropertyType.81.phpt',
'tests/Utils/Reflection.getReturnType.81.phpt',
'tests/Utils/Type.fromReflection.function.81.phpt',

// RemoteStream extends streamWrapper
PHP_CodeSniffer\Standards\PSR1\Sniffs\Methods\CamelCapsMethodNameSniff::class => [
'tests/Utils/FileSystem.phpt',
Expand Down
10 changes: 5 additions & 5 deletions src/Utils/Reflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static function isBuiltinType(string $type): bool
/**
* Returns the type of return value of given function or method and normalizes `self`, `static`, and `parent` to actual class names.
* If the function does not have a return type, it returns null.
* If the function has union type, it throws Nette\InvalidStateException.
* If the function has union or intersection type, it throws Nette\InvalidStateException.
*/
public static function getReturnType(\ReflectionFunctionAbstract $func): ?string
{
Expand All @@ -60,7 +60,7 @@ public static function getReturnTypes(\ReflectionFunctionAbstract $func): array
/**
* Returns the type of given parameter and normalizes `self` and `parent` to the actual class names.
* If the parameter does not have a type, it returns null.
* If the parameter has union type, it throws Nette\InvalidStateException.
* If the parameter has union or intersection type, it throws Nette\InvalidStateException.
*/
public static function getParameterType(\ReflectionParameter $param): ?string
{
Expand All @@ -81,7 +81,7 @@ public static function getParameterTypes(\ReflectionParameter $param): array
/**
* Returns the type of given property and normalizes `self` and `parent` to the actual class names.
* If the property does not have a type, it returns null.
* If the property has union type, it throws Nette\InvalidStateException.
* If the property has union or intersection type, it throws Nette\InvalidStateException.
*/
public static function getPropertyType(\ReflectionProperty $prop): ?string
{
Expand Down Expand Up @@ -110,8 +110,8 @@ private static function getType($reflection, ?\ReflectionType $type): ?string
} elseif ($type instanceof \ReflectionNamedType) {
return Type::resolve($type->getName(), $reflection);

} elseif ($type instanceof \ReflectionUnionType) {
throw new Nette\InvalidStateException('The ' . self::toString($reflection) . ' is not expected to have a union type.');
} elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
throw new Nette\InvalidStateException('The ' . self::toString($reflection) . ' is not expected to have a union or intersection type.');

} else {
throw new Nette\InvalidStateException('Unexpected type of ' . self::toString($reflection));
Expand Down
56 changes: 47 additions & 9 deletions src/Utils/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ final class Type
/** @var bool */
private $single;

/** @var string |, & */
private $kind;


/**
* Creates a Type object based on reflection. Resolves self, static and parent to the actual class name.
Expand All @@ -48,12 +51,13 @@ public static function fromReflection($reflection): ?self
$name = self::resolve($type->getName(), $reflection);
return new self($type->allowsNull() && $type->getName() !== 'mixed' ? [$name, 'null'] : [$name]);

} elseif ($type instanceof \ReflectionUnionType) {
} elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
return new self(
array_map(
function ($t) use ($reflection) { return self::resolve($t->getName(), $reflection); },
$type->getTypes()
)
),
$type instanceof \ReflectionUnionType ? '|' : '&'
);

} else {
Expand All @@ -67,11 +71,17 @@ function ($t) use ($reflection) { return self::resolve($t->getName(), $reflectio
*/
public static function fromString(string $type): self
{
if (!preg_match('#(?:\?([\w\\\\]+)|[\w\\\\]+(?:\|[\w\\\\]+)*)$#AD', $type, $m)) {
if (!preg_match('#(?:
\?([\w\\\\]+)|
[\w\\\\]+ (?: (&[\w\\\\]+)* | (\|[\w\\\\]+)* )
)()$#xAD', $type, $m)) {
throw new Nette\InvalidArgumentException("Invalid type '$type'.");
}
if (isset($m[1])) {
return new self([$m[1], 'null']);
[, $nType, $iType] = $m;
if ($nType) {
return new self([$nType, 'null']);
} elseif ($iType) {
return new self(explode('&', $type), '&');
} else {
return new self(explode('|', $type));
}
Expand All @@ -97,21 +107,22 @@ public static function resolve(string $type, $reflection): string
}


private function __construct(array $types)
private function __construct(array $types, string $kind = '|')
{
if ($types[0] === 'null') { // null as last
array_push($types, array_shift($types));
}
$this->types = $types;
$this->single = ($types[1] ?? 'null') === 'null';
$this->kind = count($types) > 1 ? $kind : '';
}


public function __toString(): string
{
return $this->single
? (count($this->types) > 1 ? '?' : '') . $this->types[0]
: implode('|', $this->types);
: implode($this->kind, $this->types);
}


Expand Down Expand Up @@ -151,7 +162,16 @@ public function getSingleName(): ?string
*/
public function isUnion(): bool
{
return count($this->types) > 1;
return $this->kind === '|';
}


/**
* Returns true whether it is an intersection type.
*/
public function isIntersection(): bool
{
return $this->kind === '&';
}


Expand Down Expand Up @@ -190,7 +210,25 @@ public function allows(string $type): bool
if ($this->types === ['mixed']) {
return true;
}
return Arrays::every((self::fromString($type))->types, function ($testedType) {

$type = self::fromString($type);

if ($this->isIntersection()) {
if (!$type->isIntersection()) {
return false;
}
return Arrays::every($this->types, function ($currentType) use ($type) {
$builtin = Reflection::isBuiltinType($currentType);
return Arrays::some($type->types, function ($testedType) use ($currentType, $builtin) {
return $builtin
? strcasecmp($currentType, $testedType) === 0
: is_a($testedType, $currentType, true);
});
});
}

$method = $type->isIntersection() ? 'some' : 'every';
return Arrays::$method($type->types, function ($testedType) {
$builtin = Reflection::isBuiltinType($testedType);
return Arrays::some($this->types, function ($currentType) use ($testedType, $builtin) {
return $builtin
Expand Down
4 changes: 2 additions & 2 deletions tests/Utils/Reflection.getParameterType.80.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ Assert::same(['A', 'array', 'null'], Reflection::getParameterTypes($params[9]));

Assert::exception(function () use ($params) {
Reflection::getParameterType($params[8]);
}, Nette\InvalidStateException::class, 'The $union in A::method() is not expected to have a union type.');
}, Nette\InvalidStateException::class, 'The $union in A::method() is not expected to have a union or intersection type.');

Assert::exception(function () use ($params) {
Reflection::getParameterType($params[9]);
}, Nette\InvalidStateException::class, 'The $nullableUnion in A::method() is not expected to have a union type.');
}, Nette\InvalidStateException::class, 'The $nullableUnion in A::method() is not expected to have a union or intersection type.');


$method = new ReflectionMethod('AExt', 'methodExt');
Expand Down
74 changes: 74 additions & 0 deletions tests/Utils/Reflection.getParameterType.81.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/**
* Test: Nette\Utils\Reflection::getParameterType
* @phpversion 8.1
*/

declare(strict_types=1);

use Nette\Utils\Reflection;
use Test\B; // for testing purposes
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


class A
{
public function method(
Undeclared $undeclared,
B $b,
array $array,
callable $callable,
self $self,
$none,
?B $nullable,
mixed $mixed,
array|self $union,
array|self|null $nullableUnion,
AExt&A $intersection,
) {
}
}

class AExt extends A
{
public function methodExt(parent $parent)
{
}
}

$method = new ReflectionMethod('A', 'method');
$params = $method->getParameters();

Assert::same('Undeclared', Reflection::getParameterType($params[0]));
Assert::same('Test\B', Reflection::getParameterType($params[1]));
Assert::same('array', Reflection::getParameterType($params[2]));
Assert::same('callable', Reflection::getParameterType($params[3]));
Assert::same('A', Reflection::getParameterType($params[4]));
Assert::null(Reflection::getParameterType($params[5]));
Assert::same('Test\B', Reflection::getParameterType($params[6]));
Assert::same(['Test\B', 'null'], Reflection::getParameterTypes($params[6]));
Assert::same('mixed', Reflection::getParameterType($params[7]));
Assert::same(['mixed'], Reflection::getParameterTypes($params[7]));
Assert::same(['A', 'array'], Reflection::getParameterTypes($params[8]));
Assert::same(['A', 'array', 'null'], Reflection::getParameterTypes($params[9]));

Assert::exception(function () use ($params) {
Reflection::getParameterType($params[8]);
}, Nette\InvalidStateException::class, 'The $union in A::method() is not expected to have a union or intersection type.');

Assert::exception(function () use ($params) {
Reflection::getParameterType($params[9]);
}, Nette\InvalidStateException::class, 'The $nullableUnion in A::method() is not expected to have a union or intersection type.');

Assert::exception(function () use ($params) {
Reflection::getParameterType($params[10]);
}, Nette\InvalidStateException::class, 'The $intersection in A::method() is not expected to have a union or intersection type.');

$method = new ReflectionMethod('AExt', 'methodExt');
$params = $method->getParameters();

Assert::same('A', Reflection::getParameterType($params[0]));
4 changes: 2 additions & 2 deletions tests/Utils/Reflection.getPropertyType.80.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ Assert::same(['A', 'array', 'null'], Reflection::getPropertyTypes($props[8]));

Assert::exception(function () use ($props) {
Reflection::getPropertyType($props[7]);
}, Nette\InvalidStateException::class, 'The A::$union is not expected to have a union type.');
}, Nette\InvalidStateException::class, 'The A::$union is not expected to have a union or intersection type.');

Assert::exception(function () use ($props) {
Reflection::getPropertyType($props[8]);
}, Nette\InvalidStateException::class, 'The A::$nullableUnion is not expected to have a union type.');
}, Nette\InvalidStateException::class, 'The A::$nullableUnion is not expected to have a union or intersection type.');

$class = new ReflectionClass('AExt');
$props = $class->getProperties();
Expand Down
67 changes: 67 additions & 0 deletions tests/Utils/Reflection.getPropertyType.81.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/**
* Test: Nette\Utils\Reflection::getPropertyType
* @phpversion 8.1
*/

declare(strict_types=1);

use Nette\Utils\Reflection;
use Test\B; // for testing purposes
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


class A
{
public Undeclared $undeclared;
public B $b;
public array $array;
public self $self;
public $none;
public ?B $nullable;
public mixed $mixed;
public array|self $union;
public array|self|null $nullableUnion;
public AExt&A $intersection;
}

class AExt extends A
{
public parent $parent;
}

$class = new ReflectionClass('A');
$props = $class->getProperties();

Assert::same('Undeclared', Reflection::getPropertyType($props[0]));
Assert::same('Test\B', Reflection::getPropertyType($props[1]));
Assert::same('array', Reflection::getPropertyType($props[2]));
Assert::same('A', Reflection::getPropertyType($props[3]));
Assert::null(Reflection::getPropertyType($props[4]));
Assert::same('Test\B', Reflection::getPropertyType($props[5]));
Assert::same(['Test\B', 'null'], Reflection::getPropertyTypes($props[5]));
Assert::same('mixed', Reflection::getPropertyType($props[6]));
Assert::same(['mixed'], Reflection::getPropertyTypes($props[6]));
Assert::same(['A', 'array'], Reflection::getPropertyTypes($props[7]));
Assert::same(['A', 'array', 'null'], Reflection::getPropertyTypes($props[8]));

Assert::exception(function () use ($props) {
Reflection::getPropertyType($props[7]);
}, Nette\InvalidStateException::class, 'The A::$union is not expected to have a union or intersection type.');

Assert::exception(function () use ($props) {
Reflection::getPropertyType($props[8]);
}, Nette\InvalidStateException::class, 'The A::$nullableUnion is not expected to have a union or intersection type.');

Assert::exception(function () use ($props) {
Reflection::getPropertyType($props[9]);
}, Nette\InvalidStateException::class, 'The A::$intersection is not expected to have a union or intersection type.');

$class = new ReflectionClass('AExt');
$props = $class->getProperties();

Assert::same('A', Reflection::getPropertyType($props[0]));
6 changes: 3 additions & 3 deletions tests/Utils/Reflection.getReturnType.80.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ Assert::same(['A', 'array', 'null'], Reflection::getReturnTypes(new \ReflectionM

Assert::exception(function () {
Reflection::getReturnType(new \ReflectionMethod(A::class, 'unionType'));
}, Nette\InvalidStateException::class, 'The A::unionType() is not expected to have a union type.');
}, Nette\InvalidStateException::class, 'The A::unionType() is not expected to have a union or intersection type.');

Assert::exception(function () {
Reflection::getReturnType(new \ReflectionMethod(A::class, 'nullableUnionType'));
}, Nette\InvalidStateException::class, 'The A::nullableUnionType() is not expected to have a union type.');
}, Nette\InvalidStateException::class, 'The A::nullableUnionType() is not expected to have a union or intersection type.');

Assert::same('A', Reflection::getReturnType(new \ReflectionMethod(AExt::class, 'parentTypeExt')));

Expand All @@ -134,4 +134,4 @@ Assert::same(['A', 'array'], Reflection::getReturnTypes(new \ReflectionFunction(

Assert::exception(function () {
Reflection::getReturnType(new \ReflectionFunction('unionType'));
}, Nette\InvalidStateException::class, 'The unionType() is not expected to have a union type.');
}, Nette\InvalidStateException::class, 'The unionType() is not expected to have a union or intersection type.');
Loading

0 comments on commit 9cd8039

Please sign in to comment.