diff --git a/ecs.php b/ecs.php index 601ed753d..d655ffcc4 100644 --- a/ecs.php +++ b/ecs.php @@ -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', diff --git a/src/Utils/Reflection.php b/src/Utils/Reflection.php index a06c1dfd1..0106a95ee 100644 --- a/src/Utils/Reflection.php +++ b/src/Utils/Reflection.php @@ -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 { @@ -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 { @@ -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 { @@ -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)); diff --git a/src/Utils/Type.php b/src/Utils/Type.php index af26c53df..49d5adbfc 100644 --- a/src/Utils/Type.php +++ b/src/Utils/Type.php @@ -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. @@ -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 { @@ -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)); } @@ -97,13 +107,14 @@ 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 : ''; } @@ -111,7 +122,7 @@ public function __toString(): string { return $this->single ? (count($this->types) > 1 ? '?' : '') . $this->types[0] - : implode('|', $this->types); + : implode($this->kind, $this->types); } @@ -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 === '&'; } @@ -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 diff --git a/tests/Utils/Reflection.getParameterType.80.phpt b/tests/Utils/Reflection.getParameterType.80.phpt index 7651ddda3..57261ac51 100644 --- a/tests/Utils/Reflection.getParameterType.80.phpt +++ b/tests/Utils/Reflection.getParameterType.80.phpt @@ -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'); diff --git a/tests/Utils/Reflection.getParameterType.81.phpt b/tests/Utils/Reflection.getParameterType.81.phpt new file mode 100644 index 000000000..fc6afab8e --- /dev/null +++ b/tests/Utils/Reflection.getParameterType.81.phpt @@ -0,0 +1,74 @@ +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])); diff --git a/tests/Utils/Reflection.getPropertyType.80.phpt b/tests/Utils/Reflection.getPropertyType.80.phpt index 3357ce8c8..00378ce66 100644 --- a/tests/Utils/Reflection.getPropertyType.80.phpt +++ b/tests/Utils/Reflection.getPropertyType.80.phpt @@ -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(); diff --git a/tests/Utils/Reflection.getPropertyType.81.phpt b/tests/Utils/Reflection.getPropertyType.81.phpt new file mode 100644 index 000000000..7e62bb60e --- /dev/null +++ b/tests/Utils/Reflection.getPropertyType.81.phpt @@ -0,0 +1,67 @@ +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])); diff --git a/tests/Utils/Reflection.getReturnType.80.phpt b/tests/Utils/Reflection.getReturnType.80.phpt index 5567daa0a..96c251ac9 100644 --- a/tests/Utils/Reflection.getReturnType.80.phpt +++ b/tests/Utils/Reflection.getReturnType.80.phpt @@ -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'))); @@ -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.'); diff --git a/tests/Utils/Reflection.getReturnType.81.phpt b/tests/Utils/Reflection.getReturnType.81.phpt index f40a405bc..024956f96 100644 --- a/tests/Utils/Reflection.getReturnType.81.phpt +++ b/tests/Utils/Reflection.getReturnType.81.phpt @@ -64,6 +64,11 @@ class A public function nullableUnionType(): array|self|null { } + + + public function intersectionType(): AExt&A + { + } } class AExt extends A @@ -94,6 +99,11 @@ function unionType(): array|A } +function intersectionType(): AExt&A +{ +} + + Assert::null(Reflection::getReturnType(new \ReflectionMethod(A::class, 'noType'))); Assert::same('Test\B', Reflection::getReturnType(new \ReflectionMethod(A::class, 'classType'))); @@ -116,11 +126,15 @@ 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::exception(function () { + Reflection::getReturnType(new \ReflectionMethod(A::class, 'intersectionType')); +}, Nette\InvalidStateException::class, 'The A::intersectionType() is not expected to have a union or intersection type.'); Assert::same('A', Reflection::getReturnType(new \ReflectionMethod(AExt::class, 'parentTypeExt'))); @@ -134,7 +148,11 @@ 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.'); + +Assert::exception(function () { + Reflection::getReturnType(new \ReflectionFunction('intersectionType')); +}, Nette\InvalidStateException::class, 'The intersectionType() is not expected to have a union or intersection type.'); // tentative type diff --git a/tests/Utils/Type.allows.phpt b/tests/Utils/Type.allows.phpt index e3fbf5a1a..8074c24e3 100644 --- a/tests/Utils/Type.allows.phpt +++ b/tests/Utils/Type.allows.phpt @@ -12,6 +12,10 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; +class Bar +{ +} + class Foo { } @@ -28,6 +32,7 @@ Assert::false($type->allows('string|null')); Assert::false($type->allows('Foo')); Assert::false($type->allows('FooChild')); Assert::false($type->allows('Foo|FooChild')); +Assert::false($type->allows('Foo&Bar')); $type = Type::fromString('string|null'); @@ -37,6 +42,7 @@ Assert::true($type->allows('string|null')); Assert::false($type->allows('Foo')); Assert::false($type->allows('FooChild')); Assert::false($type->allows('Foo|FooChild')); +Assert::false($type->allows('Foo&Bar')); $type = Type::fromString('string|Foo'); @@ -46,6 +52,7 @@ Assert::false($type->allows('string|null')); Assert::true($type->allows('Foo')); Assert::true($type->allows('FooChild')); Assert::true($type->allows('Foo|FooChild')); +Assert::true($type->allows('Foo&Bar')); $type = Type::fromString('mixed'); @@ -55,3 +62,18 @@ Assert::true($type->allows('string|null')); Assert::true($type->allows('Foo')); Assert::true($type->allows('FooChild')); Assert::true($type->allows('Foo|FooChild')); +Assert::true($type->allows('Foo&Bar')); + + +$type = Type::fromString('Bar&Foo'); +Assert::false($type->allows('string')); +Assert::false($type->allows('null')); +Assert::false($type->allows('Foo')); +Assert::false($type->allows('FooChild')); +Assert::true($type->allows('Foo&Bar')); +Assert::true($type->allows('FooChild&Bar')); +Assert::true($type->allows('Foo&Bar&Baz')); + + +$type = Type::fromString('Bar&FooChild'); +Assert::false($type->allows('Foo&Bar')); diff --git a/tests/Utils/Type.fromReflection.function.81.phpt b/tests/Utils/Type.fromReflection.function.81.phpt index 3e69db4d4..c2888f1b7 100644 --- a/tests/Utils/Type.fromReflection.function.81.phpt +++ b/tests/Utils/Type.fromReflection.function.81.phpt @@ -43,6 +43,12 @@ Assert::same(['mixed'], $type->getNames()); Assert::same('mixed', (string) $type); +$type = Type::fromReflection(new ReflectionFunction(function (): Bar&Foo {})); + +Assert::same(['Bar', 'Foo'], $type->getNames()); +Assert::same('Bar&Foo', (string) $type); + + // tentative type $type = Type::fromReflection(new \ReflectionMethod(\ArrayObject::class, 'count')); Assert::same('int', (string) $type); diff --git a/tests/Utils/Type.fromString.phpt b/tests/Utils/Type.fromString.phpt index cbae02253..8a568c24a 100644 --- a/tests/Utils/Type.fromString.phpt +++ b/tests/Utils/Type.fromString.phpt @@ -20,6 +20,7 @@ Assert::same('string', (string) $type); Assert::same('string', $type->getSingleName()); Assert::false($type->isClass()); Assert::false($type->isUnion()); +Assert::false($type->isIntersection()); Assert::true($type->isSingle()); Assert::true($type->isBuiltin()); @@ -32,6 +33,7 @@ Assert::same('?string', (string) $type); Assert::same('string', $type->getSingleName()); Assert::false($type->isClass()); Assert::true($type->isUnion()); +Assert::false($type->isIntersection()); Assert::true($type->isSingle()); Assert::true($type->isBuiltin()); @@ -44,6 +46,7 @@ Assert::same('?string', (string) $type); Assert::same('string', $type->getSingleName()); Assert::false($type->isClass()); Assert::true($type->isUnion()); +Assert::false($type->isIntersection()); Assert::true($type->isSingle()); Assert::true($type->isBuiltin()); @@ -56,6 +59,7 @@ Assert::same('?string', (string) $type); Assert::same('string', $type->getSingleName()); Assert::false($type->isClass()); Assert::true($type->isUnion()); +Assert::false($type->isIntersection()); Assert::true($type->isSingle()); Assert::true($type->isBuiltin()); @@ -68,6 +72,7 @@ Assert::same('NS\Foo', (string) $type); Assert::same('NS\Foo', $type->getSingleName()); Assert::true($type->isClass()); Assert::false($type->isUnion()); +Assert::false($type->isIntersection()); Assert::true($type->isSingle()); Assert::false($type->isBuiltin()); @@ -80,6 +85,7 @@ Assert::same('string|Foo', (string) $type); Assert::null($type->getSingleName()); Assert::false($type->isClass()); Assert::true($type->isUnion()); +Assert::false($type->isIntersection()); Assert::false($type->isSingle()); Assert::false($type->isBuiltin()); @@ -92,6 +98,7 @@ Assert::same('mixed', (string) $type); Assert::same('mixed', $type->getSingleName()); Assert::false($type->isClass()); Assert::false($type->isUnion()); +Assert::false($type->isIntersection()); Assert::true($type->isSingle()); Assert::true($type->isBuiltin()); @@ -104,5 +111,19 @@ Assert::same('null', (string) $type); Assert::same('null', $type->getSingleName()); Assert::false($type->isClass()); Assert::false($type->isUnion()); +Assert::false($type->isIntersection()); Assert::true($type->isSingle()); Assert::true($type->isBuiltin()); + + +$type = Type::fromString('Bar&Foo'); + +Assert::same(['Bar', 'Foo'], $type->getNames()); +Assert::equal([Type::fromString('Bar'), Type::fromString('Foo')], $type->getTypes()); +Assert::same('Bar&Foo', (string) $type); +Assert::null($type->getSingleName()); +Assert::false($type->isClass()); +Assert::false($type->isUnion()); +Assert::true($type->isIntersection()); +Assert::false($type->isSingle()); +Assert::false($type->isBuiltin());