Repository: CuyZ/Valinor (php)

Issue:
Unneeded calls to the autoloader
Please consider the following code:

```php
<?php

namespace NS;

use CuyZ\Valinor\MapperBuilder;

require('vendor/autoload.php');

\spl_autoload_register(function (string $class) {
    echo "L: ", $class, PHP_EOL;
});

final class Target
{
    /**
     * @param   array{
     *              foo?: string,
     *              bar?: int,
     *              baz?: array<string, int>,
     *          } $foo
     * @param   array<string,string> $bar 
     */
    public function __construct(
        public readonly array $foo,
        public readonly array $bar,
    )
    {
    }
}

try {
    $payload = (new MapperBuilder())
        ->mapper()
        ->map(
            Target::class,
            [
                "bar" => [],
                "foo" => [
                    "foo" => "foo",
                ]
            ]
        );

    var_dump($payload);
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    $messages = \CuyZ\Valinor\Mapper\Tree\Message\Messages::flattenFromNode(
        $error->node()
    );

    foreach ($messages as $message) {
        echo $message, PHP_EOL;
    }
}
```

When running this code, the following output is emitted:

```
L: NS\array
L: NS\array
L: NS\foo
L: NS\foo
L: foo
L: foo
L: foo
L: NS\string
L: NS\string
L: NS\bar
L: NS\bar
L: bar
L: bar
L: bar
L: NS\int
L: NS\int
L: NS\baz
L: NS\baz
L: baz
L: baz
L: baz
L: NS\array
L: NS\array
L: NS\string
L: NS\string
L: NS\int
L: NS\int
L: NS\array
L: NS\array
L: NS\string
L: NS\string
L: NS\string
L: NS\string
L: NS\array
L: NS\array
L: NS\foo
L: NS\foo
L: foo
L: foo
L: foo
L: NS\string
L: NS\string
L: NS\bar
L: NS\bar
L: bar
L: bar
L: bar
L: NS\int
L: NS\int
L: NS\baz
L: NS\baz
L: baz
L: baz
L: baz
L: NS\array
L: NS\array
L: NS\string
L: NS\string
L: NS\int
L: NS\int
L: NS\array
L: NS\array
L: NS\string
L: NS\string
L: NS\string
L: NS\string
/pwd/test.php:44:
class NS\Target#146 (2) {
  public readonly array $foo =>
  array(1) {
    'foo' =>
    string(3) "foo"
  }
  public readonly array $bar =>
  array(0) {
  }
}
```

It becomes clear that the autoloader is uselessly called for classnames that are never going to result in a hit. This can get expensive for an autoloader that checks the file system for a class to include. Especially since the calls are repeated multiple times:

- `NS\int`, `NS\string` and `NS\array` can never successfully load a class, because `int`, `string` and `array` are reserved class names.
- `foo`, `bar` and `baz` as keys of the array shape and thus should not be considered class names in the first place.

As a result I did not expect any calls to my autoloader happening at all.
Conflict in array shapes with namespaces
Hello,

Suppose we have the following class:

```php
<?php

declare(strict_types=1);

namespace Domain;

final readonly class Element
{
    public function __construct(
        public string $value,
    ) {}
}

```

And that we have this other class, that includes a `use` for the first class, and that has the class name as key in the array shape definition:

```php
<?php

declare(strict_types=1);

namespace Infrastructure;

use Domain\Element;

final readonly class ContainerWithElement
{
    public function __construct(
        /** @var array{element: string} */
        private array $elements,
    ) {}

    public function element(): Element
    {
        return new Element($this->elements['element']);
    }
}

```

When we try to map the `ContainerWithElement`:

```php
$element = (new \CuyZ\Valinor\MapperBuilder())
            ->mapper()
            ->map(
                ContainerWithElement::class,
                \CuyZ\Valinor\Mapper\Source\Source::array(['element' => 'foo'])
            );
```

we get the following error:

```
CuyZ\Valinor\Mapper\TypeTreeMapperError: Could not map type `Infrastructure\ContainerWithElement`. An error occurred at path elements.Domain\Element: Cannot be empty and must be filled with a value matching type `string`.
```

The same happens if we use quotes in the key of the array shape.

However, if we import the class using an alias, the error is solved and the type is mapped:

```php
<?php

declare(strict_types=1);

namespace Infrastructure;

use Domain\Element as DomainElement;

final readonly class ContainerWithElement
{
    public function __construct(
        /** @var array{element: string} */
        private array $elements,
    ) {}

    public function element(): DomainElement
    {
        return new DomainElement($this->elements['element']);
    }
}

```

It does not happen either if both classes are in the same namespace, thus not requiring to import it.

So I guess there is a conflict between the key of the array shape and the imported class. I do not know if this is an issue or if it is intended, or just a limitation. It happens in versions 1.10 and 1.11.

I just wanted to bring it to your attention, since I encountered it in a project :)  Right now, the solution is to use an alias, and that seems acceptable to me, in case it should remain as it is.

Thank you!

Pull request description:
fix: handle class tokens only when needed during lexing
Major refactor of how the lexer handles class/interface/enums, which aims to avoid potentially expensive operations (like `class_exists` which can call the autoloader).

This results in a minor performance bump, and also fixes some issues concerning shaped array keys matching class names.

Fixes #427 
Fixes #513 

Patch:
diff --git a/src/Type/Parser/Factory/LexingTypeParserFactory.php b/src/Type/Parser/Factory/LexingTypeParserFactory.php
index b98aa26..461f18d 100644
--- a/src/Type/Parser/Factory/LexingTypeParserFactory.php
+++ b/src/Type/Parser/Factory/LexingTypeParserFactory.php
@@ -7,7 +7,7 @@ namespace CuyZ\Valinor\Type\Parser\Factory;
 use CuyZ\Valinor\Type\Parser\CachedParser;
 use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeParserSpecification;
 use CuyZ\Valinor\Type\Parser\Lexer\NativeLexer;
