misc: save type token symbols during lexing

This commit is contained in:
Romain Canon 2022-05-26 20:52:17 +02:00
parent d7dda9d261
commit ad0f8fee17
30 changed files with 361 additions and 15 deletions

View File

@ -31,6 +31,8 @@ final class ArrayToken implements TraversingToken
/** @var class-string<ArrayType|NonEmptyArrayType> */
private string $arrayType;
private string $symbol;
private static self $array;
private static self $nonEmptyArray;
@ -38,19 +40,20 @@ final class ArrayToken implements TraversingToken
/**
* @param class-string<ArrayType|NonEmptyArrayType> $arrayType
*/
private function __construct(string $arrayType)
private function __construct(string $arrayType, string $symbol)
{
$this->arrayType = $arrayType;
$this->symbol = $symbol;
}
public static function array(): self
{
return self::$array ??= new self(ArrayType::class);
return self::$array ??= new self(ArrayType::class, 'array');
}
public static function nonEmptyArray(): self
{
return self::$nonEmptyArray ??= new self(NonEmptyArrayType::class);
return self::$nonEmptyArray ??= new self(NonEmptyArrayType::class, 'non-empty-array');
}
public function traverse(TokenStream $stream): Type
@ -70,6 +73,11 @@ final class ArrayToken implements TraversingToken
return ($this->arrayType)::native();
}
public function symbol(): string
{
return $this->symbol;
}
private function arrayType(TokenStream $stream): CompositeTraversableType
{
// @PHP8.0 use `new ($this->arrayType)(...)`
@ -130,9 +138,7 @@ final class ArrayToken implements TraversingToken
$optional = false;
if ($stream->next() instanceof UnknownSymbolToken) {
/** @var UnknownSymbolToken $token */
$token = $stream->forward();
$type = new StringValueType($token->symbol());
$type = new StringValueType($stream->forward()->symbol());
} else {
$type = $stream->read();
}

View File

@ -40,4 +40,9 @@ final class ClassNameToken implements TraversingToken
{
return $this->className;
}
public function symbol(): string
{
return $this->className;
}
}

View File

@ -38,4 +38,9 @@ final class ClassStringToken implements TraversingToken
return new ClassStringType($type);
}
public function symbol(): string
{
return 'class-string';
}
}

View File

@ -10,4 +10,9 @@ use CuyZ\Valinor\Utility\IsSingleton;
final class ClosingBracketToken implements Token
{
use IsSingleton;
public function symbol(): string
{
return '>';
}
}

View File

@ -10,4 +10,9 @@ use CuyZ\Valinor\Utility\IsSingleton;
final class ClosingCurlyBracketToken implements Token
{
use IsSingleton;
public function symbol(): string
{
return '}';
}
}

View File

@ -10,4 +10,9 @@ use CuyZ\Valinor\Utility\IsSingleton;
final class ClosingSquareBracketToken implements Token
{
use IsSingleton;
public function symbol(): string
{
return ']';
}
}

View File

@ -10,4 +10,9 @@ use CuyZ\Valinor\Utility\IsSingleton;
final class ColonToken implements Token
{
use IsSingleton;
public function symbol(): string
{
return ':';
}
}

View File

@ -10,4 +10,9 @@ use CuyZ\Valinor\Utility\IsSingleton;
final class CommaToken implements Token
{
use IsSingleton;
public function symbol(): string
{
return ',';
}
}

View File

@ -27,4 +27,9 @@ final class EnumNameToken implements TraversingToken
{
return new EnumType($this->enumName);
}
public function symbol(): string
{
return $this->enumName;
}
}

View File

@ -22,4 +22,9 @@ final class FloatValueToken implements TraversingToken
{
return new FloatValueType($this->value);
}
public function symbol(): string
{
return (string)$this->value;
}
}

View File

