From c91bac3470c34b2ecd5400f6e6fdf0b64a836a5c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 17 Jan 2023 21:27:23 +0100 Subject: [PATCH] Type: supports Disjunctive Normal Form Types --- src/Utils/Type.php | 113 +++++++++++------- tests/Utils/Type.allows.phpt | 15 +++ .../Type.fromReflection.function.82.phpt | 58 +++++++++ tests/Utils/Type.fromString.phpt | 14 +++ 4 files changed, 154 insertions(+), 46 deletions(-) create mode 100644 tests/Utils/Type.fromReflection.function.82.phpt diff --git a/src/Utils/Type.php b/src/Utils/Type.php index ba8d073f5..a4495c2cf 100644 --- a/src/Utils/Type.php +++ b/src/Utils/Type.php @@ -17,7 +17,7 @@ */ final class Type { - /** @var array */ + /** @var array */ private $types; /** @var bool */ @@ -44,24 +44,29 @@ public static function fromReflection($reflection): ?self : $reflection->getType(); } - if ($type === null) { - return null; + return $type ? self::fromReflectionType($type, $reflection, true) : null; + } - } elseif ($type instanceof \ReflectionNamedType) { - $name = self::resolve($type->getName(), $reflection); - return new self($type->allowsNull() && $type->getName() !== 'mixed' ? [$name, 'null'] : [$name]); + + private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject) + { + if ($type instanceof \ReflectionNamedType) { + $name = self::resolve($type->getName(), $of); + return $asObject + ? new self($type->allowsNull() && $name !== 'mixed' ? [$name, 'null'] : [$name]) + : $name; } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { return new self( array_map( - function ($t) use ($reflection) { return self::resolve($t->getName(), $reflection); }, + function ($t) use ($of) { return self::fromReflectionType($t, $of, false); }, $type->getTypes() ), $type instanceof \ReflectionUnionType ? '|' : '&' ); } else { - throw new Nette\InvalidStateException('Unexpected type of ' . Reflection::toString($reflection)); + throw new Nette\InvalidStateException('Unexpected type of ' . Reflection::toString($of)); } } @@ -71,21 +76,23 @@ function ($t) use ($reflection) { return self::resolve($t->getName(), $reflectio */ public static function fromString(string $type): self { - if (!preg_match('#(?: - \?([\w\\\\]+)| - [\w\\\\]+ (?: (&[\w\\\\]+)* | (\|[\w\\\\]+)* ) - )()$#xAD', $type, $m)) { + if (!Validators::isTypeDeclaration($type)) { throw new Nette\InvalidArgumentException("Invalid type '$type'."); } - [, $nType, $iType] = $m; - if ($nType) { - return new self([$nType, 'null']); - } elseif ($iType) { - return new self(explode('&', $type), '&'); - } else { - return new self(explode('|', $type)); + if ($type[0] === '?') { + return new self([substr($type, 1), 'null']); } + + $unions = []; + foreach (explode('|', $type) as $part) { + $part = explode('&', trim($part, '()')); + $unions[] = count($part) === 1 ? $part[0] : new self($part, '&'); + } + + return count($unions) === 1 && $unions[0] instanceof self + ? $unions[0] + : new self($unions); } @@ -117,26 +124,35 @@ private function __construct(array $types, string $kind = '|') } $this->types = $types; - $this->simple = ($types[1] ?? 'null') === 'null'; + $this->simple = is_string($types[0]) && ($types[1] ?? 'null') === 'null'; $this->kind = count($types) > 1 ? $kind : ''; } public function __toString(): string { - return $this->simple - ? (count($this->types) > 1 ? '?' : '') . $this->types[0] - : implode($this->kind, $this->types); + $multi = count($this->types) > 1; + if ($this->simple) { + return ($multi ? '?' : '') . $this->types[0]; + } + + $res = []; + foreach ($this->types as $type) { + $res[] = $type instanceof self && $multi ? "($type)" : $type; + } + return implode($this->kind, $res); } /** * Returns the array of subtypes that make up the compound type as strings. - * @return string[] + * @return array */ public function getNames(): array { - return $this->types; + return array_map(function ($t) { + return $t instanceof self ? $t->getNames() : $t; + }, $this->types); } @@ -146,7 +162,9 @@ public function getNames(): array */ public function getTypes(): array { - return array_map(function ($name) { return self::fromString($name); }, $this->types); + return array_map(function ($t) { + return $t instanceof self ? $t : new self([$t]); + }, $this->types); } @@ -232,29 +250,32 @@ public function allows(string $subtype): bool } $subtype = self::fromString($subtype); + return $subtype->isUnion() + ? Arrays::every($subtype->types, function ($t) { + return $this->allows2($t instanceof self ? $t->types : [$t]); + }) + : $this->allows2($subtype->types); + } - if ($this->isIntersection()) { - if (!$subtype->isIntersection()) { - return false; - } - - return Arrays::every($this->types, function ($currentType) use ($subtype) { - $builtin = Reflection::isBuiltinType($currentType); - return Arrays::some($subtype->types, function ($testedType) use ($currentType, $builtin) { - return $builtin - ? strcasecmp($currentType, $testedType) === 0 - : is_a($testedType, $currentType, true); - }); - }); - } - $method = $subtype->isIntersection() ? 'some' : 'every'; - return Arrays::$method($subtype->types, function ($testedType) { - $builtin = Validators::isBuiltinType($testedType); - return Arrays::some($this->types, function ($currentType) use ($testedType, $builtin) { + private function allows2(array $subtypes): bool + { + return $this->isUnion() + ? Arrays::some($this->types, function ($t) use ($subtypes) { + return $this->allows3($t instanceof self ? $t->types : [$t], $subtypes); + }) + : $this->allows3($this->types, $subtypes); + } + + + private function allows3(array $types, array $subtypes): bool + { + return Arrays::every($types, function ($type) use ($subtypes) { + $builtin = Validators::isBuiltinType($type); + return Arrays::some($subtypes, function ($subtype) use ($type, $builtin) { return $builtin - ? strcasecmp($currentType, $testedType) === 0 - : is_a($testedType, $currentType, true); + ? strcasecmp($type, $subtype) === 0 + : is_a($subtype, $type, true); }); }); } diff --git a/tests/Utils/Type.allows.phpt b/tests/Utils/Type.allows.phpt index 8074c24e3..e92d294c2 100644 --- a/tests/Utils/Type.allows.phpt +++ b/tests/Utils/Type.allows.phpt @@ -16,6 +16,10 @@ class Bar { } +class Baz +{ +} + class Foo { } @@ -77,3 +81,14 @@ Assert::true($type->allows('Foo&Bar&Baz')); $type = Type::fromString('Bar&FooChild'); Assert::false($type->allows('Foo&Bar')); + + +$type = Type::fromString('(Bar&Foo)|null'); +Assert::false($type->allows('string')); +Assert::true($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')); +Assert::true($type->allows('(Foo&Bar&Baz)|null')); diff --git a/tests/Utils/Type.fromReflection.function.82.phpt b/tests/Utils/Type.fromReflection.function.82.phpt new file mode 100644 index 000000000..5a70e9da6 --- /dev/null +++ b/tests/Utils/Type.fromReflection.function.82.phpt @@ -0,0 +1,58 @@ +getNames()); +Assert::same('string', (string) $type); + + +$type = Type::fromReflection(new ReflectionFunction(function (): ?string {})); + +Assert::same(['string', 'null'], $type->getNames()); +Assert::same('?string', (string) $type); + + +$type = Type::fromReflection(new ReflectionFunction(function (): Foo {})); + +Assert::same(['Foo'], $type->getNames()); +Assert::same('Foo', (string) $type); + + +$type = Type::fromReflection(new ReflectionFunction(function (): Foo|string {})); + +Assert::same(['Foo', 'string'], $type->getNames()); +Assert::same('Foo|string', (string) $type); + + +$type = Type::fromReflection(new ReflectionFunction(function (): mixed {})); + +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); + + +// disjunctive normal form +$type = Type::fromReflection(new ReflectionFunction(function (): (Bar & Foo)|string|int|null {})); +Assert::same('(Bar&Foo)|string|int|null', (string) $type); diff --git a/tests/Utils/Type.fromString.phpt b/tests/Utils/Type.fromString.phpt index d1844d0de..d1284ac07 100644 --- a/tests/Utils/Type.fromString.phpt +++ b/tests/Utils/Type.fromString.phpt @@ -164,3 +164,17 @@ Assert::false($type->isIntersection()); Assert::true($type->isSimple()); Assert::false($type->isBuiltin()); Assert::true($type->isClassKeyword()); + + +$type = Type::fromString('(A&B)|null'); + +Assert::same([['A', 'B'], 'null'], $type->getNames()); +Assert::equal([Type::fromString('A&B'), Type::fromString('null')], $type->getTypes()); +Assert::same('(A&B)|null', (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()); +Assert::false($type->isClassKeyword());