-use CuyZ\Valinor\Type\Parser\Lexer\ObjectLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\SpecificationsLexer;
 use CuyZ\Valinor\Type\Parser\LexingParser;
 use CuyZ\Valinor\Type\Parser\TypeParser;
 
@@ -27,12 +27,7 @@ final class LexingTypeParserFactory implements TypeParserFactory
 
     private function buildTypeParser(TypeParserSpecification ...$specifications): TypeParser
     {
-        $lexer = new ObjectLexer();
-
-        foreach ($specifications as $specification) {
-            $lexer = $specification->manipulateLexer($lexer);
-        }
-
+        $lexer = new SpecificationsLexer($specifications);
         $lexer = new NativeLexer($lexer);
 
         $parser = new LexingParser($lexer);
diff --git a/src/Type/Parser/Factory/Specifications/AliasSpecification.php b/src/Type/Parser/Factory/Specifications/AliasSpecification.php
index 57d6189..bf8e4aa 100644
--- a/src/Type/Parser/Factory/Specifications/AliasSpecification.php
+++ b/src/Type/Parser/Factory/Specifications/AliasSpecification.php
@@ -5,9 +5,11 @@ declare(strict_types=1);
 namespace CuyZ\Valinor\Type\Parser\Factory\Specifications;
 
 use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
-use CuyZ\Valinor\Type\Parser\Lexer\AliasLexer;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\ObjectToken;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken;
 use CuyZ\Valinor\Type\Parser\TypeParser;
+use CuyZ\Valinor\Utility\Reflection\PhpParser;
+use CuyZ\Valinor\Utility\Reflection\Reflection;
 use ReflectionClass;
 use ReflectionFunction;
 use Reflector;
@@ -17,16 +19,95 @@ final class AliasSpecification implements TypeParserSpecification
 {
     public function __construct(
         /** @var ReflectionClass<object>|ReflectionFunction */
-        private Reflector $reflection
+        private Reflector $reflection,
     ) {}
 
-    public function manipulateLexer(TypeLexer $lexer): TypeLexer
+    public function manipulateToken(TraversingToken $token): TraversingToken
     {
-        return new AliasLexer($lexer, $this->reflection);
+        $symbol = $token->symbol();
+
+        // Matches the case where a class extends a class with the same name but
+        // in a different namespace.
+        if ($symbol === $this->reflection->getShortName() && Reflection::classOrInterfaceExists($symbol)) {
+            return $token;
+        }
+
+        $alias = $this->resolveAlias($symbol);
+
+        if (strtolower($alias) !== strtolower($symbol)) {
+            /** @var class-string $alias */
+            return new ObjectToken($alias);
+        }
+
+        $namespaced = $this->resolveNamespaced($symbol);
+
+        if ($namespaced !== $symbol) {
+            /** @var class-string $namespaced */
+            return new ObjectToken($namespaced);
+        }
+
+        return $token;
     }
 
     public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser
     {
         return $parser;
     }
+
+    private function resolveAlias(string $symbol): string
+    {
+        $alias = $symbol;
+
+        $namespaceParts = explode('\\', $symbol);
+        $lastPart = array_shift($namespaceParts);
+
+        if ($lastPart) {
+            $alias = strtolower($lastPart);
+        }
+
+        $aliases = PhpParser::parseUseStatements($this->reflection);
+
+        if (! isset($aliases[$alias])) {
+            return $symbol;
+        }
+
+        if ($aliases[$alias] === $symbol) {
+            return $symbol;
+        }
+
+        $full = $aliases[$alias];
+
+        if (! empty($namespaceParts)) {
+            $full .= '\\' . implode('\\', $namespaceParts);
+        }
+
+        return $full;
+    }
+
+    private function resolveNamespaced(string $symbol): string
+    {
+        $reflection = $this->reflection;
+
+        if ($reflection instanceof ReflectionFunction) {
+            $classReflection = $reflection->getClosureScopeClass();
+
+            if ($classReflection && $classReflection->getFileName() === $reflection->getFileName()) {
+                $reflection = $classReflection;
+            }
+        }
+
+        $namespace = $reflection->getNamespaceName();
+
+        if (! $namespace) {
+            return $symbol;
+        }
+
+        $full = $namespace . '\\' . $symbol;
+
+        if (Reflection::classOrInterfaceExists($full)) {
+            return $full;
+        }
+
+        return $symbol;
+    }
 }
diff --git a/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php b/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php
index 25ffe09..272ea94 100644
--- a/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php
+++ b/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
 namespace CuyZ\Valinor\Type\Parser\Factory\Specifications;
 
 use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
-use CuyZ\Valinor\Type\Parser\Lexer\ClassContextLexer;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\ObjectToken;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken;
 use CuyZ\Valinor\Type\Parser\TypeParser;
 
 /** @internal */
@@ -14,12 +14,16 @@ final class ClassContextSpecification implements TypeParserSpecification
 {
     public function __construct(
         /** @var class-string */
-        private string $className
+        private string $className,
     ) {}
 
-    public function manipulateLexer(TypeLexer $lexer): TypeLexer
+    public function manipulateToken(TraversingToken $token): TraversingToken
     {
-        return new ClassContextLexer($lexer, $this->className);
+        if ($token->symbol() === 'self' || $token->symbol() === 'static') {
+            return new ObjectToken($this->className);
+        }
+
+        return $token;
     }
 
     public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser
diff --git a/src/Type/Parser/Factory/Specifications/GenericCheckerSpecification.php b/src/Type/Parser/Factory/Specifications/GenericCheckerSpecification.php
index bd0f665..a33eaa5 100644
--- a/src/Type/Parser/Factory/Specifications/GenericCheckerSpecification.php
+++ b/src/Type/Parser/Factory/Specifications/GenericCheckerSpecification.php
@@ -6,15 +6,15 @@ namespace CuyZ\Valinor\Type\Parser\Factory\Specifications;
 
 use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
 use CuyZ\Valinor\Type\Parser\GenericCheckerParser;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken;
 use CuyZ\Valinor\Type\Parser\TypeParser;
 
 /** @internal */
 final class GenericCheckerSpecification implements TypeParserSpecification
 {
-    public function manipulateLexer(TypeLexer $lexer): TypeLexer
+    public function manipulateToken(TraversingToken $token): TraversingToken
     {
-        return $lexer;
+        return $token;
     }
 
     public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser
diff --git a/src/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php b/src/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php
index 4cad03a..1c07a9b 100644
--- a/src/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php
+++ b/src/Type/Parser/Factory/Specifications/TypeAliasAssignerSpecification.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
 namespace CuyZ\Valinor\Type\Parser\Factory\Specifications;
 
 use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeAliasLexer;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\TypeToken;
 use CuyZ\Valinor\Type\Parser\TypeParser;
 use CuyZ\Valinor\Type\Type;
 
@@ -15,12 +15,18 @@ final class TypeAliasAssignerSpecification implements TypeParserSpecification
 {
     public function __construct(
         /** @var array<string, Type> */
-        private array $aliases
+        private array $aliases,
     ) {}
 
-    public function manipulateLexer(TypeLexer $lexer): TypeLexer
+    public function manipulateToken(TraversingToken $token): TraversingToken
     {
-        return new TypeAliasLexer($lexer, $this->aliases);
+        $symbol = $token->symbol();
+
+        if (isset($this->aliases[$symbol])) {
+            return new TypeToken($this->aliases[$symbol], $symbol);
+        }
+
+        return $token;
     }
 
     public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser
diff --git a/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php b/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php
index 5462013..a48aba8 100644
--- a/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php
+++ b/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
 namespace CuyZ\Valinor\Type\Parser\Factory\Specifications;
 
 use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken;
 use CuyZ\Valinor\Type\Parser\TypeParser;
 
 /** @internal */
 interface TypeParserSpecification
 {
-    public function manipulateLexer(TypeLexer $lexer): TypeLexer;
+    public function manipulateToken(TraversingToken $token): TraversingToken;
 
     public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser;
 }
diff --git a/src/Type/Parser/Lexer/AliasLexer.php b/src/Type/Parser/Lexer/AliasLexer.php
deleted file mode 100644
index f9ac6cb..0000000
--- a/src/Type/Parser/Lexer/AliasLexer.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
-use CuyZ\Valinor\Utility\Reflection\PhpParser;
-use CuyZ\Valinor\Utility\Reflection\Reflection;
-use ReflectionClass;
-use ReflectionFunction;
-use Reflector;
-
-use function strtolower;
-
-/** @internal */
-final class AliasLexer implements TypeLexer
-{
-    public function __construct(
-        private TypeLexer $delegate,
-        /** @var ReflectionClass<object>|ReflectionFunction */
-        private Reflector $reflection
-    ) {}
-
-    public function tokenize(string $symbol): Token
-    {
-        $symbol = $this->resolve($symbol);
-
-        return $this->delegate->tokenize($symbol);
-    }
-
-    private function resolve(string $symbol): string
-    {
-        // Matches the case where a class extends a class with the same name but
-        // in a different namespace.
-        if ($symbol === $this->reflection->getShortName() && Reflection::classOrInterfaceExists($symbol)) {
-            return $symbol;
-        }
-
-        $alias = $this->resolveAlias($symbol);
-
-        if (strtolower($alias) !== strtolower($symbol)) {
-            return $alias;
-        }
-
-        $namespaced = $this->resolveNamespaced($symbol);
-
-        if ($namespaced !== $symbol) {
-            return $namespaced;
-        }
-
-        return $symbol;
-    }
-
-    private function resolveAlias(string $symbol): string
-    {
-        $alias = $symbol;
-
-        $namespaceParts = explode('\\', $symbol);
-        $lastPart = array_shift($namespaceParts);
-
-        if ($lastPart) {
-            $alias = strtolower($lastPart);
-        }
-
-        $aliases = PhpParser::parseUseStatements($this->reflection);
-
-        if (! isset($aliases[$alias])) {
-            return $symbol;
-        }
-
-        if ($aliases[$alias] === $symbol) {
-            return $symbol;
-        }
-
-        $full = $aliases[$alias];
-
-        if (! empty($namespaceParts)) {
-            $full .= '\\' . implode('\\', $namespaceParts);
-        }
-
-        return $full;
-    }
-
-    private function resolveNamespaced(string $symbol): string
-    {
-        $reflection = $this->reflection;
-
-        if ($reflection instanceof ReflectionFunction) {
-            $classReflection = $reflection->getClosureScopeClass();
-
-            if ($classReflection && $classReflection->getFileName() === $reflection->getFileName()) {
-                $reflection = $classReflection;
-            }
-        }
-
-        $namespace = $reflection->getNamespaceName();
-
-        if (! $namespace) {
-            return $symbol;
-        }
-
-        $full = $namespace . '\\' . $symbol;
-
-        if (Reflection::classOrInterfaceExists($full)) {
-            return $full;
-        }
-
-        return $symbol;
-    }
-}
diff --git a/src/Type/Parser/Lexer/ClassContextLexer.php b/src/Type/Parser/Lexer/ClassContextLexer.php
deleted file mode 100644
index b3af6bf..0000000
--- a/src/Type/Parser/Lexer/ClassContextLexer.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
-use CuyZ\Valinor\Utility\Reflection\Reflection;
-use ReflectionClass;
-
-/** @internal */
-final class ClassContextLexer implements TypeLexer
-{
-    private TypeLexer $delegate;
-
-    /** @var ReflectionClass<object> */
-    private ReflectionClass $reflection;
-
-    /**
-     * @param class-string $className
-     */
-    public function __construct(TypeLexer $delegate, string $className)
-    {
-        $this->delegate = $delegate;
-        $this->reflection = Reflection::class($className);
-    }
-
-    public function tokenize(string $symbol): Token
-    {
-        $symbol = $this->resolve($symbol);
-
-        return $this->delegate->tokenize($symbol);
-    }
-
-    private function resolve(string $symbol): string
-    {
-        if ($symbol === 'self' || $symbol === 'static') {
-            return $this->reflection->name;
-        }
-
-        return $symbol;
-    }
-}
diff --git a/src/Type/Parser/Lexer/ObjectLexer.php b/src/Type/Parser/Lexer/ObjectLexer.php
deleted file mode 100644
index 7f016fb..0000000
--- a/src/Type/Parser/Lexer/ObjectLexer.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Type\Parser\Lexer\Token\ClassNameToken;
-use CuyZ\Valinor\Type\Parser\Lexer\Token\EnumNameToken;
-use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
-use CuyZ\Valinor\Type\Parser\Lexer\Token\UnknownSymbolToken;
-use CuyZ\Valinor\Utility\Reflection\Reflection;
-use UnitEnum;
-
-/** @internal */
-final class ObjectLexer implements TypeLexer
-{
-    public function tokenize(string $symbol): Token
-    {
-        if (Reflection::enumExists($symbol)) {
-            /** @var class-string<UnitEnum> $symbol */
-            return new EnumNameToken($symbol);
-        }
-
-        if (Reflection::classOrInterfaceExists($symbol)) {
-            /** @var class-string $symbol */
-            return new ClassNameToken($symbol);
-        }
-
-        return new UnknownSymbolToken($symbol);
-    }
-}
diff --git a/src/Type/Parser/Lexer/SpecificationsLexer.php b/src/Type/Parser/Lexer/SpecificationsLexer.php
new file mode 100644
index 0000000..521863a
--- /dev/null
+++ b/src/Type/Parser/Lexer/SpecificationsLexer.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Type\Parser\Lexer;
+
+use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeParserSpecification;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\VacantToken;
+
+/** @internal */
+final class SpecificationsLexer implements TypeLexer
+{
+    public function __construct(
+        /** @var array<TypeParserSpecification> */
+        private array $specifications,
+    ) {}
+
+    public function tokenize(string $symbol): Token
+    {
+        return (new VacantToken($symbol, $this->specifications));
+    }
+}
diff --git a/src/Type/Parser/Lexer/Token/ArrayToken.php b/src/Type/Parser/Lexer/Token/ArrayToken.php
index 4bd8846..57bcd85 100644
--- a/src/Type/Parser/Lexer/Token/ArrayToken.php
+++ b/src/Type/Parser/Lexer/Token/ArrayToken.php
@@ -135,7 +135,7 @@ final class ArrayToken implements TraversingToken
                 throw new ShapedArrayClosingBracketMissing($elements, unsealedType: false);
             }
 
-            if ($stream->next() instanceof UnknownSymbolToken) {
+            if ($stream->next() instanceof VacantToken) {
                 $type = new StringValueType($stream->forward()->symbol());
             } elseif ($isUnsealed && $stream->next() instanceof ClosingCurlyBracketToken) {
                 $stream->forward();
diff --git a/src/Type/Parser/Lexer/Token/IntegerToken.php b/src/Type/Parser/Lexer/Token/IntegerToken.php
index 2f2e699..43b0597 100644
--- a/src/Type/Parser/Lexer/Token/IntegerToken.php
+++ b/src/Type/Parser/Lexer/Token/IntegerToken.php
@@ -34,7 +34,7 @@ final class IntegerToken implements TraversingToken
             throw new IntegerRangeMissingMinValue();
         }
 
-        if ($stream->next() instanceof UnknownSymbolToken) {
+        if ($stream->next()->symbol() === 'min') {
             $min = new IntegerValueType(PHP_INT_MIN);
             $stream->forward();
         } else {
@@ -53,7 +53,7 @@ final class IntegerToken implements TraversingToken
             throw new IntegerRangeMissingMaxValue($min);
         }
 
-        if ($stream->next() instanceof UnknownSymbolToken) {
+        if ($stream->next()->symbol() === 'max') {
             $max = new IntegerValueType(PHP_INT_MAX);
             $stream->forward();
         } else {
diff --git a/src/Type/Parser/Lexer/Token/ObjectToken.php b/src/Type/Parser/Lexer/Token/ObjectToken.php
new file mode 100644
index 0000000..4141dc8
--- /dev/null
+++ b/src/Type/Parser/Lexer/Token/ObjectToken.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Type\Parser\Lexer\Token;
+
+use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
+use CuyZ\Valinor\Type\Type;
+use CuyZ\Valinor\Utility\Reflection\Reflection;
+
+/** @internal */
+final class ObjectToken implements TraversingToken
+{
+    public function __construct(
+        /** @var class-string */
+        private string $className,
+    ) {}
+
+    public function traverse(TokenStream $stream): Type
+    {
+        return Reflection::enumExists($this->className)
+            ? (new EnumNameToken($this->className))->traverse($stream) // @phpstan-ignore-line / We know `$this->className` matches `class-string<UnitEnum>`
+            : (new ClassNameToken($this->className))->traverse($stream);
+    }
+
+    public function symbol(): string
+    {
+        return $this->className;
+    }
+}
diff --git a/src/Type/Parser/Lexer/Token/UnknownSymbolToken.php b/src/Type/Parser/Lexer/Token/UnknownSymbolToken.php
deleted file mode 100644
index 0104962..0000000
--- a/src/Type/Parser/Lexer/Token/UnknownSymbolToken.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Type\Parser\Lexer\Token;
-
-use CuyZ\Valinor\Type\Parser\Exception\UnknownSymbol;
-use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
-use CuyZ\Valinor\Type\Type;
-
-/** @internal */
-final class UnknownSymbolToken implements TraversingToken
-{
-    public function __construct(private string $symbol) {}
-
-    public function traverse(TokenStream $stream): Type
-    {
-        throw new UnknownSymbol($this->symbol);
-    }
-
-    public function symbol(): string
-    {
-        return $this->symbol;
-    }
-}
diff --git a/src/Type/Parser/Lexer/Token/VacantToken.php b/src/Type/Parser/Lexer/Token/VacantToken.php
new file mode 100644
index 0000000..53b2d0b
--- /dev/null
+++ b/src/Type/Parser/Lexer/Token/VacantToken.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Type\Parser\Lexer\Token;
+
+use CuyZ\Valinor\Type\Parser\Exception\UnknownSymbol;
+use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeParserSpecification;
+use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
+use CuyZ\Valinor\Type\Type;
+use CuyZ\Valinor\Utility\Reflection\Reflection;
+
+/** @internal */
+final class VacantToken implements TraversingToken
+{
+    public function __construct(
+        private string $symbol,
+        /** @var array<TypeParserSpecification> */
+        private array $specifications,
+    ) {}
+
+    public function traverse(TokenStream $stream): Type
+    {
+        $token = $this;
+
+        foreach ($this->specifications as $specification) {
+            $new = $specification->manipulateToken($token);
+
+            if ($new !== $token) {
+                return $new->traverse($stream);
+            }
+        }
+
+        if (Reflection::enumExists($this->symbol)) {
+            // @phpstan-ignore-next-line / We know `$this->symbol` matches `class-string<UnitEnum>`
+            return (new EnumNameToken($this->symbol))->traverse($stream);
+        }
+
+        if (Reflection::classOrInterfaceExists($this->symbol)) {
+            // @phpstan-ignore-next-line / We know `$this->symbol` matches `class-string`
+            return (new ClassNameToken($this->symbol))->traverse($stream);
+        }
+
+        throw new UnknownSymbol($this->symbol);
+    }
+
+    public function symbol(): string
+    {
+        return $this->symbol;
+    }
+}
diff --git a/src/Type/Parser/Lexer/TypeAliasLexer.php b/src/Type/Parser/Lexer/TypeAliasLexer.php
deleted file mode 100644
index 698a929..0000000
--- a/src/Type/Parser/Lexer/TypeAliasLexer.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
-use CuyZ\Valinor\Type\Parser\Lexer\Token\TypeToken;
-use CuyZ\Valinor\Type\Type;
-
-/** @internal */
-final class TypeAliasLexer implements TypeLexer
-{
-    public function __construct(
-        private TypeLexer $delegate,
-        /** @var array<string, Type> */
-        private array $aliases
-    ) {}
-
-    public function tokenize(string $symbol): Token
-    {
-        if (isset($this->aliases[$symbol])) {
-            return new TypeToken($this->aliases[$symbol], $symbol);
-        }
-
-        return $this->delegate->tokenize($symbol);
-    }
-}


Test patch:
diff --git a/tests/Fake/Type/Parser/Lexer/FakeTypeLexer.php b/tests/Fake/Type/Parser/Lexer/FakeTypeLexer.php
deleted file mode 100644
index ef205e2..0000000
--- a/tests/Fake/Type/Parser/Lexer/FakeTypeLexer.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token\FakeToken;
-use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeLexer;
-
-final class FakeTypeLexer implements TypeLexer
-{
-    /** @var array<string, Token> */
-    private array $tokens = [];
-
-    public function tokenize(string $symbol): Token
-    {
-        return $this->tokens[$symbol] ??= new FakeToken();
-    }
-
-    public function will(string $symbol, Token $token): void
-    {
-        $this->tokens[$symbol] = $token;
-    }
-}
diff --git a/tests/Fake/Type/Parser/Lexer/Token/FakeToken.php b/tests/Fake/Type/Parser/Lexer/Token/FakeToken.php
index de8e781..ee15f58 100644
--- a/tests/Fake/Type/Parser/Lexer/Token/FakeToken.php
+++ b/tests/Fake/Type/Parser/Lexer/Token/FakeToken.php
@@ -4,12 +4,24 @@ declare(strict_types=1);
 
 namespace CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token;
 
-use CuyZ\Valinor\Type\Parser\Lexer\Token\Token;
+use CuyZ\Valinor\Tests\Fake\Type\FakeType;
+use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken;
+use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
+use CuyZ\Valinor\Type\Type;
 
-final class FakeToken implements Token
+final class FakeToken implements TraversingToken
 {
+    public function __construct(
+        private string $symbol = 'fake-token',
+    ) {}
+
+    public function traverse(TokenStream $stream): Type
+    {
+        return new FakeType();
+    }
+
     public function symbol(): string
     {
-        return 'fake-token';
+        return $this->symbol;
     }
 }
diff --git a/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php b/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php
index 8f2a41c..b8336e2 100644
--- a/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php
+++ b/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php
@@ -45,7 +45,7 @@ use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingMaxValue;
 use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingMinValue;
 use CuyZ\Valinor\Type\Parser\Exception\Scalar\InvalidClassStringSubType;
 use CuyZ\Valinor\Type\Parser\Lexer\NativeLexer;
-use CuyZ\Valinor\Type\Parser\Lexer\ObjectLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\SpecificationsLexer;
 use CuyZ\Valinor\Type\Parser\LexingParser;
 use CuyZ\Valinor\Type\Parser\TypeParser;
 use CuyZ\Valinor\Type\StringType;
@@ -93,7 +93,7 @@ final class NativeLexerTest extends TestCase
     {
         parent::setUp();
 
-        $lexer = new NativeLexer(new ObjectLexer());
+        $lexer = new NativeLexer(new SpecificationsLexer([]));
 
         $this->parser = new LexingParser($lexer);
     }
diff --git a/tests/Integration/Mapping/ShapedArrayWithKeyMatchingImportedClassNameMappingTest.php b/tests/Integration/Mapping/ShapedArrayWithKeyMatchingImportedClassNameMappingTest.php
new file mode 100644
index 0000000..b9a321d
--- /dev/null
+++ b/tests/Integration/Mapping/ShapedArrayWithKeyMatchingImportedClassNameMappingTest.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Tests\Integration\Mapping {
+
+    use CuyZ\Valinor\Mapper\MappingError;
+    use CuyZ\Valinor\Tests\Integration\IntegrationTestCase;
+    use SomeNamespaceB\ClassWithShapedArrayWithKeyMatchingImportedClassName;
+
+    final class ShapedArrayWithKeyMatchingImportedClassNameMappingTest extends IntegrationTestCase
+    {
+        public function test_shaped_array_with_key_matching_imported_class_name_can_be_mapped_properly(): void
+        {
+            try {
+                $object = $this->mapperBuilder()->mapper()->map(ClassWithShapedArrayWithKeyMatchingImportedClassName::class, [
+                    'Element' => 'Some element',
+                ]);
+            } catch (MappingError $error) {
+                $this->mappingFail($error);
+            }
+
+            self::assertSame('Some element', $object->elements['Element']->value);
+        }
+    }
+}
+
+namespace SomeNamespaceA {
+
+    final class Element
+    {
+        public function __construct(
+            public string $value,
+        ) {}
+    }
+}
+
+namespace SomeNamespaceB {
+
+    use SomeNamespaceA\Element;
+
+    final class ClassWithShapedArrayWithKeyMatchingImportedClassName
+    {
+        public function __construct(
+            /** @var array{Element: Element} */
+            public array $elements,
+        ) {}
+    }
+}
diff --git a/tests/Unit/Type/Parser/Factory/Specification/AliasSpecificationTest.php b/tests/Unit/Type/Parser/Factory/Specification/AliasSpecificationTest.php
new file mode 100644
index 0000000..782d240
--- /dev/null
+++ b/tests/Unit/Type/Parser/Factory/Specification/AliasSpecificationTest.php
@@ -0,0 +1,138 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Factory\Specification;
+
+use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token\FakeToken;
+use CuyZ\Valinor\Tests\Unit\Type\Parser\Factory\Specification as PathAlias;
+use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification;
+use DateTimeInterface as SecondClassAlias;
+use ObjectWithNameMatchingRootNamespace\ObjectWithNameMatchingRootNamespace;
+use PHPUnit\Framework\TestCase;
+use ReflectionClass;
+use ReflectionFunction;
+use stdClass;
+use stdClass as FirstClassAlias;
+
+final class AliasSpecificationTest extends TestCase
+{
+    public function test_resolve_unsupported_type_in_class_returns_same_type(): void
+    {
+        $specification = new AliasSpecification(new ReflectionClass(stdClass::class));
+
+        $token = new FakeToken('SomeType');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame($token, $newToken);
+    }
+
+    public function test_class_alias_are_checked_in_class(): void
+    {
+        $specification = new AliasSpecification(new ReflectionClass(ClassWithAlias::class));
+
+        $token = new FakeToken('FirstClassAlias');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(stdClass::class, $newToken->symbol());
+    }
+
+    public function test_class_alias_with_namespace_parts_is_checked_in_class(): void
+    {
+        $specification = new AliasSpecification(new ReflectionClass(ClassWithAlias::class));
+
+        $token = new FakeToken('PathAlias\\AliasSpecificationTest');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(AliasSpecificationTest::class, $newToken->symbol());
+    }
+
+    public function test_same_namespace_classes_are_checked_in_class(): void
+    {
+        $specification = new AliasSpecification(new ReflectionClass(ClassWithAlias::class));
+
+        $token = new FakeToken('ClassWithAlias');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(ClassWithAlias::class, $newToken->symbol());
+    }
+
+    public function test_object_with_same_name_as_root_namespace_are_checked_in_class(): void
+    {
+        $specification = new AliasSpecification(new ReflectionClass(ClassWithAlias::class));
+
+        $token = new FakeToken(ObjectWithNameMatchingRootNamespace::class);
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(ObjectWithNameMatchingRootNamespace::class, $newToken->symbol());
+    }
+
+    public function test_resolve_unsupported_type_in_function_returns_same_type(): void
+    {
+        $function = fn () => 42;
+
+        $specification = new AliasSpecification(new ReflectionFunction($function));
+
+        $token = new FakeToken('SomeType');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame($token, $newToken);
+    }
+
+    public function test_class_alias_are_checked_in_function(): void
+    {
+        $function = fn () => 42;
+
+        $specification = new AliasSpecification(new ReflectionFunction($function));
+
+        $token = new FakeToken('FirstClassAlias');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(stdClass::class, $newToken->symbol());
+    }
+
+    public function test_class_alias_with_namespace_parts_is_checked_in_function(): void
+    {
+        $function = fn () => 42;
+
+        $specification = new AliasSpecification(new ReflectionFunction($function));
+
+        $token = new FakeToken('PathAlias\\AliasSpecificationTest');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(AliasSpecificationTest::class, $newToken->symbol());
+    }
+
+    public function test_same_namespace_classes_are_checked_in_function(): void
+    {
+        $function = fn () => 42;
+
+        $specification = new AliasSpecification(new ReflectionFunction($function));
+
+        $token = new FakeToken('ClassWithAlias');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(ClassWithAlias::class, $newToken->symbol());
+    }
+
+    public function test_object_with_same_name_as_root_namespace_are_checked_in_function(): void
+    {
+        $function = fn () => 42;
+
+        $specification = new AliasSpecification(new ReflectionFunction($function));
+
+        $token = new FakeToken(ObjectWithNameMatchingRootNamespace::class);
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(ObjectWithNameMatchingRootNamespace::class, $newToken->symbol());
+    }
+}
+
+final class ClassWithAlias
+{
+    public FirstClassAlias $propertyWithFirstAliasType;
+
+    public SecondClassAlias $propertyWithSecondAliasType;
+
+    public PathAlias\AliasSpecificationTest $propertyWithPathAliasType;
+}
diff --git a/tests/Unit/Type/Parser/Factory/Specification/ClassContextSpecificationTest.php b/tests/Unit/Type/Parser/Factory/Specification/ClassContextSpecificationTest.php
new file mode 100644
index 0000000..4553313
--- /dev/null
+++ b/tests/Unit/Type/Parser/Factory/Specification/ClassContextSpecificationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Factory\Specification;
+
+use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token\FakeToken;
+use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
+use PHPUnit\Framework\TestCase;
+use stdClass;
+
+final class ClassContextSpecificationTest extends TestCase
+{
+    public function test_self_returns_class_name(): void
+    {
+        $specification = new ClassContextSpecification(stdClass::class);
+
+        $token = new FakeToken('self');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(stdClass::class, $newToken->symbol());
+    }
+
+    public function test_static_returns_class_name(): void
+    {
+        $specification = new ClassContextSpecification(stdClass::class);
+
+        $token = new FakeToken('static');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame(stdClass::class, $newToken->symbol());
+    }
+}
diff --git a/tests/Unit/Type/Parser/Factory/Specification/TypeAliasAssignerSpecificationTest.php b/tests/Unit/Type/Parser/Factory/Specification/TypeAliasAssignerSpecificationTest.php
new file mode 100644
index 0000000..35063d8
--- /dev/null
+++ b/tests/Unit/Type/Parser/Factory/Specification/TypeAliasAssignerSpecificationTest.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Factory\Specification;
+
+use CuyZ\Valinor\Tests\Fake\Type\FakeType;
+use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token\FakeToken;
+use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeAliasAssignerSpecification;
+use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
+use PHPUnit\Framework\TestCase;
+
+final class TypeAliasAssignerSpecificationTest extends TestCase
+{
+    public function test_non_generic_symbol_is_handled_by_delegate(): void
+    {
+        $specification = new TypeAliasAssignerSpecification(['TemplateA' => new FakeType()]);
+
+        $token = new FakeToken('foo');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame($token, $newToken);
+    }
+
+    public function test_symbol_is_generic_is_returned(): void
+    {
+        $type = new FakeType();
+        $specification = new TypeAliasAssignerSpecification(['Template' => $type]);
+
+        $token = new FakeToken('Template');
+        $newToken = $specification->manipulateToken($token);
+
+        self::assertSame($type, $newToken->traverse(new TokenStream()));
+    }
+}
diff --git a/tests/Unit/Type/Parser/Lexer/AliasLexerTest.php b/tests/Unit/Type/Parser/Lexer/AliasLexerTest.php
deleted file mode 100644
index cd6ec91..0000000
--- a/tests/Unit/Type/Parser/Lexer/AliasLexerTest.php
+++ /dev/null
@@ -1,158 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\FakeTypeLexer;
-use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token\FakeToken;
-use CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer as PathAlias;
-use CuyZ\Valinor\Type\Parser\Lexer\AliasLexer;
-use DateTimeInterface as SecondClassAlias;
-use ObjectWithNameMatchingRootNamespace\ObjectWithNameMatchingRootNamespace;
-use PHPUnit\Framework\TestCase;
-use ReflectionClass;
-use ReflectionFunction;
-use stdClass;
-use stdClass as FirstClassAlias;
-
-final class AliasLexerTest extends TestCase
-{
-    private FakeTypeLexer $delegate;
-
-    protected function setUp(): void
-    {
-        parent::setUp();
-
-        $this->delegate = new FakeTypeLexer();
-    }
-
-    public function test_resolve_unsupported_type_in_class_returns_same_type(): void
-    {
-        $lexer = new AliasLexer($this->delegate, new ReflectionClass(stdClass::class));
-        $symbol = 'SomeType';
-        $token = new FakeToken();
-
-        $this->delegate->will($symbol, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_class_alias_are_checked_in_class(): void
-    {
-        $lexer = new AliasLexer($this->delegate, new ReflectionClass(ClassWithAlias::class));
-        $symbol = 'FirstClassAlias';
-        $token = new FakeToken();
-
-        $this->delegate->will(stdClass::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_class_alias_with_namespace_parts_is_checked_in_class(): void
-    {
-        $lexer = new AliasLexer($this->delegate, new ReflectionClass(ClassWithAlias::class));
-        $symbol = 'PathAlias\\AliasLexerTest';
-        $token = new FakeToken();
-
-        $this->delegate->will(self::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_same_namespace_classes_are_checked_in_class(): void
-    {
-        $lexer = new AliasLexer($this->delegate, new ReflectionClass(ClassWithAlias::class));
-        $symbol = 'ClassWithAlias';
-        $token = new FakeToken();
-
-        $this->delegate->will(ClassWithAlias::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_object_with_same_name_as_root_namespace_are_checked_in_class(): void
-    {
-        $lexer = new AliasLexer($this->delegate, new ReflectionClass(ClassWithAlias::class));
-        $symbol = ObjectWithNameMatchingRootNamespace::class;
-        $token = new FakeToken();
-
-        $this->delegate->will(ObjectWithNameMatchingRootNamespace::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_resolve_unsupported_type_in_function_returns_same_type(): void
-    {
-        $function = fn () => 42;
-
-        $lexer = new AliasLexer($this->delegate, new ReflectionFunction($function));
-        $symbol = 'SomeType';
-        $token = new FakeToken();
-
-        $this->delegate->will($symbol, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_class_alias_are_checked_in_function(): void
-    {
-        $function = fn () => 42;
-
-        $lexer = new AliasLexer($this->delegate, new ReflectionFunction($function));
-        $symbol = 'FirstClassAlias';
-        $token = new FakeToken();
-
-        $this->delegate->will(stdClass::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_class_alias_with_namespace_parts_is_checked_in_function(): void
-    {
-        $function = fn () => 42;
-
-        $lexer = new AliasLexer($this->delegate, new ReflectionFunction($function));
-        $symbol = 'PathAlias\\AliasLexerTest';
-        $token = new FakeToken();
-
-        $this->delegate->will(self::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_same_namespace_classes_are_checked_in_function(): void
-    {
-        $function = fn () => 42;
-
-        $lexer = new AliasLexer($this->delegate, new ReflectionFunction($function));
-        $symbol = 'ClassWithAlias';
-        $token = new FakeToken();
-
-        $this->delegate->will(ClassWithAlias::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_object_with_same_name_as_root_namespace_are_checked_in_function(): void
-    {
-        $function = fn () => 42;
-
-        $lexer = new AliasLexer($this->delegate, new ReflectionFunction($function));
-        $symbol = ObjectWithNameMatchingRootNamespace::class;
-        $token = new FakeToken();
-
-        $this->delegate->will(ObjectWithNameMatchingRootNamespace::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-}
-
-final class ClassWithAlias
-{
-    public FirstClassAlias $propertyWithFirstAliasType;
-
-    public SecondClassAlias $propertyWithSecondAliasType;
-
-    public PathAlias\AliasLexerTest $propertyWithPathAliasType;
-}
diff --git a/tests/Unit/Type/Parser/Lexer/ClassContextLexerTest.php b/tests/Unit/Type/Parser/Lexer/ClassContextLexerTest.php
deleted file mode 100644
index 01a7654..0000000
--- a/tests/Unit/Type/Parser/Lexer/ClassContextLexerTest.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\FakeTypeLexer;
-use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token\FakeToken;
-use CuyZ\Valinor\Type\Parser\Lexer\ClassContextLexer;
-use PHPUnit\Framework\TestCase;
-use stdClass;
-
-final class ClassContextLexerTest extends TestCase
-{
-    private FakeTypeLexer $delegate;
-
-    protected function setUp(): void
-    {
-        parent::setUp();
-
-        $this->delegate = new FakeTypeLexer();
-    }
-
-    public function test_self_returns_class_name(): void
-    {
-        $lexer = new ClassContextLexer($this->delegate, stdClass::class);
-        $symbol = 'self';
-        $token = new FakeToken();
-
-        $this->delegate->will(stdClass::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_static_returns_class_name(): void
-    {
-        $lexer = new ClassContextLexer($this->delegate, stdClass::class);
-        $symbol = 'static';
-        $token = new FakeToken();
-
-        $this->delegate->will(stdClass::class, $token);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-}
diff --git a/tests/Unit/Type/Parser/Lexer/NativeLexerTest.php b/tests/Unit/Type/Parser/Lexer/NativeLexerTest.php
index 289bf3a..85bb32d 100644
--- a/tests/Unit/Type/Parser/Lexer/NativeLexerTest.php
+++ b/tests/Unit/Type/Parser/Lexer/NativeLexerTest.php
@@ -6,16 +6,14 @@ 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\ObjectLexer;
+use CuyZ\Valinor\Type\Parser\Lexer\SpecificationsLexer;
 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;
@@ -29,7 +27,7 @@ use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningCurlyBracketToken;
 use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningSquareBracketToken;
 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 CuyZ\Valinor\Type\Parser\Lexer\Token\VacantToken;
 use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\TestCase;
 use stdClass;
@@ -42,7 +40,7 @@ final class NativeLexerTest extends TestCase
     {
         parent::setUp();
 
-        $this->lexer = new NativeLexer(new ObjectLexer());
+        $this->lexer = new NativeLexer(new SpecificationsLexer([]));
     }
 
     /**
@@ -145,16 +143,16 @@ final class NativeLexerTest extends TestCase
         ];
         yield 'class' => [
             'symbol' => stdClass::class,
-            'token' => ClassNameToken::class,
+            'token' => VacantToken::class,
         ];
         yield 'unknown' => [
             'symbol' => 'unknown',
-            'token' => UnknownSymbolToken::class,
+            'token' => VacantToken::class,
         ];
 
         yield 'enum' => [
             'symbol' => PureEnum::class,
-            'token' => EnumNameToken::class,
+            'token' => VacantToken::class,
         ];
     }
 }
diff --git a/tests/Unit/Type/Parser/Lexer/TypeAliasLexerTest.php b/tests/Unit/Type/Parser/Lexer/TypeAliasLexerTest.php
deleted file mode 100644
index 164d2c9..0000000
--- a/tests/Unit/Type/Parser/Lexer/TypeAliasLexerTest.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer;
-
-use CuyZ\Valinor\Tests\Fake\Type\FakeType;
-use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\FakeTypeLexer;
-use CuyZ\Valinor\Tests\Fake\Type\Parser\Lexer\Token\FakeToken;
-use CuyZ\Valinor\Type\Parser\Lexer\TypeAliasLexer;
-use CuyZ\Valinor\Type\Parser\Lexer\Token\TypeToken;
-use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
-use PHPUnit\Framework\TestCase;
-
-final class TypeAliasLexerTest extends TestCase
-{
-    private FakeTypeLexer $delegate;
-
-    protected function setUp(): void
-    {
-        parent::setUp();
-
-        $this->delegate = new FakeTypeLexer();
-    }
-
-    public function test_non_generic_symbol_is_handled_by_delegate(): void
-    {
-        $symbol = 'foo';
-        $token = new FakeToken();
-
-        $this->delegate->will($symbol, $token);
-
-        $lexer = new TypeAliasLexer($this->delegate, ['TemplateA' => new FakeType()]);
-
-        self::assertSame($token, $lexer->tokenize($symbol));
-    }
-
-    public function test_symbol_is_generic_is_returned(): void
-    {
-        $type = new FakeType();
-
-        $lexer = new TypeAliasLexer($this->delegate, ['Template' => $type]);
-
-        $result = $lexer->tokenize('Template');
-
-        self::assertInstanceOf(TypeToken::class, $result);
-        self::assertSame($type, $result->traverse(new TokenStream()));
-    }
-}