@ -78,6 +78,11 @@ final class GenericClassNameToken implements TraversingToken
: new ClassType($this->className, $generics);
}
public function symbol(): string
{
return $this->className;
}
/**
* @param array<string, Type> $templates
* @return Type[]

View File

@ -70,4 +70,9 @@ final class IntegerToken implements TraversingToken
return new IntegerRangeType($min->value(), $max->value());
}
public function symbol(): string
{
return 'int';
}
}

View File

@ -22,4 +22,9 @@ final class IntegerValueToken implements TraversingToken
{
return new IntegerValueType($this->value);
}
public function symbol(): string
{
return (string)$this->value;
}
}

View File

@ -39,4 +39,9 @@ final class IntersectionToken implements LeftTraversingToken
return new IntersectionType($type, $rightType);
}
public function symbol(): string
{
return '&';
}
}

View File

@ -56,4 +56,9 @@ final class IterableToken implements TraversingToken
return $iterableType;
}
public function symbol(): string
{
return 'iterable';
}
}

View File

@ -16,6 +16,8 @@ final class ListToken implements TraversingToken
/** @var class-string<ListType|NonEmptyListType> */
private string $listType;
private string $symbol;
private static self $list;
private static self $nonEmptyList;
@ -23,19 +25,20 @@ final class ListToken implements TraversingToken
/**
* @param class-string<ListType|NonEmptyListType> $listType
*/
private function __construct(string $listType)
private function __construct(string $listType, string $symbol)
{
$this->listType = $listType;
$this->symbol = $symbol;
}
public static function list(): self
{
return self::$list ??= new self(ListType::class);
return self::$list ??= new self(ListType::class, 'list');
}
public static function nonEmptyList(): self
{
return self::$nonEmptyList ??= new self(NonEmptyListType::class);
return self::$nonEmptyList ??= new self(NonEmptyListType::class, 'non-empty-list');
}
public function traverse(TokenStream $stream): Type
@ -58,4 +61,9 @@ final class ListToken implements TraversingToken
return ($this->listType)::native();
}
public function symbol(): string
{
return $this->symbol;
}
}

View File

@ -29,23 +29,27 @@ final class NativeToken implements TraversingToken
private Type $type;
private function __construct(Type $type)
private string $symbol;
private function __construct(Type $type, string $symbol)
{
$this->type = $type;
$this->symbol = $symbol;
}
public static function accepts(string $symbol): bool
{
return (bool)self::type($symbol);
return (bool)self::type(strtolower($symbol));
}
public static function from(string $symbol): self
{
$symbol = strtolower($symbol);
$type = self::type($symbol);
assert($type instanceof Type);
return self::$map[$symbol] ??= new self($type);
return self::$map[$symbol] ??= new self($type, $symbol);
}
public function traverse(TokenStream $stream): Type
@ -53,10 +57,15 @@ final class NativeToken implements TraversingToken
return $this->type;
}
public function symbol(): string
{
return $this->symbol;
}
private static function type(string $symbol): ?Type
{
// @PHP8.0 match
switch (strtolower($symbol)) {
switch ($symbol) {
case 'null':
return NullType::get();
case 'true':

View File

@ -19,4 +19,9 @@ final class NullableToken implements TraversingToken
{
return new UnionType(NullType::get(), $stream->read());
}
public function symbol(): string
{
return '?';
}
}

View File

@ -10,4 +10,9 @@ use CuyZ\Valinor\Utility\IsSingleton;
final class OpeningBracketToken implements Token
{
use IsSingleton;
public function symbol(): string
{
return '<';
}
}

View File

@ -10,4 +10,9 @@ use CuyZ\Valinor\Utility\IsSingleton;
final class OpeningCurlyBracketToken implements Token
{
use IsSingleton;
public function symbol(): string
{
return '{';
}
}

View File

@ -23,4 +23,9 @@ final class OpeningSquareBracketToken implements LeftTraversingToken
return ArrayType::simple($type);
}
public function symbol(): string
{
return '[';
}
}

View File

@ -32,4 +32,9 @@ final class StringValueToken implements TraversingToken
{
return $this->type;
}
public function symbol(): string
{
return $this->type->toString();
}
}

View File

@ -7,4 +7,5 @@ namespace CuyZ\Valinor\Type\Parser\Lexer\Token;
/** @internal */
interface Token
{
public function symbol(): string;
}

View File

@ -12,13 +12,21 @@ final class TypeToken implements TraversingToken
{
private Type $type;
public function __construct(Type $type)
private string $symbol;
public function __construct(Type $type, string $symbol)
{
$this->type = $type;
$this->symbol = $symbol;
}
public function traverse(TokenStream $stream): Type
{
return $this->type;
}
public function symbol(): string
{
return $this->symbol;
}
}

View File

@ -23,4 +23,9 @@ final class UnionToken implements LeftTraversingToken
return new UnionType($type, $stream->read());
}
public function symbol(): string
{
return '|';
}
}

View File

