Skip to content

Commit

Permalink
Type: supports Disjunctive Normal Form Types
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Jan 18, 2023
1 parent 912db4a commit c91bac3
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 46 deletions.
113 changes: 67 additions & 46 deletions src/Utils/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/
final class Type
{
/** @var array */
/** @var array<int, string|self> */
private $types;

/** @var bool */
Expand All @@ -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));
}
}

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


Expand Down Expand Up @@ -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<int, string|string[]>
*/
public function getNames(): array
{
return $this->types;
return array_map(function ($t) {
return $t instanceof self ? $t->getNames() : $t;
}, $this->types);
}


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


Expand Down Expand Up @@ -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);
});
});
}
Expand Down
15 changes: 15 additions & 0 deletions tests/Utils/Type.allows.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class Bar
{
}

class Baz
{
}

class Foo
{
}
Expand Down Expand Up @@ -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'));
58 changes: 58 additions & 0 deletions tests/Utils/Type.fromReflection.function.82.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/**
* @phpversion 8.2
*/

declare(strict_types=1);

use Nette\Utils\Type;
use Tester\Assert;

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


$type = Type::fromReflection(new ReflectionFunction(function (): string {}));

Assert::same(['string'], $type->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);
14 changes: 14 additions & 0 deletions tests/Utils/Type.fromString.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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());

0 comments on commit c91bac3

Please sign in to comment.