@ -28,7 +28,7 @@ final class TypeAliasLexer implements TypeLexer
public function tokenize(string $symbol): Token
{
if (isset($this->aliases[$symbol])) {
return new TypeToken($this->aliases[$symbol]);
return new TypeToken($this->aliases[$symbol], $symbol);
}
return $this->delegate->tokenize($symbol);

View File

@ -8,4 +8,8 @@ use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
final class FakeToken implements Token
{
public function symbol(): string
{
return 'fake-token';
}
}

View File

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer;
use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum;
use CuyZ\Valinor\Type\Parser\Lexer\NativeLexer;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ArrayToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ClassNameToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ClassStringToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ClosingBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ClosingCurlyBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ClosingSquareBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ColonToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\CommaToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\EnumNameToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\FloatValueToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IntegerToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IntegerValueToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IntersectionToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IterableToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\ListToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\NativeToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\NullableToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningCurlyBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningSquareBracketToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\StringValueToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
use CuyZ\Valinor\Type\Parser\Lexer\Token\UnionToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\UnknownSymbolToken;
use PHPUnit\Framework\TestCase;
use stdClass;
final class NativeLexerTest extends TestCase
{
private NativeLexer $lexer;
protected function setUp(): void
{
parent::setUp();
$this->lexer = new NativeLexer();
}
/**
* @dataProvider tokenized_type_is_correct_data_provider
*
* @param class-string<Token> $tokenClassName
*/
public function test_tokenized_type_is_correct(string $symbol, string $tokenClassName): void
{
$token = $this->lexer->tokenize($symbol);
self::assertInstanceOf($tokenClassName, $token);
self::assertSame($symbol, $token->symbol());
}
public function tokenized_type_is_correct_data_provider(): iterable
{
yield 'null' => [
'symbol' => 'null',
'token' => NativeToken::class,
];
yield 'union' => [
'symbol' => '|',
'token' => UnionToken::class,
];
yield 'intersection' => [
'symbol' => '&',
'token' => IntersectionToken::class,
];
yield 'opening bracket' => [
'symbol' => '<',
'token' => OpeningBracketToken::class,
];
yield 'closing bracket' => [
'symbol' => '>',
'token' => ClosingBracketToken::class,
];
yield 'opening square bracket' => [
'symbol' => '[',
'token' => OpeningSquareBracketToken::class,
];
yield 'closing square bracket' => [
'symbol' => ']',
'token' => ClosingSquareBracketToken::class,
];
yield 'opening curly bracket' => [
'symbol' => '{',
'token' => OpeningCurlyBracketToken::class,
];
yield 'closing curly bracket' => [
'symbol' => '}',
'token' => ClosingCurlyBracketToken::class,
];
yield 'colon' => [
'symbol' => ':',
'token' => ColonToken::class,
];
yield 'nullable' => [
'symbol' => '?',
'token' => NullableToken::class,
];
yield 'comma' => [
'symbol' => ',',
'token' => CommaToken::class,
];
yield 'int' => [
'symbol' => 'int',
'token' => IntegerToken::class,
];
yield 'array' => [
'symbol' => 'array',
'token' => ArrayToken::class,
];
yield 'non empty array' => [
'symbol' => 'non-empty-array',
'token' => ArrayToken::class,
];
yield 'list' => [
'symbol' => 'list',
'token' => ListToken::class,
];
yield 'non empty list' => [
'symbol' => 'non-empty-list',
'token' => ListToken::class,
];
yield 'iterable' => [
'symbol' => 'iterable',
'token' => IterableToken::class,
];
yield 'class-string' => [
'symbol' => 'class-string',
'token' => ClassStringToken::class,
];
yield 'single quote string value' => [
'symbol' => "'foo'",
'token' => StringValueToken::class,
];
yield 'double quote string value' => [
'symbol' => '"foo"',
'token' => StringValueToken::class,
];
yield 'integer value' => [
'symbol' => '1337',
'token' => IntegerValueToken::class,
];
yield 'float value' => [
'symbol' => '1337.42',
'token' => FloatValueToken::class,
];
yield 'class' => [
'symbol' => stdClass::class,
'token' => ClassNameToken::class,
];
yield 'unknown' => [
'symbol' => 'unknown',
'token' => UnknownSymbolToken::class,
];
if (PHP_VERSION_ID >= 8_01_00) {
yield 'enum' => [
'symbol' => PureEnum::class,
'token' => EnumNameToken::class,
];
}
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer\Token;
use CuyZ\Valinor\Tests\Fake\Type\Parser\Factory\FakeTypeParserFactory;
use CuyZ\Valinor\Tests\Fake\Type\Parser\Template\FakeTemplateParser;
use CuyZ\Valinor\Type\Parser\Lexer\Token\GenericClassNameToken;
use PHPUnit\Framework\TestCase;
use stdClass;
final class GenericClassNameTokenTest extends TestCase
{
public function test_symbol_is_correct(): void
{
$token = new GenericClassNameToken(stdClass::class, new FakeTypeParserFactory(), new FakeTemplateParser());
self::assertSame(stdClass::class, $token->symbol());
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer\Token;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Parser\Lexer\Token\TypeToken;
use PHPUnit\Framework\TestCase;
final class TypeTokenTest extends TestCase
{
public function test_symbol_is_correct(): void
{
$token = new TypeToken(FakeType::permissive(), 'foo');
self::assertSame('foo', $token->symbol());
}
}