From 171e2eb487095bb08c03ae1d2bf412af9dc45bc8 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 14:00:52 +0300 Subject: [PATCH 001/119] Add php 8.1 support to the type-lang/types package --- libs/types/.github/workflows/tests.yml | 2 +- libs/types/README.md | 2 +- libs/types/composer.json | 4 +- libs/types/phpstan.neon | 4 + libs/types/src/Identifier.php | 91 ++++++++----- libs/types/src/Literal/LiteralNode.php | 18 +++ .../src/Literal/LiteralNodeInterface.php | 12 +- libs/types/src/Literal/StringLiteralNode.php | 6 +- libs/types/src/Name.php | 121 ++++++++++++------ libs/types/src/Node.php | 7 + libs/types/src/NodeInterface.php | 8 +- libs/types/src/NodeList.php | 71 +++++++--- libs/types/src/Shape/ClassConstFieldNode.php | 15 ++- .../src/Shape/ClassConstMaskFieldNode.php | 15 ++- libs/types/src/Shape/ConstMaskFieldNode.php | 9 +- libs/types/src/Shape/ExplicitFieldNode.php | 30 ++++- libs/types/src/Shape/NamedFieldNode.php | 9 +- libs/types/src/Shape/NumericFieldNode.php | 9 +- libs/types/src/Shape/StringNamedFieldNode.php | 9 +- 19 files changed, 305 insertions(+), 137 deletions(-) diff --git a/libs/types/.github/workflows/tests.yml b/libs/types/.github/workflows/tests.yml index a7df15d..febc976 100644 --- a/libs/types/.github/workflows/tests.yml +++ b/libs/types/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5', '8.6' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/types/README.md b/libs/types/README.md index d775cc4..20db434 100644 --- a/libs/types/README.md +++ b/libs/types/README.md @@ -35,7 +35,7 @@ composer require type-lang/types ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/types/composer.json b/libs/types/composer.json index f9c8a01..4de2110 100644 --- a/libs/types/composer.json +++ b/libs/types/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/types/issues" }, "require": { - "php": "^8.4" + "php": "^8.1" }, "autoload": { "psr-4": { @@ -18,7 +18,7 @@ }, "require-dev": { "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4" + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" }, "autoload-dev": { "psr-4": { diff --git a/libs/types/phpstan.neon b/libs/types/phpstan.neon index 018e46e..2ccfbac 100644 --- a/libs/types/phpstan.neon +++ b/libs/types/phpstan.neon @@ -1,6 +1,10 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 fileExtensions: - php paths: - src + reportUnmatchedIgnoredErrors: false diff --git a/libs/types/src/Identifier.php b/libs/types/src/Identifier.php index 579e4c1..9b39ba2 100644 --- a/libs/types/src/Identifier.php +++ b/libs/types/src/Identifier.php @@ -6,13 +6,26 @@ /** * @phpstan-consistent-constructor + * + * @property-read bool $isVirtual An alias of {@see isVirtual()} method. + * @property-read bool $isSpecial An alias of {@see isSpecial()} method. + * @property-read bool $isBuiltin An alias of {@see isBuiltin()} method. */ final class Identifier extends Node implements \Stringable { /** * @var list */ - protected const array SPECIAL_CLASS_NAME = [ + private const VIRTUAL_PROPERTIES = [ + 'isVirtual', + 'isSpecial', + 'isBuiltin', + ]; + + /** + * @var list + */ + protected const SPECIAL_CLASS_NAME = [ 'self', 'parent', 'static', @@ -21,7 +34,7 @@ final class Identifier extends Node implements \Stringable /** * @var list */ - protected const array BUILTIN_TYPE_NAME = [ + protected const BUILTIN_TYPE_NAME = [ 'mixed', 'string', 'int', @@ -38,33 +51,6 @@ final class Identifier extends Node implements \Stringable 'false', ]; - /** - * Returns {@see true} if the identifier contains the name of - * a "virtual" type, i.e. invalid in the PHP namespace. - * - * - `SomeClass` - Non-virtual, can be a type in PHP. - * - `false` - Non-virtual, can be a type in PHP. - * - `non-empty-array` - Virtual, cannot be defined in PHP. - * - `empty-string` - Virtual, cannot be defined in PHP. - */ - public bool $isVirtual { - get => \str_contains($this->value, '-'); - } - - /** - * Returns {@see true} in case of name contains special class reference. - */ - public bool $isSpecial { - get => self::isLooksLikeSpecial($this->value); - } - - /** - * Returns {@see true} in case of name contains builtin type name. - */ - public bool $isBuiltin { - get => self::isLooksLikeBuiltin($this->value); - } - public function __construct( /** * @var non-empty-string @@ -105,6 +91,53 @@ public static function isLooksLikeBuiltin(string $value): bool return \in_array(\strtolower($value), self::BUILTIN_TYPE_NAME, true); } + /** + * Returns {@see true} if the identifier contains the name of a "virtual" + * type, i.e. invalid in the PHP namespace. + * + * - `SomeClass` - Non-virtual, can be a type in PHP. + * - `false` - Non-virtual, can be a type in PHP. + * - `non-empty-array` - Virtual, cannot be defined in PHP. + * - `empty-string` - Virtual, cannot be defined in PHP. + */ + public function isVirtual(): bool + { + return \str_contains($this->value, '-'); + } + + /** + * Returns {@see true} in case of name contains special class reference. + */ + public function isSpecial(): bool + { + return self::isLooksLikeSpecial($this->value); + } + + /** + * Returns {@see true} in case of name contains builtin type name. + */ + public function isBuiltin(): bool + { + return self::isLooksLikeBuiltin($this->value); + } + + public function __get(string $name): mixed + { + return match ($name) { + 'isVirtual' => $this->isVirtual(), + 'isSpecial' => $this->isSpecial(), + 'isBuiltin' => $this->isBuiltin(), + default => throw new \OutOfRangeException( + message: \sprintf('Undefined property %s::$%s', self::class, $name), + ), + }; + } + + public function __isset(string $name): bool + { + return \in_array($name, self::VIRTUAL_PROPERTIES, true); + } + /** * Returns name as string. * diff --git a/libs/types/src/Literal/LiteralNode.php b/libs/types/src/Literal/LiteralNode.php index 6b490bb..e83b230 100644 --- a/libs/types/src/Literal/LiteralNode.php +++ b/libs/types/src/Literal/LiteralNode.php @@ -15,12 +15,30 @@ abstract class LiteralNode extends TypeNode implements LiteralNodeInterface { public function __construct( /** + * An alias of {@see value()} method. + * * @var TValue */ public readonly mixed $value, + /** + * An alias of {@see raw()} method. + */ public readonly string $raw, ) {} + /** + * @return TValue + */ + public function value(): mixed + { + return $this->value; + } + + public function raw(): string + { + return $this->raw; + } + public function __toString(): string { return $this->raw; diff --git a/libs/types/src/Literal/LiteralNodeInterface.php b/libs/types/src/Literal/LiteralNodeInterface.php index cababb6..40bc5f1 100644 --- a/libs/types/src/Literal/LiteralNodeInterface.php +++ b/libs/types/src/Literal/LiteralNodeInterface.php @@ -12,21 +12,17 @@ interface LiteralNodeInterface extends \Stringable /** * Gets a PHP representation of the literal value. * - * @var TValue + * @return TValue */ - public mixed $value { - get; - } + public function value(): mixed; /** * Gets the original literal value specified in the token. */ - public string $raw { - get; - } + public function raw(): string; /** - * Returns the processed ({@see $value}) literal value as a string. + * Returns the processed ({@see value()}) literal value as a string. */ public function __toString(): string; } diff --git a/libs/types/src/Literal/StringLiteralNode.php b/libs/types/src/Literal/StringLiteralNode.php index 553bd24..6e86455 100644 --- a/libs/types/src/Literal/StringLiteralNode.php +++ b/libs/types/src/Literal/StringLiteralNode.php @@ -14,17 +14,17 @@ final class StringLiteralNode extends LiteralNode implements ParsableLiteralNode /** * @var non-empty-string */ - private const string UTF_SEQUENCE_PATTERN = '/(? */ - private const array ESCAPED_CHARS = [ + private const ESCAPED_CHARS = [ '\n' => "\n", '\r' => "\r", '\t' => "\t", diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index c9b6237..e2cddd3 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -8,55 +8,37 @@ * @phpstan-consistent-constructor * * @template-implements \IteratorAggregate + * + * @property-read Identifier $first An alias of {@see first()} method. + * @property-read Identifier $last An alias of {@see last()} method. + * @property-read bool $isSimple An alias of {@see isSimple()} method. + * @property-read bool $isSpecial An alias of {@see isSpecial()} method. + * @property-read bool $isBuiltin An alias of {@see isBuiltin()} method. */ final class Name extends Node implements \IteratorAggregate, \Countable, \Stringable { /** - * @var non-empty-string + * @var list */ - private const string NAMESPACE_DELIMITER = '\\'; - - public const bool IS_FULLY_QUALIFIED_DEFAULT_VALUE = false; + private const VIRTUAL_PROPERTIES = [ + 'first', + 'last', + 'isSimple', + 'isSpecial', + 'isBuiltin', + ]; /** - * @var non-empty-list - */ - public array $segments; - - /** - * Gets the first segment of a name - */ - public Identifier $first { - get => $this->segments[0]; - } - - /** - * Gets the last segment of a name - */ - public Identifier $last { - get => $this->segments[\count($this->segments) - 1]; - } - - /** - * Gets whether the name is simple. + * @var non-empty-string */ - public bool $isSimple { - get => \count($this->segments) === 1; - } + private const NAMESPACE_DELIMITER = '\\'; - /** - * Gets {@see true} in case of name contains special class reference. - */ - public bool $isSpecial { - get => $this->isSimple && $this->first->isSpecial; - } + public const IS_FULLY_QUALIFIED_DEFAULT_VALUE = false; /** - * Gets {@see true} in case of name contains builtin type name. + * @var non-empty-list */ - public bool $isBuiltin { - get => $this->isSimple && $this->first->isBuiltin; - } + public array $segments; /** * @param iterable $segments @@ -65,7 +47,11 @@ public function __construct( iterable $segments, public readonly bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, ) { - $segments = \iterator_to_array($segments, false); + $segments = match (true) { + $segments instanceof \Traversable => \iterator_to_array($segments, false), + \array_is_list($segments) => $segments, + default => \array_values($segments), + }; \assert($segments !== [], new \InvalidArgumentException('Name segments count can not be empty')); @@ -321,6 +307,65 @@ public function count(): int return \count($this->segments); } + /** + * Gets the first segment of a name + */ + public function first(): Identifier + { + return $this->segments[0]; + } + + /** + * Gets the last segment of a name + */ + public function last(): Identifier + { + return $this->segments[\count($this->segments) - 1]; + } + + /** + * Gets whether the name is simple. + */ + public function isSimple(): bool + { + return \count($this->segments) === 1; + } + + /** + * Gets {@see true} in case of name contains special class reference. + */ + public function isSpecial(): bool + { + return $this->isSimple() && $this->first()->isSpecial(); + } + + /** + * Gets {@see true} in case of name contains builtin type name. + */ + public function isBuiltin(): bool + { + return $this->isSimple() && $this->first()->isBuiltin(); + } + + public function __get(string $name): mixed + { + return match ($name) { + 'first' => $this->first(), + 'last' => $this->last(), + 'isSimple' => $this->isSimple(), + 'isSpecial' => $this->isSpecial(), + 'isBuiltin' => $this->isBuiltin(), + default => throw new \OutOfRangeException( + message: \sprintf('Undefined property %s::$%s', self::class, $name), + ), + }; + } + + public function __isset(string $name): bool + { + return \in_array($name, self::VIRTUAL_PROPERTIES, true); + } + /** * @return non-empty-string */ diff --git a/libs/types/src/Node.php b/libs/types/src/Node.php index 09c28db..98e371c 100644 --- a/libs/types/src/Node.php +++ b/libs/types/src/Node.php @@ -7,7 +7,14 @@ abstract class Node implements NodeInterface { /** + * An alias of {@see offset()} method. + * * @var int<0, max> */ public int $offset = 0; + + public function offset(): int + { + return $this->offset; + } } diff --git a/libs/types/src/NodeInterface.php b/libs/types/src/NodeInterface.php index 2880840..fc2d056 100644 --- a/libs/types/src/NodeInterface.php +++ b/libs/types/src/NodeInterface.php @@ -15,16 +15,14 @@ interface NodeInterface * ```php * $position = Phplrt\Position\Position::fromOffset( * source: \file_get_contents($filename), - * offset: $node->offset, + * offset: $node->offset(), * ); * * echo 'line: ' . $position->getLine() . "\n" * 'column: ' . $position->getColumn(); * ``` * - * @var int<0, max> + * @return int<0, max> */ - public int $offset { - get; - } + public function offset(): int; } diff --git a/libs/types/src/NodeList.php b/libs/types/src/NodeList.php index fcbee93..6b13f69 100644 --- a/libs/types/src/NodeList.php +++ b/libs/types/src/NodeList.php @@ -9,6 +9,9 @@ * * @template-implements \IteratorAggregate * @template-implements \ArrayAccess, TNode> + * + * @property-read TNode|null $first An alias of {@see first()} method. + * @property-read TNode|null $last An alias of {@see last()} method. */ abstract class NodeList extends Node implements \IteratorAggregate, @@ -16,32 +19,26 @@ abstract class NodeList extends Node implements \Countable { /** - * @var list - */ - public array $items = []; - - /** - * @var TNode|null + * @var list */ - public ?Node $first { - get => $this->items[0] ?? null; - } + private const VIRTUAL_PROPERTIES = [ + 'first', + 'last', + ]; /** - * @var TNode|null + * @var list */ - public ?Node $last { - get => ($lastKey = \array_key_last($this->items)) !== null - ? $this->items[$lastKey] - : null; - } + public array $items = []; /** * @param iterable $items */ public function __construct(iterable $items = []) { - $this->items = \iterator_to_array($items, false); + $this->items = \is_array($items) + ? \array_values($items) + : \iterator_to_array($items, false); } /** @@ -118,4 +115,46 @@ public function count(): int { return \count($this->items); } + + /** + * Gets the first node of a list or {@see null} in case of the list is empty. + * + * @return TNode|null + */ + public function first(): ?Node + { + return $this->items[0] ?? null; + } + + /** + * Gets the last node of a list or {@see null} in case of the list is empty. + * + * @return TNode|null + */ + public function last(): ?Node + { + $key = \array_key_last($this->items); + + if ($key === null) { + return null; + } + + return $this->items[$key]; + } + + public function __get(string $name): mixed + { + return match ($name) { + 'first' => $this->first(), + 'last' => $this->last(), + default => throw new \OutOfRangeException( + message: \sprintf('Undefined property %s::$%s', static::class, $name), + ), + }; + } + + public function __isset(string $name): bool + { + return \in_array($name, self::VIRTUAL_PROPERTIES, true); + } } diff --git a/libs/types/src/Shape/ClassConstFieldNode.php b/libs/types/src/Shape/ClassConstFieldNode.php index 2f17d40..0151c65 100644 --- a/libs/types/src/Shape/ClassConstFieldNode.php +++ b/libs/types/src/Shape/ClassConstFieldNode.php @@ -13,13 +13,6 @@ */ final class ClassConstFieldNode extends ExplicitFieldNode { - public string $index { - get => \vsprintf('%s::%s', [ - $this->key->class->toString(), - $this->key->constant->toString(), - ]); - } - public function __construct( ClassConstNode $key, TypeNode $type, @@ -33,4 +26,12 @@ public function __construct( attributes: $attributes, ); } + + public function index(): string + { + return \vsprintf('%s::%s', [ + $this->key->class->toString(), + $this->key->constant->toString(), + ]); + } } diff --git a/libs/types/src/Shape/ClassConstMaskFieldNode.php b/libs/types/src/Shape/ClassConstMaskFieldNode.php index 6e4089a..0c50a73 100644 --- a/libs/types/src/Shape/ClassConstMaskFieldNode.php +++ b/libs/types/src/Shape/ClassConstMaskFieldNode.php @@ -13,13 +13,6 @@ */ final class ClassConstMaskFieldNode extends ExplicitFieldNode { - public string $index { - get => \vsprintf('%s::%s*', [ - $this->key->class->toString(), - $this->key->constant?->toString(), - ]); - } - public function __construct( ClassConstMaskNode $key, TypeNode $type, @@ -33,4 +26,12 @@ public function __construct( attributes: $attributes, ); } + + public function index(): string + { + return \vsprintf('%s::%s*', [ + $this->key->class->toString(), + $this->key->constant?->toString(), + ]); + } } diff --git a/libs/types/src/Shape/ConstMaskFieldNode.php b/libs/types/src/Shape/ConstMaskFieldNode.php index 6642a86..b2d4847 100644 --- a/libs/types/src/Shape/ConstMaskFieldNode.php +++ b/libs/types/src/Shape/ConstMaskFieldNode.php @@ -13,10 +13,6 @@ */ final class ConstMaskFieldNode extends ExplicitFieldNode { - public string $index { - get => (string) $this->key; - } - public function __construct( ConstMaskNode $key, TypeNode $type, @@ -30,4 +26,9 @@ public function __construct( attributes: $attributes, ); } + + public function index(): string + { + return (string) $this->key; + } } diff --git a/libs/types/src/Shape/ExplicitFieldNode.php b/libs/types/src/Shape/ExplicitFieldNode.php index 6e90c96..fbd933a 100644 --- a/libs/types/src/Shape/ExplicitFieldNode.php +++ b/libs/types/src/Shape/ExplicitFieldNode.php @@ -9,15 +9,17 @@ /** * @template TKey of mixed + * + * @property-read string $index An alias of {@see index()} method. */ abstract class ExplicitFieldNode extends FieldNode { /** - * Gets a pretty-printed string representation of the key + * @var list */ - abstract public string $index { - get; - } + private const VIRTUAL_PROPERTIES = [ + 'index', + ]; public function __construct( /** @@ -34,4 +36,24 @@ public function __construct( attributes: $attributes, ); } + + /** + * Gets a pretty-printed string representation of the key + */ + abstract public function index(): string; + + public function __get(string $name): mixed + { + return match ($name) { + 'index' => $this->index(), + default => throw new \OutOfRangeException( + message: \sprintf('Undefined property %s::$%s', static::class, $name), + ), + }; + } + + public function __isset(string $name): bool + { + return \in_array($name, self::VIRTUAL_PROPERTIES, true); + } } diff --git a/libs/types/src/Shape/NamedFieldNode.php b/libs/types/src/Shape/NamedFieldNode.php index dfdb0db..d13896c 100644 --- a/libs/types/src/Shape/NamedFieldNode.php +++ b/libs/types/src/Shape/NamedFieldNode.php @@ -13,10 +13,6 @@ */ final class NamedFieldNode extends ExplicitFieldNode { - public string $index { - get => $this->key->toString(); - } - public function __construct( Identifier $key, TypeNode $type, @@ -30,4 +26,9 @@ public function __construct( attributes: $attributes, ); } + + public function index(): string + { + return $this->key->toString(); + } } diff --git a/libs/types/src/Shape/NumericFieldNode.php b/libs/types/src/Shape/NumericFieldNode.php index fd14ed0..410c94c 100644 --- a/libs/types/src/Shape/NumericFieldNode.php +++ b/libs/types/src/Shape/NumericFieldNode.php @@ -13,10 +13,6 @@ */ final class NumericFieldNode extends ExplicitFieldNode { - public string $index { - get => (string) $this->key->value; - } - public function __construct( IntLiteralNode $key, TypeNode $type, @@ -30,4 +26,9 @@ public function __construct( attributes: $attributes, ); } + + public function index(): string + { + return (string) $this->key->value; + } } diff --git a/libs/types/src/Shape/StringNamedFieldNode.php b/libs/types/src/Shape/StringNamedFieldNode.php index 83cee15..1f03dbc 100644 --- a/libs/types/src/Shape/StringNamedFieldNode.php +++ b/libs/types/src/Shape/StringNamedFieldNode.php @@ -13,10 +13,6 @@ */ final class StringNamedFieldNode extends ExplicitFieldNode { - public string $index { - get => $this->key->value; - } - public function __construct( StringLiteralNode $key, TypeNode $type, @@ -30,4 +26,9 @@ public function __construct( attributes: $attributes, ); } + + public function index(): string + { + return $this->key->value; + } } From 3aea6a59150e99a0f091f851cd147bca2d0e1402 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 16:29:50 +0300 Subject: [PATCH 002/119] Actualize phpstan configs --- libs/parser/phpstan.neon | 2 ++ libs/phpdoc/phpstan.neon | 1 + libs/printer/phpstan.neon | 1 + libs/reader/phpstan.neon | 1 + libs/types/phpstan.neon | 1 + phpstan.neon | 4 ++++ 6 files changed, 10 insertions(+) diff --git a/libs/parser/phpstan.neon b/libs/parser/phpstan.neon index 018e46e..ba4ecb1 100644 --- a/libs/parser/phpstan.neon +++ b/libs/parser/phpstan.neon @@ -1,6 +1,8 @@ parameters: level: max + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: - src + reportUnmatchedIgnoredErrors: false diff --git a/libs/phpdoc/phpstan.neon b/libs/phpdoc/phpstan.neon index 018e46e..cb1db62 100644 --- a/libs/phpdoc/phpstan.neon +++ b/libs/phpdoc/phpstan.neon @@ -1,5 +1,6 @@ parameters: level: max + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: diff --git a/libs/printer/phpstan.neon b/libs/printer/phpstan.neon index 018e46e..cb1db62 100644 --- a/libs/printer/phpstan.neon +++ b/libs/printer/phpstan.neon @@ -1,5 +1,6 @@ parameters: level: max + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: diff --git a/libs/reader/phpstan.neon b/libs/reader/phpstan.neon index 018e46e..cb1db62 100644 --- a/libs/reader/phpstan.neon +++ b/libs/reader/phpstan.neon @@ -1,5 +1,6 @@ parameters: level: max + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: diff --git a/libs/types/phpstan.neon b/libs/types/phpstan.neon index 2ccfbac..cfce637 100644 --- a/libs/types/phpstan.neon +++ b/libs/types/phpstan.neon @@ -1,5 +1,6 @@ parameters: level: max + treatPhpDocTypesAsCertain: false phpVersion: min: 80100 max: 80599 diff --git a/phpstan.neon b/phpstan.neon index 8a1867d..3a8b6e2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,6 +10,10 @@ includes: - libs/types/phpstan.neon parameters: level: max + treatPhpDocTypesAsCertain: false + phpVersion: + min: 80100 + max: 80599 strictRules: allRules: true fileExtensions: From 78a560c0b012a55eaf754d97e52f9215bc8b2919 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 16:41:05 +0300 Subject: [PATCH 003/119] Simplify AST nodes --- libs/types/src/Literal/BoolLiteralNode.php | 7 +- libs/types/src/Literal/FloatLiteralNode.php | 13 +- libs/types/src/Literal/IntLiteralNode.php | 54 +----- .../Literal/ParsableLiteralNodeInterface.php | 15 -- libs/types/src/Literal/StringLiteralNode.php | 167 +----------------- .../types/src/Literal/VariableLiteralNode.php | 24 +-- libs/types/src/Name.php | 17 +- libs/types/src/NodeList.php | 40 +++-- 8 files changed, 44 insertions(+), 293 deletions(-) delete mode 100644 libs/types/src/Literal/ParsableLiteralNodeInterface.php diff --git a/libs/types/src/Literal/BoolLiteralNode.php b/libs/types/src/Literal/BoolLiteralNode.php index 1cef959..c798d41 100644 --- a/libs/types/src/Literal/BoolLiteralNode.php +++ b/libs/types/src/Literal/BoolLiteralNode.php @@ -9,7 +9,7 @@ * * @phpstan-consistent-constructor */ -final class BoolLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class BoolLiteralNode extends LiteralNode { public function __construct( bool $value, @@ -17,9 +17,4 @@ public function __construct( ) { parent::__construct($value, $raw ?? ($value ? 'true' : 'false')); } - - public static function parse(string $value): self - { - return new self(\strtolower($value) === 'true', $value); - } } diff --git a/libs/types/src/Literal/FloatLiteralNode.php b/libs/types/src/Literal/FloatLiteralNode.php index e6dceb2..ed9fc9e 100644 --- a/libs/types/src/Literal/FloatLiteralNode.php +++ b/libs/types/src/Literal/FloatLiteralNode.php @@ -9,21 +9,12 @@ * * @phpstan-consistent-constructor */ -final class FloatLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class FloatLiteralNode extends LiteralNode { public function __construct( float $value, ?string $raw = null, ) { - parent::__construct($value, $raw ?? (string) $this->value); - } - - public static function parse(string $value): self - { - if (!\is_numeric($value)) { - return new self(0.0, $value); - } - - return new self((float) $value, $value); + parent::__construct($value, $raw ?? (string) $value); } } diff --git a/libs/types/src/Literal/IntLiteralNode.php b/libs/types/src/Literal/IntLiteralNode.php index d6b3003..2b18b06 100644 --- a/libs/types/src/Literal/IntLiteralNode.php +++ b/libs/types/src/Literal/IntLiteralNode.php @@ -9,7 +9,7 @@ * * @phpstan-consistent-constructor */ -final class IntLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class IntLiteralNode extends LiteralNode { /** * @var numeric-string @@ -24,56 +24,8 @@ public function __construct( ?string $raw = null, ?string $decimal = null, ) { - $this->decimal = $decimal ?? (string) $this->value; + $this->decimal = $decimal ?? (string) $value; - parent::__construct($value, $raw ?? (string) $this->value); - } - - public static function parse(string $value): self - { - [$negative, $decimal] = self::split($value); - - $inverse = '-' . $decimal; - - if ($negative) { - if ((string) \PHP_INT_MIN === $inverse) { - return new self(\PHP_INT_MIN, $value, $inverse); - } - - /** @phpstan-ignore-next-line : An "$inverse" variable contain numeric-string */ - return new self((int) $inverse, $value, $inverse); - } - - return new self((int) $decimal, $value, $decimal); - } - - /** - * @return array{bool, numeric-string} - */ - private static function split(string $literal): array - { - $literal = \str_replace('_', '', $literal); - - if ($negative = ($literal[0] === '-')) { - $literal = \substr($literal, 1); - } - - // One of: [ 0123, 0o23, 0x00, 0b01 ] - if ($literal[0] === '0' && isset($literal[1])) { - /** @var array{bool, numeric-string} */ - return [$negative, match ($literal[1]) { - // hexadecimal - 'x', 'X' => \base_convert(\substr($literal, 2), 16, 10), - // binary - 'b', 'B' => \base_convert(\substr($literal, 2), 2, 10), - // octal - 'o', 'O' => \base_convert(\substr($literal, 2), 8, 10), - // octal (legacy) - default => \base_convert($literal, 8, 10), - }]; - } - - /** @var array{bool, numeric-string} */ - return [$negative, $literal]; + parent::__construct($value, $raw ?? (string) $value); } } diff --git a/libs/types/src/Literal/ParsableLiteralNodeInterface.php b/libs/types/src/Literal/ParsableLiteralNodeInterface.php deleted file mode 100644 index aac49ec..0000000 --- a/libs/types/src/Literal/ParsableLiteralNodeInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - - */ - private const ESCAPED_CHARS = [ - '\n' => "\n", - '\r' => "\r", - '\t' => "\t", - '\v' => "\v", - '\e' => "\e", - '\f' => "\f", - '\$' => '$', - ]; - final public function __construct( string $value, ?string $raw = null, @@ -42,146 +19,4 @@ final public function __construct( parent::__construct($value, $raw); } - - public static function parse(string $value): self - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - if ($value[0] === '"') { - return self::createFromDoubleQuotedString($value); - } - - return self::createFromSingleQuotedString($value); - } - - /** - * @param non-empty-string $value - */ - public static function createFromDoubleQuotedString(string $value): self - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - $body = \substr($value, 1, -1); - - return self::parseEncodedValue( - string: \str_replace('\"', '"', $body), - raw: $value, - ); - } - - /** - * @param non-empty-string $value - */ - public static function createFromSingleQuotedString(string $value): self - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - $body = \substr($value, 1, -1); - - return new self( - value: \str_replace("\'", "'", $body), - raw: $value, - ); - } - - private static function parseEncodedValue(string $string, ?string $raw = null): self - { - $raw ??= $string; - - if (\str_contains($string, '\\')) { - // Replace double backslash to "\0" - $string = \str_replace('\\\\', "\0", $string); - - // Replace escaped chars (like a "\n") to real bytes - $string = self::renderEscapeSequences($string); - - // Replace hex sequences (like a "\xFF") to real bytes - $string = self::renderHexadecimalSequences($string); - - // Replace unicode sequences (like a "\u{FFFF}") to real bytes - $string = self::renderUtfSequences($string); - - // Rollback double backslash escaping - $string = \str_replace("\0", '\\\\', $string); - } - - return new self($string, $raw); - } - - /** - * Method for parsing and decode special escaped character sequences - * like a "\n", "\r" etc... - * - * @link https://www.php.net/manual/en/language.types.string.php - */ - private static function renderEscapeSequences(string $body): string - { - return \str_replace( - \array_keys(self::ESCAPED_CHARS), - \array_values(self::ESCAPED_CHARS), - $body, - ); - } - - /** - * Method for parsing and decode hexadecimal character sequences - * like a "\xFF" type. - * - * @link https://www.php.net/manual/en/language.types.string.php - */ - private static function renderHexadecimalSequences(string $body): string - { - $callback = static fn(array $matches): string - // @phpstan-ignore-next-line : A hexdec returns int<0, 255> - => \chr((int) \hexdec($matches[1])); - - return @\preg_replace_callback(self::HEX_SEQUENCE_PATTERN, $callback, $body) - ?? $body; - } - - /** - * Method for parsing and decode utf-8 character sequences - * like a "\u{FFFF}" type. - * - * @link https://www.php.net/manual/en/language.types.string.php - */ - private static function renderUtfSequences(string $body): string - { - return @\preg_replace_callback(self::UTF_SEQUENCE_PATTERN, static function (array $matches): string { - /** @var int<0, 1114112> $code */ - $code = (int) \hexdec($matches[1]); - - // @phpstan-ignore-next-line : PHPStan false-positive mb_chr evaluation - if (\function_exists('\\mb_chr') && ($result = \mb_chr($code)) !== false) { - return $result; - } - - if (0x80 > $code %= 0x200000) { - // @phpstan-ignore-next-line : Code is valid - return \chr($code); - } - - if (0x800 > $code) { - return \chr(0xC0 | $code >> 6) - . \chr(0x80 | $code & 0x3F); - } - - if (0x10000 > $code) { - return \chr(0xE0 | $code >> 12) - . \chr(0x80 | $code >> 6 & 0x3F) - . \chr(0x80 | $code & 0x3F); - } - - return \chr(0xF0 | $code >> 18) - . \chr(0x80 | $code >> 12 & 0x3F) - . \chr(0x80 | $code >> 6 & 0x3F) - . \chr(0x80 | $code & 0x3F); - }, $body) ?? $body; - } } diff --git a/libs/types/src/Literal/VariableLiteralNode.php b/libs/types/src/Literal/VariableLiteralNode.php index a80ed97..222ea91 100644 --- a/libs/types/src/Literal/VariableLiteralNode.php +++ b/libs/types/src/Literal/VariableLiteralNode.php @@ -9,33 +9,13 @@ * * @phpstan-consistent-constructor */ -final class VariableLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class VariableLiteralNode extends LiteralNode { /** * @param non-empty-string $value */ public function __construct(string $value) { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Variable name length must be greater than 1'); - } - - if (!\str_starts_with($value, '$')) { - throw new \InvalidArgumentException('Variable name must start with "$" character'); - } - - /** @var non-empty-string $normalized */ - $normalized = \substr($value, 1); - - parent::__construct($normalized, $value); - } - - public static function parse(string $value): self - { - if (!\str_starts_with($value, '$')) { - $value = '$' . $value; - } - - return new self($value); + parent::__construct($value, '$' . $value); } } diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index e2cddd3..c26e340 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -288,7 +288,7 @@ public function toUnqualifiedLowerString(): string */ public function toFullQualifiedLowerString(): string { - return \strtolower($this->toUnqualifiedString()); + return \strtolower($this->toFullQualifiedString()); } /** @@ -375,25 +375,24 @@ public function __toString(): string } /** - * @return array{int<0, max>, non-empty-list} + * @return array{non-empty-list, int<0, max>, bool} */ public function __serialize(): array { - return [$this->offset, $this->segments]; + return [$this->segments, $this->offset, $this->isFullyQualified]; } /** - * @param array{0?: int<0, max>, 1?: non-empty-list} $data + * @param array{0?: non-empty-list, 1?: int<0, max>, 2?: bool} $data * @throws \UnexpectedValueException */ public function __unserialize(array $data): void { - $this->offset = $data[0] ?? throw new \UnexpectedValueException( - message: 'Unable to unserialize Name offset', + $this->segments = $data[0] ?? throw new \UnexpectedValueException( + message: 'Unable to unserialize Name segments', ); - $this->segments = $data[1] ?? throw new \UnexpectedValueException( - message: 'Unable to unserialize Name identifier parts', - ); + $this->offset = $data[1] ?? 0; + $this->isFullyQualified = $data[2] ?? self::IS_FULLY_QUALIFIED_DEFAULT_VALUE; } } diff --git a/libs/types/src/NodeList.php b/libs/types/src/NodeList.php index 6b13f69..049fcbb 100644 --- a/libs/types/src/NodeList.php +++ b/libs/types/src/NodeList.php @@ -64,26 +64,37 @@ public function findIndex(Node $node): ?int public function offsetExists(mixed $offset): bool { - // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException('A NodeList index must be an int<0, max>'); + } return isset($this->items[$offset]); } public function offsetGet(mixed $offset): ?Node { - // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException('A NodeList index must be an int<0, max>'); + } return $this->items[$offset] ?? null; } public function offsetSet(mixed $offset, mixed $value): void { - // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); - // @phpstan-ignore-next-line - \assert($value instanceof Node); + if (!$value instanceof Node) { + throw new \InvalidArgumentException('A NodeList value must be instance of Node'); + } + + if ($offset === null) { + $this->items[] = $value; + + return; + } + + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException('A NodeList index must be an int<0, max>|null'); + } // @phpstan-ignore-next-line $this->items[$offset] = $value; @@ -95,12 +106,15 @@ public function offsetSet(mixed $offset, mixed $value): void public function offsetUnset(mixed $offset): void { - // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException('A NodeList index must be an int<0, max>|null'); + } - $items = $this->items; - unset($items[$offset]); - $this->items = \array_values($items); + unset($this->items[$offset]); + + if (!\array_is_list($this->items)) { + $this->items = \array_values($this->items); + } } public function getIterator(): \Traversable From 32817e4b421d5a6461a44b49d091ec79e161c706 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 16:41:14 +0300 Subject: [PATCH 004/119] Add more AST tests --- .../Callable/CallableParameterNodeTest.php | 4 +- libs/types/tests/IdentifierTest.php | 206 +++++++++++ .../tests/Literal/BoolLiteralNodeTest.php | 41 --- .../tests/Literal/FloatLiteralNodeTest.php | 45 +-- .../tests/Literal/IntLiteralNodeTest.php | 79 +--- libs/types/tests/Literal/LiteralNodeTest.php | 134 +++++++ .../tests/Literal/StringLiteralNodeTest.php | 101 +----- .../tests/Literal/VariableLiteralNodeTest.php | 42 +-- libs/types/tests/LogicalTypeNodeTest.php | 194 ++++++++++ libs/types/tests/NameTest.php | 337 ++++++++++++++++++ libs/types/tests/NodeListTest.php | 287 +++++++++++++++ libs/types/tests/NodeTest.php | 248 +++++++++++++ .../tests/Shape/ExplicitFieldNodeTest.php | 224 ++++++++++++ .../tests/Shape/NumericFieldNodeTest.php | 12 +- 14 files changed, 1660 insertions(+), 294 deletions(-) create mode 100644 libs/types/tests/Literal/LiteralNodeTest.php create mode 100644 libs/types/tests/LogicalTypeNodeTest.php create mode 100644 libs/types/tests/NodeListTest.php create mode 100644 libs/types/tests/NodeTest.php create mode 100644 libs/types/tests/Shape/ExplicitFieldNodeTest.php diff --git a/libs/types/tests/Callable/CallableParameterNodeTest.php b/libs/types/tests/Callable/CallableParameterNodeTest.php index 6b97baf..cab9272 100644 --- a/libs/types/tests/Callable/CallableParameterNodeTest.php +++ b/libs/types/tests/Callable/CallableParameterNodeTest.php @@ -30,7 +30,7 @@ public function constructorWithTypeOnly(): void #[Test] public function constructorWithNameOnly(): void { - $name = VariableLiteralNode::parse('param'); + $name = new VariableLiteralNode('param'); $node = new CallableParameterNode(name: $name); self::assertNull($node->type); @@ -41,7 +41,7 @@ public function constructorWithNameOnly(): void public function constructorWithTypeAndName(): void { $type = new NamedTypeNode(Name::createFromString('int')); - $name = VariableLiteralNode::parse('count'); + $name = new VariableLiteralNode('count'); $node = new CallableParameterNode($type, $name); self::assertSame($type, $node->type); diff --git a/libs/types/tests/IdentifierTest.php b/libs/types/tests/IdentifierTest.php index d093dd4..766c7ea 100644 --- a/libs/types/tests/IdentifierTest.php +++ b/libs/types/tests/IdentifierTest.php @@ -192,4 +192,210 @@ public function serializeAndUnserializeRoundtrip(): void self::assertSame('MyClass', $restored->value); self::assertSame(42, $restored->offset); } + + #[Test] + public function isVirtualMethodIsAnAliasOfProperty(): void + { + $id = new Identifier('non-empty-string'); + + self::assertTrue($id->isVirtual()); + self::assertSame($id->isVirtual(), $id->isVirtual); + } + + #[Test] + public function isSpecialMethodIsAnAliasOfProperty(): void + { + $id = new Identifier('self'); + + self::assertTrue($id->isSpecial()); + self::assertSame($id->isSpecial(), $id->isSpecial); + } + + #[Test] + public function isBuiltinMethodIsAnAliasOfProperty(): void + { + $id = new Identifier('int'); + + self::assertTrue($id->isBuiltin()); + self::assertSame($id->isBuiltin(), $id->isBuiltin); + } + + #[Test] + public function issetReturnsTrueForVirtualProperties(): void + { + $id = new Identifier('Example'); + + self::assertTrue(isset($id->isVirtual)); + self::assertTrue(isset($id->isSpecial)); + self::assertTrue(isset($id->isBuiltin)); + } + + #[Test] + public function issetReturnsFalseForUnknownProperty(): void + { + $id = new Identifier('Example'); + + self::assertFalse(isset($id->unknown)); + } + + #[Test] + public function readingUnknownPropertyThrows(): void + { + $id = new Identifier('Example'); + + $this->expectException(\OutOfRangeException::class); + + /** @phpstan-ignore-next-line */ + $id->unknown; + } + + #[Test] + public function specialNamesAreCaseInsensitive(): void + { + self::assertTrue((new Identifier('SELF'))->isSpecial()); + self::assertTrue((new Identifier('Parent'))->isSpecial()); + self::assertTrue(Identifier::isLooksLikeSpecial('StAtIc')); + } + + #[Test] + public function builtinNamesAreCaseInsensitive(): void + { + self::assertTrue((new Identifier('INT'))->isBuiltin()); + self::assertTrue((new Identifier('Iterable'))->isBuiltin()); + self::assertTrue(Identifier::isLooksLikeBuiltin('NuLl')); + } + + #[Test] + public function specialNameIsNotBuiltin(): void + { + $id = new Identifier('self'); + + self::assertTrue($id->isSpecial()); + self::assertFalse($id->isBuiltin()); + } + + #[Test] + public function builtinNameIsNotSpecial(): void + { + $id = new Identifier('string'); + + self::assertTrue($id->isBuiltin()); + self::assertFalse($id->isSpecial()); + } + + #[Test] + public function virtualNameIsDetectedInAnyPosition(): void + { + self::assertTrue((new Identifier('-leading'))->isVirtual()); + self::assertTrue((new Identifier('trailing-'))->isVirtual()); + self::assertTrue((new Identifier('a-b-c'))->isVirtual()); + } + + #[Test] + public function underscoredNameIsNotVirtual(): void + { + self::assertFalse((new Identifier('non_empty_string'))->isVirtual()); + } + + #[Test] + public function createFromStringAcceptsStringableObject(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return 'Example'; + } + }; + + self::assertSame('Example', Identifier::createFromString($stringable)->value); + } + + #[Test] + public function createFromStringTrimsAllSurroundingWhitespaceKinds(): void + { + self::assertSame('Example', Identifier::createFromString("\n\t Example \r\n")->value); + } + + #[Test] + public function createFromStringThrowsOnStringableReturningEmptyString(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return ' '; + } + }; + + $this->expectException(\InvalidArgumentException::class); + + Identifier::createFromString($stringable); + } + + #[Test] + public function offsetMethodIsAnAliasOfProperty(): void + { + $id = new Identifier('Example'); + $id->offset = 17; + + self::assertSame(17, $id->offset()); + } + + #[Test] + public function serializePayloadContainsOffsetAndValue(): void + { + $id = new Identifier('Example'); + $id->offset = 5; + + self::assertSame([5, 'Example'], $id->__serialize()); + } + + #[Test] + public function serializationRoundtripPreservesOffset(): void + { + $id = new Identifier('Example'); + $id->offset = 5; + + /** @var Identifier $restored */ + $restored = \unserialize(\serialize($id)); + + self::assertSame(5, $restored->offset()); + self::assertSame('Example', $restored->value); + } + + #[Test] + public function unserializeThrowsWhenOffsetIsMissing(): void + { + $id = new Identifier('Example'); + + $this->expectException(\UnexpectedValueException::class); + + $id->__unserialize([]); + } + + #[Test] + public function unserializeThrowsWhenValueIsMissing(): void + { + $id = new Identifier('Example'); + + $this->expectException(\UnexpectedValueException::class); + + $id->__unserialize([0]); + } + + #[Test] + public function stringCastIsTheSameAsToStringMethod(): void + { + $id = new Identifier('Example'); + + self::assertSame($id->toString(), (string) $id); + } + + #[Test] + public function toLowerStringDoesNotModifyTheValue(): void + { + $id = new Identifier('SomeClass'); + + self::assertSame('someclass', $id->toLowerString()); + self::assertSame('SomeClass', $id->value); + } } diff --git a/libs/types/tests/Literal/BoolLiteralNodeTest.php b/libs/types/tests/Literal/BoolLiteralNodeTest.php index 86c0331..ea5c55d 100644 --- a/libs/types/tests/Literal/BoolLiteralNodeTest.php +++ b/libs/types/tests/Literal/BoolLiteralNodeTest.php @@ -45,47 +45,6 @@ public function toStringReturnsRaw(): void self::assertSame('true', (string) $node); } - #[Test] - public function parseLowercaseTrue(): void - { - $node = BoolLiteralNode::parse('true'); - - self::assertTrue($node->value); - self::assertSame('true', $node->raw); - } - - #[Test] - public function parseUppercaseTrue(): void - { - $node = BoolLiteralNode::parse('TRUE'); - - self::assertTrue($node->value); - } - - #[Test] - public function parseLowercaseFalse(): void - { - $node = BoolLiteralNode::parse('false'); - - self::assertFalse($node->value); - } - - #[Test] - public function parseUppercaseFalse(): void - { - $node = BoolLiteralNode::parse('FALSE'); - - self::assertFalse($node->value); - } - - #[Test] - public function parseNonTrueStringReturnsFalse(): void - { - $node = BoolLiteralNode::parse('yes'); - - self::assertFalse($node->value); - } - #[Test] public function defaultOffsetIsZero(): void { diff --git a/libs/types/tests/Literal/FloatLiteralNodeTest.php b/libs/types/tests/Literal/FloatLiteralNodeTest.php index 4093144..042faa1 100644 --- a/libs/types/tests/Literal/FloatLiteralNodeTest.php +++ b/libs/types/tests/Literal/FloatLiteralNodeTest.php @@ -19,15 +19,6 @@ public function constructorStoresValueAndRaw(): void self::assertSame('3.14', $node->raw); } - #[Test] - public function parseDerivesRawFromValue(): void - { - $node = FloatLiteralNode::parse('1.5'); - - self::assertSame(1.5, $node->value); - self::assertSame('1.5', $node->raw); - } - #[Test] public function toStringReturnsRaw(): void { @@ -36,44 +27,10 @@ public function toStringReturnsRaw(): void self::assertSame('3.14', (string) $node); } - #[Test] - public function parseValidFloat(): void - { - $node = FloatLiteralNode::parse('3.14'); - - self::assertSame(3.14, $node->value); - self::assertSame('3.14', $node->raw); - } - - #[Test] - public function parseNegativeFloat(): void - { - $node = FloatLiteralNode::parse('-1.5'); - - self::assertSame(-1.5, $node->value); - } - - #[Test] - public function parseScientificNotation(): void - { - $node = FloatLiteralNode::parse('1.5e2'); - - self::assertSame(150.0, $node->value); - } - - #[Test] - public function parseNonNumericStringReturnsZero(): void - { - $node = FloatLiteralNode::parse('not-a-number'); - - self::assertSame(0.0, $node->value); - self::assertSame('not-a-number', $node->raw); - } - #[Test] public function defaultOffsetIsZero(): void { - $node = FloatLiteralNode::parse('0.0'); + $node = new FloatLiteralNode(0.0); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Literal/IntLiteralNodeTest.php b/libs/types/tests/Literal/IntLiteralNodeTest.php index 6432a3a..55571be 100644 --- a/libs/types/tests/Literal/IntLiteralNodeTest.php +++ b/libs/types/tests/Literal/IntLiteralNodeTest.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Tests\Literal; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\Literal\IntLiteralNode; use TypeLang\Type\Tests\TestCase; @@ -39,86 +38,10 @@ public function toStringReturnsRaw(): void self::assertSame('0x2A', (string) $node); } - #[Test] - #[DataProvider('provideDecimalIntegers')] - public function parseDecimalInteger(string $input, int $expected): void - { - $node = IntLiteralNode::parse($input); - - self::assertSame($expected, $node->value); - self::assertSame($input, $node->raw); - } - - public static function provideDecimalIntegers(): iterable - { - return [ - ['0', 0], - ['1', 1], - ['42', 42], - ['100', 100], - ]; - } - - #[Test] - public function parseNegativeInteger(): void - { - $node = IntLiteralNode::parse('-42'); - - self::assertSame(-42, $node->value); - } - - #[Test] - public function parseHexadecimalInteger(): void - { - $node = IntLiteralNode::parse('0xFF'); - - self::assertSame(255, $node->value); - } - - #[Test] - public function parseBinaryInteger(): void - { - $node = IntLiteralNode::parse('0b1010'); - - self::assertSame(10, $node->value); - } - - #[Test] - public function parseOctalInteger(): void - { - $node = IntLiteralNode::parse('0o17'); - - self::assertSame(15, $node->value); - } - - #[Test] - public function parseLegacyOctalInteger(): void - { - $node = IntLiteralNode::parse('017'); - - self::assertSame(15, $node->value); - } - - #[Test] - public function parseIntegerWithUnderscores(): void - { - $node = IntLiteralNode::parse('1_000_000'); - - self::assertSame(1000000, $node->value); - } - - #[Test] - public function parsePhpIntMin(): void - { - $node = IntLiteralNode::parse((string) \PHP_INT_MIN); - - self::assertSame(\PHP_INT_MIN, $node->value); - } - #[Test] public function defaultOffsetIsZero(): void { - $node = IntLiteralNode::parse('0'); + $node = new IntLiteralNode(0); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Literal/LiteralNodeTest.php b/libs/types/tests/Literal/LiteralNodeTest.php new file mode 100644 index 0000000..cbf0184 --- /dev/null +++ b/libs/types/tests/Literal/LiteralNodeTest.php @@ -0,0 +1,134 @@ + + */ + public static function provideLiterals(): iterable + { + yield 'bool(true)' => [new BoolLiteralNode(true, 'true'), true, 'true']; + yield 'bool(false)' => [new BoolLiteralNode(false, 'false'), false, 'false']; + yield 'float' => [new FloatLiteralNode(0.5, '0.5'), 0.5, '0.5']; + yield 'int' => [new IntLiteralNode(42, '42', '42'), 42, '42']; + yield 'null' => [new NullLiteralNode(), null, 'null']; + yield 'string' => [new StringLiteralNode('example', '"example"'), 'example', '"example"']; + yield 'variable' => [new VariableLiteralNode('example'), 'example', '$example']; + } + + #[Test] + #[DataProvider('provideLiterals')] + public function valueMethodIsAnAliasOfValueProperty(LiteralNode $node, mixed $value, string $raw): void + { + self::assertSame($value, $node->value); + self::assertSame($value, $node->value()); + } + + #[Test] + #[DataProvider('provideLiterals')] + public function rawMethodIsAnAliasOfRawProperty(LiteralNode $node, mixed $value, string $raw): void + { + self::assertSame($raw, $node->raw); + self::assertSame($raw, $node->raw()); + } + + #[Test] + #[DataProvider('provideLiterals')] + public function stringRepresentationIsTheRawValue(LiteralNode $node, mixed $value, string $raw): void + { + self::assertSame($raw, (string) $node); + } + + #[Test] + #[DataProvider('provideLiterals')] + public function everyLiteralImplementsItsContracts(LiteralNode $node, mixed $value, string $raw): void + { + self::assertInstanceOf(LiteralNodeInterface::class, $node); + self::assertInstanceOf(TypeNode::class, $node); + self::assertInstanceOf(\Stringable::class, $node); + } + + #[Test] + public function boolLiteralDerivesRawFromValue(): void + { + self::assertSame('true', (new BoolLiteralNode(true))->raw()); + self::assertSame('false', (new BoolLiteralNode(false))->raw()); + } + + #[Test] + public function nullLiteralDerivesRawFromValue(): void + { + self::assertSame('null', (new NullLiteralNode())->raw()); + self::assertNull((new NullLiteralNode())->value()); + } + + #[Test] + public function stringLiteralDerivesRawFromValue(): void + { + self::assertSame('"example"', (new StringLiteralNode('example'))->raw()); + } + + #[Test] + public function intLiteralDerivesRawFromValue(): void + { + $node = new IntLiteralNode(42); + + self::assertSame(42, $node->value()); + self::assertSame('42', $node->raw()); + self::assertSame('42', $node->decimal); + } + + #[Test] + public function negativeIntLiteralDerivesRawFromValue(): void + { + $node = new IntLiteralNode(-42); + + self::assertSame('-42', $node->raw()); + self::assertSame('-42', $node->decimal); + } + + #[Test] + public function floatLiteralDerivesRawFromValue(): void + { + $node = new FloatLiteralNode(0.5); + + self::assertSame(0.5, $node->value()); + self::assertSame('0.5', $node->raw()); + } + + #[Test] + public function variableLiteralValueDoesNotContainDollarSign(): void + { + $node = new VariableLiteralNode('example'); + + self::assertSame('example', $node->value()); + self::assertSame('$example', $node->raw()); + } + + #[Test] + public function literalValueAndRawArePartOfTheNodeState(): void + { + $node = new StringLiteralNode('example'); + $node->offset = 5; + + self::assertSame(5, $node->offset()); + self::assertSame('example', $node->value()); + } +} diff --git a/libs/types/tests/Literal/StringLiteralNodeTest.php b/libs/types/tests/Literal/StringLiteralNodeTest.php index 4cbac41..a9dc121 100644 --- a/libs/types/tests/Literal/StringLiteralNodeTest.php +++ b/libs/types/tests/Literal/StringLiteralNodeTest.php @@ -37,106 +37,19 @@ public function toStringReturnsRaw(): void } #[Test] - public function parseDoubleQuotedString(): void - { - $node = StringLiteralNode::parse('"hello"'); - - self::assertSame('hello', $node->value); - self::assertSame('"hello"', $node->raw); - } - - #[Test] - public function parseSingleQuotedString(): void - { - $node = StringLiteralNode::parse("'hello'"); - - self::assertSame('hello', $node->value); - self::assertSame("'hello'", $node->raw); - } - - #[Test] - public function parseDoubleQuotedWithEscapedQuote(): void - { - $node = StringLiteralNode::parse('"say \\"hello\\""'); - - self::assertSame('say "hello"', $node->value); - } - - #[Test] - public function parseSingleQuotedWithEscapedQuote(): void - { - $node = StringLiteralNode::parse("'it\\'s'"); - - self::assertSame("it's", $node->value); - } - - #[Test] - public function parseNewlineEscapeSequence(): void - { - $node = StringLiteralNode::parse('"line1\\nline2"'); - - self::assertSame("line1\nline2", $node->value); - } - - #[Test] - public function parseTabEscapeSequence(): void - { - $node = StringLiteralNode::parse('"col1\\tcol2"'); - - self::assertSame("col1\tcol2", $node->value); - } - - #[Test] - public function parseHexSequence(): void - { - $node = StringLiteralNode::parse('"\\x41"'); - - self::assertSame('A', $node->value); - } - - #[Test] - public function parseUnicodeSequence(): void - { - $node = StringLiteralNode::parse('"\\u{0041}"'); - - self::assertSame('A', $node->value); - } - - #[Test] - public function createFromDoubleQuotedString(): void - { - $node = StringLiteralNode::createFromDoubleQuotedString('"world"'); - - self::assertSame('world', $node->value); - } - - #[Test] - public function createFromSingleQuotedString(): void - { - $node = StringLiteralNode::createFromSingleQuotedString("'world'"); - - self::assertSame('world', $node->value); - } - - #[Test] - public function parseThrowsOnStringTooShort(): void + public function defaultOffsetIsZero(): void { - $this->expectException(\InvalidArgumentException::class); - StringLiteralNode::parse('"'); - } + $node = new StringLiteralNode('test'); - #[Test] - public function parseThrowsOnEmptyString(): void - { - $this->expectException(\InvalidArgumentException::class); - StringLiteralNode::parse(''); + self::assertSame(0, $node->offset); } #[Test] - public function defaultOffsetIsZero(): void + public function constructorEscapesTheDerivedRawValue(): void { - $node = new StringLiteralNode('test'); + $node = new StringLiteralNode('a"b'); - self::assertSame(0, $node->offset); + self::assertSame('"a\"b"', $node->raw); + self::assertSame('a"b', $node->value); } } diff --git a/libs/types/tests/Literal/VariableLiteralNodeTest.php b/libs/types/tests/Literal/VariableLiteralNodeTest.php index 9c06f80..706ba27 100644 --- a/libs/types/tests/Literal/VariableLiteralNodeTest.php +++ b/libs/types/tests/Literal/VariableLiteralNodeTest.php @@ -11,59 +11,43 @@ final class VariableLiteralNodeTest extends TestCase { #[Test] - public function constructorStripsLeadingDollarSign(): void + public function constructorStoresNameWithoutDollarSign(): void { - $node = new VariableLiteralNode('$foo'); + $node = new VariableLiteralNode('foo'); self::assertSame('foo', $node->value); - self::assertSame('$foo', $node->raw); } #[Test] - public function toStringReturnsRaw(): void + public function rawValueIsPrefixedByDollarSign(): void { - $node = new VariableLiteralNode('$bar'); + $node = new VariableLiteralNode('foo'); - self::assertSame('$bar', (string) $node); + self::assertSame('$foo', $node->raw); } #[Test] - public function parseWithoutDollarSignAddsDollar(): void + public function toStringReturnsRaw(): void { - $node = VariableLiteralNode::parse('myVar'); + $node = new VariableLiteralNode('bar'); - self::assertSame('myVar', $node->value); - self::assertSame('$myVar', $node->raw); + self::assertSame('$bar', (string) $node); } #[Test] - public function parseWithDollarSignKeepsValue(): void + public function singleCharacterVariableIsAllowed(): void { - $node = VariableLiteralNode::parse('$myVar'); + $node = new VariableLiteralNode('a'); - self::assertSame('myVar', $node->value); - self::assertSame('$myVar', $node->raw); + self::assertSame('a', $node->value); + self::assertSame('$a', $node->raw); } #[Test] public function defaultOffsetIsZero(): void { - $node = new VariableLiteralNode('$x'); + $node = new VariableLiteralNode('x'); self::assertSame(0, $node->offset); } - - #[Test] - public function constructorThrowsWhenStringTooShort(): void - { - $this->expectException(\InvalidArgumentException::class); - new VariableLiteralNode('$'); - } - - #[Test] - public function constructorThrowsWhenMissingDollarSign(): void - { - $this->expectException(\InvalidArgumentException::class); - new VariableLiteralNode('foo'); - } } diff --git a/libs/types/tests/LogicalTypeNodeTest.php b/libs/types/tests/LogicalTypeNodeTest.php new file mode 100644 index 0000000..6f1c365 --- /dev/null +++ b/libs/types/tests/LogicalTypeNodeTest.php @@ -0,0 +1,194 @@ +}> + */ + public static function provideLogicalTypes(): iterable + { + yield UnionTypeNode::class => [UnionTypeNode::class]; + yield IntersectionTypeNode::class => [IntersectionTypeNode::class]; + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function constructorRequiresAtLeastTwoStatements(string $class): void + { + $node = new $class($this->type('A'), $this->type('B')); + + self::assertCount(2, $node); + self::assertCount(2, $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function variadicStatementsAreStored(string $class): void + { + $node = new $class($this->type('A'), $this->type('B'), $this->type('C'), $this->type('D')); + + self::assertCount(4, $node); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsOrderIsPreserved(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + + $node = new $class($a, $b, $c); + + self::assertSame([$a, $b, $c], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function nestedStatementsOfTheSameTypeAreFlattened(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + + $node = new $class(new $class($a, $b), $c); + + self::assertSame([$a, $b, $c], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function deeplyNestedStatementsAreFlattened(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + $d = $this->type('D'); + + $node = new $class(new $class(new $class($a, $b), $c), $d); + + self::assertSame([$a, $b, $c, $d], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function nestedStatementsAreFlattenedInAnyPosition(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + + $node = new $class($a, new $class($b, $c)); + + self::assertSame([$a, $b, $c], $node->statements); + } + + #[Test] + public function unionDoesNotFlattenIntersection(): void + { + $intersection = new IntersectionTypeNode($this->type('A'), $this->type('B')); + + $node = new UnionTypeNode($intersection, $this->type('C')); + + self::assertCount(2, $node); + self::assertSame($intersection, $node->statements[0]); + } + + #[Test] + public function intersectionDoesNotFlattenUnion(): void + { + $union = new UnionTypeNode($this->type('A'), $this->type('B')); + + $node = new IntersectionTypeNode($union, $this->type('C')); + + self::assertCount(2, $node); + self::assertSame($union, $node->statements[0]); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function iteratorYieldsStatements(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class($a, $b); + + self::assertSame([$a, $b], \iterator_to_array($node->getIterator())); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function serializationRoundtripPreservesStatementsAndOffset(string $class): void + { + $node = new $class($this->type('A'), $this->type('B')); + $node->offset = 13; + + /** @var LogicalTypeNode $restored */ + $restored = \unserialize(\serialize($node)); + + self::assertInstanceOf($class, $restored); + self::assertSame(13, $restored->offset()); + self::assertCount(2, $restored); + self::assertSame('A', $restored->statements[0]->name->toString()); + self::assertSame('B', $restored->statements[1]->name->toString()); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function serializePayloadContainsOffsetAndStatements(string $class): void + { + $node = new $class($this->type('A'), $this->type('B')); + $node->offset = 7; + + self::assertSame([7, $node->statements], $node->__serialize()); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function unserializeThrowsWhenOffsetIsMissing(string $class): void + { + $node = new $class($this->type('A'), $this->type('B')); + + $this->expectException(\UnexpectedValueException::class); + + $node->__unserialize([]); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function unserializeThrowsWhenStatementsAreMissing(string $class): void + { + $node = new $class($this->type('A'), $this->type('B')); + + $this->expectException(\UnexpectedValueException::class); + + $node->__unserialize([0]); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function defaultOffsetIsZero(string $class): void + { + $node = new $class($this->type('A'), $this->type('B')); + + self::assertSame(0, $node->offset()); + } +} diff --git a/libs/types/tests/NameTest.php b/libs/types/tests/NameTest.php index e03974e..575d8b3 100644 --- a/libs/types/tests/NameTest.php +++ b/libs/types/tests/NameTest.php @@ -299,4 +299,341 @@ public function constructorThrowsOnEmptySegmentsArray(): void new Name([]); } + + #[Test] + public function firstMethodIsAnAliasOfProperty(): void + { + $name = new Name([$this->id('Foo'), $this->id('Bar')]); + + self::assertSame('Foo', $name->first()->value); + self::assertSame($name->first(), $name->first); + } + + #[Test] + public function lastMethodIsAnAliasOfProperty(): void + { + $name = new Name([$this->id('Foo'), $this->id('Bar')]); + + self::assertSame('Bar', $name->last()->value); + self::assertSame($name->last(), $name->last); + } + + #[Test] + public function isSimpleMethodIsAnAliasOfProperty(): void + { + $name = new Name([$this->id('Foo')]); + + self::assertTrue($name->isSimple()); + self::assertSame($name->isSimple(), $name->isSimple); + } + + #[Test] + public function isSpecialMethodIsAnAliasOfProperty(): void + { + $name = new Name([$this->id('self')]); + + self::assertTrue($name->isSpecial()); + self::assertSame($name->isSpecial(), $name->isSpecial); + } + + #[Test] + public function isBuiltinMethodIsAnAliasOfProperty(): void + { + $name = new Name([$this->id('int')]); + + self::assertTrue($name->isBuiltin()); + self::assertSame($name->isBuiltin(), $name->isBuiltin); + } + + #[Test] + public function virtualPropertiesReflectSegmentMutations(): void + { + $name = new Name([$this->id('Foo')]); + + self::assertTrue($name->isSimple); + + $name->segments[] = $this->id('Bar'); + + self::assertFalse($name->isSimple, 'Virtual properties must be recalculated on each access'); + self::assertSame('Bar', $name->last->value); + } + + #[Test] + public function issetReturnsTrueForVirtualProperties(): void + { + $name = new Name([$this->id('Foo')]); + + self::assertTrue(isset($name->first)); + self::assertTrue(isset($name->last)); + self::assertTrue(isset($name->isSimple)); + self::assertTrue(isset($name->isSpecial)); + self::assertTrue(isset($name->isBuiltin)); + } + + #[Test] + public function issetReturnsFalseForUnknownProperty(): void + { + $name = new Name([$this->id('Foo')]); + + self::assertFalse(isset($name->unknown)); + } + + #[Test] + public function readingUnknownPropertyThrows(): void + { + $name = new Name([$this->id('Foo')]); + + $this->expectException(\OutOfRangeException::class); + + /** @phpstan-ignore-next-line */ + $name->unknown; + } + + #[Test] + public function fullyQualifiedDefaultValueConstantIsFalse(): void + { + self::assertFalse(Name::IS_FULLY_QUALIFIED_DEFAULT_VALUE); + } + + #[Test] + public function toUnqualifiedLowerStringLowercasesNameWithoutLeadingDelimiter(): void + { + $name = Name::createFromString('\Vendor\Package\SomeClass'); + + self::assertSame('vendor\package\someclass', $name->toUnqualifiedLowerString()); + } + + #[Test] + public function toFullQualifiedLowerStringKeepsLeadingDelimiter(): void + { + $name = Name::createFromString('\Vendor\Package\SomeClass'); + + self::assertSame('\vendor\package\someclass', $name->toFullQualifiedLowerString()); + } + + #[Test] + public function toFullQualifiedStringIsIndependentOfTheQualificationFlag(): void + { + $name = Name::createFromString('Vendor\SomeClass'); + + self::assertFalse($name->isFullyQualified); + self::assertSame('\Vendor\SomeClass', $name->toFullQualifiedString()); + self::assertSame('\vendor\someclass', $name->toFullQualifiedLowerString()); + } + + #[Test] + public function toStringDependsOnTheQualificationFlag(): void + { + $unqualified = new Name([$this->id('Foo')], false); + $qualified = new Name([$this->id('Foo')], true); + + self::assertSame('Foo', $unqualified->toString()); + self::assertSame('\Foo', $qualified->toString()); + } + + #[Test] + public function createFromStringIgnoresRepeatedDelimiters(): void + { + $name = Name::createFromString('Foo\\\\Bar'); + + self::assertSame(['Foo', 'Bar'], $name->toStringArray()); + } + + #[Test] + public function createFromStringIgnoresTrailingDelimiter(): void + { + $name = Name::createFromString('Foo\Bar\\'); + + self::assertSame(['Foo', 'Bar'], $name->toStringArray()); + self::assertFalse($name->isFullyQualified); + } + + #[Test] + public function createFromStringAcceptsStringableObject(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return 'Foo\Bar'; + } + }; + + self::assertSame(['Foo', 'Bar'], Name::createFromString($stringable)->toStringArray()); + } + + #[Test] + public function createFromStringSegmentsAcceptsStrings(): void + { + $name = Name::createFromStringSegments(['Foo', 'Bar'], true); + + self::assertSame(['Foo', 'Bar'], $name->toStringArray()); + self::assertTrue($name->isFullyQualified); + } + + #[Test] + public function createFromStringThrowsOnEmptyString(): void + { + self::skipWhenAssertsAreDisabled(); + + $this->expectException(\InvalidArgumentException::class); + + Name::createFromString(''); + } + + #[Test] + public function createFromStringThrowsOnDelimiterOnlyString(): void + { + self::skipWhenAssertsAreDisabled(); + + $this->expectException(\InvalidArgumentException::class); + + Name::createFromString('\\'); + } + + #[Test] + public function sliceKeepsQualificationFlag(): void + { + $name = Name::createFromString('\Foo\Bar\Baz'); + + $sliced = $name->slice(1); + + self::assertSame(['Bar', 'Baz'], $sliced->toStringArray()); + self::assertTrue($sliced->isFullyQualified); + } + + #[Test] + public function sliceDoesNotModifyTheOriginalName(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + $name->slice(1); + + self::assertSame(['Foo', 'Bar', 'Baz'], $name->toStringArray()); + } + + #[Test] + public function withAddedKeepsQualificationFlagOfTheReceiver(): void + { + $name = Name::createFromString('\Some\Any'); + + $result = $name->withAdded(Name::createFromString('Test\Class')); + + self::assertSame(['Some', 'Any', 'Test', 'Class'], $result->toStringArray()); + self::assertTrue($result->isFullyQualified); + } + + #[Test] + public function withAddedDoesNotModifyArguments(): void + { + $name = Name::createFromString('Some\Any'); + $added = Name::createFromString('Test'); + + $name->withAdded($added); + + self::assertSame(['Some', 'Any'], $name->toStringArray()); + self::assertSame(['Test'], $added->toStringArray()); + } + + #[Test] + public function mergeWithReplacesTheAliasSegment(): void + { + $name = Name::createFromString('TypeLang\Parser\Exception'); + + $result = $name->mergeWith(Name::createFromString('Error\SemanticException')); + + self::assertSame('TypeLang\Parser\Exception\SemanticException', $result->toString()); + } + + #[Test] + public function mergeWithSimpleNameReturnsTheReceiverSegments(): void + { + $name = Name::createFromString('TypeLang\Parser\Node'); + + $result = $name->mergeWith(Name::createFromString('Node')); + + self::assertSame('TypeLang\Parser\Node', $result->toString()); + } + + #[Test] + public function toFullQualifiedKeepsSegments(): void + { + $name = Name::createFromString('Foo\Bar'); + + $result = $name->toFullQualified(); + + self::assertTrue($result->isFullyQualified); + self::assertSame(['Foo', 'Bar'], $result->toStringArray()); + self::assertFalse($name->isFullyQualified, 'The original name must not be modified'); + } + + #[Test] + public function toUnqualifiedKeepsSegments(): void + { + $name = Name::createFromString('\Foo\Bar'); + + $result = $name->toUnqualified(); + + self::assertFalse($result->isFullyQualified); + self::assertSame(['Foo', 'Bar'], $result->toStringArray()); + self::assertTrue($name->isFullyQualified, 'The original name must not be modified'); + } + + #[Test] + public function toUnqualifiedOfUnqualifiedNameReturnsEqualName(): void + { + $name = Name::createFromString('Foo\Bar'); + + $result = $name->toUnqualified(); + + self::assertFalse($result->isFullyQualified); + self::assertSame('Foo\Bar', $result->toString()); + } + + #[Test] + public function serializationRoundtripPreservesQualificationFlag(): void + { + $name = Name::createFromString('\Foo\Bar'); + + /** @var Name $restored */ + $restored = \unserialize(\serialize($name)); + + self::assertTrue($restored->isFullyQualified); + self::assertSame('\Foo\Bar', $restored->toString()); + } + + #[Test] + public function serializePayloadContainsSegmentsOffsetAndFqnFlag(): void + { + $name = Name::createFromString('Foo'); + $name->offset = 3; + + self::assertSame([$name->segments, 3, false], $name->__serialize()); + } + + #[Test] + public function unserializeThrowsWhenSegmentsAreMissing(): void + { + $name = Name::createFromString('Foo'); + + $this->expectException(\UnexpectedValueException::class); + + $name->__unserialize([]); + } + + #[Test] + public function offsetMethodIsAnAliasOfProperty(): void + { + $name = Name::createFromString('Foo'); + $name->offset = 9; + + self::assertSame(9, $name->offset()); + } + + #[Test] + public function stringCastIsTheSameAsToStringMethod(): void + { + $name = Name::createFromString('\Foo\Bar'); + + self::assertSame($name->toString(), (string) $name); + } } diff --git a/libs/types/tests/NodeListTest.php b/libs/types/tests/NodeListTest.php new file mode 100644 index 0000000..87f2a87 --- /dev/null +++ b/libs/types/tests/NodeListTest.php @@ -0,0 +1,287 @@ + $items + * @return NodeList + */ + private function list(array $items = []): NodeList + { + /** @var NodeList */ + return new class ($items) extends NodeList {}; + } + + private function node(string $name = 'Example'): Identifier + { + return new Identifier($name); + } + + #[Test] + public function emptyByDefault(): void + { + $list = $this->list(); + + self::assertCount(0, $list); + self::assertSame([], $list->items); + } + + #[Test] + public function constructorAcceptsTraversable(): void + { + $generator = (function (): \Generator { + yield $this->node('A'); + yield $this->node('B'); + })(); + + $list = $this->list(); + $list = new class ($generator) extends NodeList {}; + + self::assertCount(2, $list); + self::assertSame('A', (string) $list->items[0]); + self::assertSame('B', (string) $list->items[1]); + } + + #[Test] + public function constructorReindexesNonListArrays(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + + $list = new class ([7 => $a, 42 => $b]) extends NodeList {}; + + self::assertSame([$a, $b], $list->items); + } + + #[Test] + public function firstAndLastAreNullWhenEmpty(): void + { + $list = $this->list(); + + self::assertNull($list->first()); + self::assertNull($list->last()); + self::assertNull($list->first); + self::assertNull($list->last); + } + + #[Test] + public function firstAndLastReturnBoundaryNodes(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $c = $this->node('C'); + $list = $this->list([$a, $b, $c]); + + self::assertSame($a, $list->first()); + self::assertSame($c, $list->last()); + } + + #[Test] + public function firstAndLastAreTheSameNodeInSingleItemList(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + self::assertSame($a, $list->first()); + self::assertSame($a, $list->last()); + } + + #[Test] + public function virtualPropertiesAreAliasesOfMethods(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a, $b]); + + self::assertSame($list->first(), $list->first); + self::assertSame($list->last(), $list->last); + } + + #[Test] + public function virtualPropertiesReflectListMutations(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list->items[] = $b; + + self::assertSame($b, $list->last, 'The $last property must be recalculated on each access'); + } + + #[Test] + public function issetReturnsTrueForVirtualProperties(): void + { + $list = $this->list(); + + self::assertTrue(isset($list->first)); + self::assertTrue(isset($list->last)); + } + + #[Test] + public function issetReturnsFalseForUnknownProperty(): void + { + $list = $this->list(); + + self::assertFalse(isset($list->unknown)); + } + + #[Test] + public function readingUnknownPropertyThrows(): void + { + $list = $this->list(); + + $this->expectException(\OutOfRangeException::class); + + /** @phpstan-ignore-next-line */ + $list->unknown; + } + + #[Test] + public function findIndexReturnsPositionOfNode(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a, $b]); + + self::assertSame(0, $list->findIndex($a)); + self::assertSame(1, $list->findIndex($b)); + } + + #[Test] + public function findIndexReturnsNullForUnknownNode(): void + { + $list = $this->list([$this->node('A')]); + + self::assertNull($list->findIndex($this->node('B'))); + } + + #[Test] + public function findIndexComparesByIdentityNotByValue(): void + { + $list = $this->list([$this->node('A')]); + + self::assertNull($list->findIndex($this->node('A'))); + } + + #[Test] + public function offsetExistsReflectsListContent(): void + { + $list = $this->list([$this->node('A')]); + + self::assertTrue($list->offsetExists(0)); + self::assertFalse($list->offsetExists(1)); + self::assertTrue(isset($list[0])); + self::assertFalse(isset($list[1])); + } + + #[Test] + public function offsetGetReturnsNodeOrNull(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + self::assertSame($a, $list[0]); + self::assertNull($list[1]); + } + + #[Test] + public function offsetSetReplacesExistingNode(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list[0] = $b; + + self::assertCount(1, $list); + self::assertSame($b, $list[0]); + } + + #[Test] + public function offsetSetKeepsListIndexedSequentially(): void + { + $list = $this->list([$this->node('A')]); + + $list[5] = $this->node('B'); + + self::assertSame([0, 1], \array_keys($list->items)); + } + + #[Test] + public function offsetSetWithoutIndexAppendsNode(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list[] = $b; + + self::assertCount(2, $list); + self::assertSame($b, $list->last()); + } + + #[Test] + public function offsetUnsetRemovesNodeAndReindexes(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $c = $this->node('C'); + $list = $this->list([$a, $b, $c]); + + unset($list[1]); + + self::assertCount(2, $list); + self::assertSame([$a, $c], $list->items); + } + + #[Test] + public function offsetUnsetOfUnknownIndexKeepsListUnchanged(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + unset($list[42]); + + self::assertSame([$a], $list->items); + } + + #[Test] + public function iteratorYieldsAllNodesInOrder(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a, $b]); + + self::assertSame([$a, $b], \iterator_to_array($list->getIterator())); + } + + #[Test] + public function iteratorOfEmptyListYieldsNothing(): void + { + self::assertSame([], \iterator_to_array($this->list()->getIterator())); + } + + #[Test] + public function countReflectsNumberOfNodes(): void + { + self::assertCount(0, $this->list()); + self::assertCount(1, $this->list([$this->node('A')])); + self::assertCount(2, $this->list([$this->node('A'), $this->node('B')])); + } + + #[Test] + public function defaultOffsetIsZero(): void + { + self::assertSame(0, $this->list()->offset()); + } +} diff --git a/libs/types/tests/NodeTest.php b/libs/types/tests/NodeTest.php new file mode 100644 index 0000000..93c3e94 --- /dev/null +++ b/libs/types/tests/NodeTest.php @@ -0,0 +1,248 @@ + + */ + public static function provideNodes(): iterable + { + yield CallableTypeNode::class => [new CallableTypeNode(Name::createFromString('callable'))]; + yield ClassConstMaskNode::class => [new ClassConstMaskNode(Name::createFromString('Example'))]; + yield ClassConstNode::class => [ + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), + ]; + yield ConstMaskNode::class => [new ConstMaskNode(Name::createFromString('CONST'))]; + yield Identifier::class => [new Identifier('Example')]; + yield IntersectionTypeNode::class => [new IntersectionTypeNode(self::type('A'), self::type('B'))]; + yield Name::class => [Name::createFromString('Example')]; + yield NamedTypeNode::class => [self::type()]; + yield NullableTypeNode::class => [new NullableTypeNode(self::type())]; + yield TernaryExpressionNode::class => [ + new TernaryExpressionNode( + new EqualConditionNode(self::type('A'), self::type('B')), + self::type('C'), + self::type('D'), + ), + ]; + yield TypeOffsetAccessNode::class => [new TypeOffsetAccessNode(self::type(), self::type('Offset'))]; + yield TypesListNode::class => [new TypesListNode(self::type())]; + yield UnionTypeNode::class => [new UnionTypeNode(self::type('A'), self::type('B'))]; + + yield AttributeArgumentListNode::class => [new AttributeArgumentListNode()]; + yield AttributeArgumentNode::class => [new AttributeArgumentNode(self::type())]; + yield AttributeGroupListNode::class => [new AttributeGroupListNode()]; + yield AttributeGroupNode::class => [new AttributeGroupNode()]; + yield AttributeNode::class => [new AttributeNode(Name::createFromString('Deprecated'))]; + + yield CallableParameterListNode::class => [new CallableParameterListNode()]; + yield CallableParameterNode::class => [new CallableParameterNode(self::type())]; + + yield EqualConditionNode::class => [new EqualConditionNode(self::type('A'), self::type('B'))]; + yield GreaterThanConditionNode::class => [new GreaterThanConditionNode(self::type('A'), self::type('B'))]; + yield GreaterThanOrEqualConditionNode::class => [ + new GreaterThanOrEqualConditionNode(self::type('A'), self::type('B')), + ]; + yield LessThanConditionNode::class => [new LessThanConditionNode(self::type('A'), self::type('B'))]; + yield LessThanOrEqualConditionNode::class => [ + new LessThanOrEqualConditionNode(self::type('A'), self::type('B')), + ]; + yield NotEqualConditionNode::class => [new NotEqualConditionNode(self::type('A'), self::type('B'))]; + + yield BoolLiteralNode::class => [new BoolLiteralNode(true)]; + yield FloatLiteralNode::class => [new FloatLiteralNode(0.1)]; + yield IntLiteralNode::class => [new IntLiteralNode(42)]; + yield NullLiteralNode::class => [new NullLiteralNode()]; + yield StringLiteralNode::class => [new StringLiteralNode('example')]; + yield VariableLiteralNode::class => [new VariableLiteralNode('example')]; + + yield ClassConstFieldNode::class => [ + new ClassConstFieldNode( + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), + self::type(), + ), + ]; + yield ClassConstMaskFieldNode::class => [ + new ClassConstMaskFieldNode(new ClassConstMaskNode(Name::createFromString('Example')), self::type()), + ]; + yield ConstMaskFieldNode::class => [ + new ConstMaskFieldNode(new ConstMaskNode(Name::createFromString('CONST')), self::type()), + ]; + yield FieldsListNode::class => [new FieldsListNode()]; + yield ImplicitFieldNode::class => [new ImplicitFieldNode(self::type())]; + yield NamedFieldNode::class => [new NamedFieldNode(new Identifier('key'), self::type())]; + yield NumericFieldNode::class => [new NumericFieldNode(new IntLiteralNode(0), self::type())]; + yield StringNamedFieldNode::class => [ + new StringNamedFieldNode(new StringLiteralNode('key'), self::type()), + ]; + + yield TemplateArgumentListNode::class => [new TemplateArgumentListNode()]; + yield TemplateArgumentNode::class => [new TemplateArgumentNode(self::type())]; + } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeImplementsNodeInterface(Node $node): void + { + self::assertInstanceOf(NodeInterface::class, $node); + } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeHasZeroOffsetByDefault(Node $node): void + { + self::assertSame(0, $node->offset); + self::assertSame(0, $node->offset()); + } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeOffsetIsWritableAndReflectedByMethod(Node $node): void + { + $node->offset = 42; + + self::assertSame(42, $node->offset); + self::assertSame(42, $node->offset(), 'The offset() method must be an alias of the $offset property'); + } + + /** + * Guards the data provider above from getting out of sync with the package: + * every concrete node class must be listed there. + */ + #[Test] + public function providerCoversEveryConcreteNodeClass(): void + { + $covered = []; + + foreach (self::provideNodes() as [$node]) { + $covered[] = $node::class; + } + + $expected = []; + $directory = \dirname(__DIR__) . '/src'; + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), + ); + + foreach ($files as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $relative = \substr($file->getPathname(), \strlen($directory) + 1, -4); + $class = 'TypeLang\\Type\\' . \str_replace(['/', '\\'], '\\', $relative); + + if (!\class_exists($class)) { + continue; + } + + $reflection = new \ReflectionClass($class); + + if ($reflection->isAbstract() || !$reflection->isSubclassOf(Node::class)) { + continue; + } + + $expected[] = $class; + } + + \sort($expected); + \sort($covered); + + self::assertSame($expected, $covered); + } + + #[Test] + public function abstractNodeOffsetDefaultsToZero(): void + { + $node = new class extends Node {}; + + self::assertSame(0, $node->offset()); + } + + #[Test] + public function typeNodeIsANode(): void + { + $node = new class extends TypeNode {}; + + self::assertInstanceOf(Node::class, $node); + self::assertInstanceOf(NodeInterface::class, $node); + } + + #[Test] + public function wrappingTypeNodeStoresWrappedType(): void + { + $inner = self::type(); + $node = new TypesListNode($inner); + + self::assertSame($inner, $node->type); + } + + #[Test] + public function wrappingTypeNodeTypeIsMutable(): void + { + $node = new TypesListNode(self::type('A')); + $node->type = self::type('B'); + + self::assertSame('B', $node->type->name->toString()); + } +} diff --git a/libs/types/tests/Shape/ExplicitFieldNodeTest.php b/libs/types/tests/Shape/ExplicitFieldNodeTest.php new file mode 100644 index 0000000..a685792 --- /dev/null +++ b/libs/types/tests/Shape/ExplicitFieldNodeTest.php @@ -0,0 +1,224 @@ + + */ + public static function provideExplicitFields(): iterable + { + yield 'named' => [ + new NamedFieldNode(new Identifier('key'), self::type()), + 'key', + ]; + + yield 'string named' => [ + new StringNamedFieldNode(new StringLiteralNode('some key'), self::type()), + 'some key', + ]; + + yield 'numeric' => [ + new NumericFieldNode(new IntLiteralNode(42), self::type()), + '42', + ]; + + yield 'class const' => [ + new ClassConstFieldNode( + new ClassConstNode(Name::createFromString('Vendor\Status'), new Identifier('OK')), + self::type(), + ), + 'Vendor\Status::OK', + ]; + + yield 'class const mask' => [ + new ClassConstMaskFieldNode( + new ClassConstMaskNode(Name::createFromString('Vendor\Status'), new Identifier('IS_')), + self::type(), + ), + 'Vendor\Status::IS_*', + ]; + + yield 'const mask' => [ + new ConstMaskFieldNode(new ConstMaskNode(Name::createFromString('Vendor\STATUS_')), self::type()), + 'Vendor\STATUS_*', + ]; + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function indexMethodReturnsPrettyPrintedKey(ExplicitFieldNode $field, string $index): void + { + self::assertSame($index, $field->index()); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function indexPropertyIsAnAliasOfIndexMethod(ExplicitFieldNode $field, string $index): void + { + self::assertSame($field->index(), $field->index); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function issetReturnsTrueForIndexProperty(ExplicitFieldNode $field, string $index): void + { + self::assertTrue(isset($field->index)); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function issetReturnsFalseForUnknownProperty(ExplicitFieldNode $field, string $index): void + { + self::assertFalse(isset($field->unknown)); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function readingUnknownPropertyThrows(ExplicitFieldNode $field, string $index): void + { + $this->expectException(\OutOfRangeException::class); + + /** @phpstan-ignore-next-line */ + $field->unknown; + } + + #[Test] + public function indexIsRecalculatedAfterKeyMutation(): void + { + $field = new NamedFieldNode(new Identifier('key'), self::type()); + + self::assertSame('key', $field->index()); + + $field->key = new Identifier('other'); + + self::assertSame('other', $field->index(), 'The index must be derived from the current key'); + self::assertSame('other', $field->index); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function explicitFieldIsRequiredByDefault(ExplicitFieldNode $field, string $index): void + { + self::assertFalse($field->isOptional); + self::assertSame('required', (string) $field); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function explicitFieldHasNoAttributesByDefault(ExplicitFieldNode $field, string $index): void + { + self::assertNull($field->attributes); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function explicitFieldIsAFieldNode(ExplicitFieldNode $field, string $index): void + { + self::assertInstanceOf(FieldNode::class, $field); + self::assertInstanceOf(TypeNode::class, $field->type); + } + + #[Test] + public function optionalFieldIsStringifiedAsOptional(): void + { + $field = new NamedFieldNode(new Identifier('key'), self::type(), true); + + self::assertTrue($field->isOptional); + self::assertSame('optional', (string) $field); + } + + #[Test] + public function attributesAreStored(): void + { + $attributes = new AttributeGroupListNode(); + $field = new NamedFieldNode(new Identifier('key'), self::type(), false, $attributes); + + self::assertSame($attributes, $field->attributes); + } + + #[Test] + public function implicitFieldHasNoKey(): void + { + $field = new ImplicitFieldNode(self::type()); + + self::assertInstanceOf(FieldNode::class, $field); + self::assertNotInstanceOf(ExplicitFieldNode::class, $field); + self::assertSame('required', (string) $field); + } + + #[Test] + public function classConstMaskFieldWithoutConstantIsStringified(): void + { + $field = new ClassConstMaskFieldNode( + new ClassConstMaskNode(Name::createFromString('Vendor\Status')), + self::type(), + ); + + self::assertSame('Vendor\Status::*', $field->index()); + } + + #[Test] + public function numericFieldIndexOfZero(): void + { + $field = new NumericFieldNode(new IntLiteralNode(0), self::type()); + + self::assertSame('0', $field->index()); + } + + #[Test] + public function numericFieldIndexOfNegativeKey(): void + { + $field = new NumericFieldNode(new IntLiteralNode(-1), self::type()); + + self::assertSame('-1', $field->index()); + } + + #[Test] + public function stringNamedFieldIndexIsTheDecodedValue(): void + { + $field = new StringNamedFieldNode(new StringLiteralNode("a\nb"), self::type()); + + self::assertSame("a\nb", $field->index()); + } + + #[Test] + public function fieldTypeIsMutable(): void + { + $field = new NamedFieldNode(new Identifier('key'), self::type('A')); + $field->type = self::type('B'); + + self::assertSame('B', $field->type->name->toString()); + } +} diff --git a/libs/types/tests/Shape/NumericFieldNodeTest.php b/libs/types/tests/Shape/NumericFieldNodeTest.php index f01f6aa..c3e12c7 100644 --- a/libs/types/tests/Shape/NumericFieldNodeTest.php +++ b/libs/types/tests/Shape/NumericFieldNodeTest.php @@ -16,7 +16,7 @@ final class NumericFieldNodeTest extends TestCase #[Test] public function constructorStoresKeyAndType(): void { - $key = IntLiteralNode::parse('0'); + $key = new IntLiteralNode(0); $type = new NamedTypeNode(Name::createFromString('string')); $node = new NumericFieldNode($key, $type); @@ -27,7 +27,7 @@ public function constructorStoresKeyAndType(): void #[Test] public function indexReturnsStringifiedKeyValue(): void { - $key = IntLiteralNode::parse('42'); + $key = new IntLiteralNode(42); $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); self::assertSame('42', $node->index); @@ -36,7 +36,7 @@ public function indexReturnsStringifiedKeyValue(): void #[Test] public function indexForZeroKey(): void { - $key = IntLiteralNode::parse('0'); + $key = new IntLiteralNode(0); $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); self::assertSame('0', $node->index); @@ -46,7 +46,7 @@ public function indexForZeroKey(): void public function optionalDefaultsToFalse(): void { $node = new NumericFieldNode( - IntLiteralNode::parse('1'), + new IntLiteralNode(1), new NamedTypeNode(Name::createFromString('string')), ); @@ -57,7 +57,7 @@ public function optionalDefaultsToFalse(): void public function constructorStoresOptionalFlag(): void { $node = new NumericFieldNode( - IntLiteralNode::parse('1'), + new IntLiteralNode(1), new NamedTypeNode(Name::createFromString('string')), true, ); @@ -69,7 +69,7 @@ public function constructorStoresOptionalFlag(): void public function defaultOffsetIsZero(): void { $node = new NumericFieldNode( - IntLiteralNode::parse('0'), + new IntLiteralNode(0), new NamedTypeNode(Name::createFromString('int')), ); From 6a5da4f69c9427d89e0da34cb5d28600bbc0a383 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 16:52:02 +0300 Subject: [PATCH 005/119] Lowering type-lang/reader package to PHP 8.1 --- libs/reader/.github/workflows/tests.yml | 2 +- libs/reader/README.md | 2 +- libs/reader/composer.json | 4 ++-- libs/reader/phpstan.neon | 3 +++ libs/reader/src/PropertyAccessDirection.php | 2 +- libs/reader/src/ReflectionReader.php | 15 +++++++++++++-- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/libs/reader/.github/workflows/tests.yml b/libs/reader/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/reader/.github/workflows/tests.yml +++ b/libs/reader/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/reader/README.md b/libs/reader/README.md index fa1a977..c5967c3 100644 --- a/libs/reader/README.md +++ b/libs/reader/README.md @@ -32,7 +32,7 @@ composer require type-lang/reader ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/reader/composer.json b/libs/reader/composer.json index 462002b..f3ecd69 100644 --- a/libs/reader/composer.json +++ b/libs/reader/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/reader/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/types": "^2.0" }, "autoload": { @@ -19,7 +19,7 @@ }, "require-dev": { "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4", + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4", "type-lang/printer": "^2.0" }, "autoload-dev": { diff --git a/libs/reader/phpstan.neon b/libs/reader/phpstan.neon index cb1db62..f2d4b90 100644 --- a/libs/reader/phpstan.neon +++ b/libs/reader/phpstan.neon @@ -1,5 +1,8 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 treatPhpDocTypesAsCertain: false fileExtensions: - php diff --git a/libs/reader/src/PropertyAccessDirection.php b/libs/reader/src/PropertyAccessDirection.php index a1a8e71..f090435 100644 --- a/libs/reader/src/PropertyAccessDirection.php +++ b/libs/reader/src/PropertyAccessDirection.php @@ -9,5 +9,5 @@ enum PropertyAccessDirection case Read; case Write; - public const self DEFAULT = self::Read; + public const DEFAULT = self::Read; } diff --git a/libs/reader/src/ReflectionReader.php b/libs/reader/src/ReflectionReader.php index 3818a85..804e7e1 100644 --- a/libs/reader/src/ReflectionReader.php +++ b/libs/reader/src/ReflectionReader.php @@ -23,10 +23,15 @@ final class ReflectionReader implements ReaderInterface /** * @var list */ - private const array NULLABLE_BUILTIN_TYPES = ['null', 'mixed']; + private const NULLABLE_BUILTIN_TYPES = ['null', 'mixed']; public function findConstantType(\ReflectionClassConstant $constant): ?TypeNode { + // Constant types are available since PHP 8.3 + if (!\method_exists($constant, 'getType')) { + return null; + } + $type = $constant->getType(); if ($type instanceof \ReflectionType) { @@ -47,7 +52,13 @@ private function findPropertyNativeReadType(\ReflectionProperty $property): ?\Re private function findPropertyNativeWriteType(\ReflectionProperty $property): ?\ReflectionType { - $setter = $property->getHook(\PropertyHookType::Set); + // Property hooks are available since PHP 8.4 + if (!\method_exists($property, 'getHook')) { + return $this->findPropertyNativeReadType($property); + } + + /** @var \ReflectionMethod|null $setter */ + $setter = $property->getHook(\constant('PropertyHookType::Set')); if ($setter === null) { return $this->findPropertyNativeReadType($property); From e71e5170236be9906226efba61e1c81b06661122 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 16:52:47 +0300 Subject: [PATCH 006/119] Lowering type-lang/printer package to PHP 8.1 and remove type-lang/parser dependency --- libs/printer/.github/workflows/tests.yml | 2 +- libs/printer/README.md | 2 +- libs/printer/composer.json | 4 +- libs/printer/phpstan.neon | 3 + libs/printer/src/PrettyTypePrinter.php | 108 +++++++++++++++++------ libs/printer/src/TypePrinter.php | 4 +- 6 files changed, 91 insertions(+), 32 deletions(-) diff --git a/libs/printer/.github/workflows/tests.yml b/libs/printer/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/printer/.github/workflows/tests.yml +++ b/libs/printer/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/printer/README.md b/libs/printer/README.md index 65eac83..4c634d0 100644 --- a/libs/printer/README.md +++ b/libs/printer/README.md @@ -36,7 +36,7 @@ composer require type-lang/printer ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/printer/composer.json b/libs/printer/composer.json index 75d99c8..6133ca2 100644 --- a/libs/printer/composer.json +++ b/libs/printer/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/printer/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/types": "^2.0" }, "autoload": { @@ -20,7 +20,7 @@ "require-dev": { "type-lang/parser": "^2.0", "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4" + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" }, "autoload-dev": { "psr-4": { diff --git a/libs/printer/phpstan.neon b/libs/printer/phpstan.neon index cb1db62..f2d4b90 100644 --- a/libs/printer/phpstan.neon +++ b/libs/printer/phpstan.neon @@ -1,5 +1,8 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 treatPhpDocTypesAsCertain: false fileExtensions: - php diff --git a/libs/printer/src/PrettyTypePrinter.php b/libs/printer/src/PrettyTypePrinter.php index 7052d56..9f3aafa 100644 --- a/libs/printer/src/PrettyTypePrinter.php +++ b/libs/printer/src/PrettyTypePrinter.php @@ -4,7 +4,6 @@ namespace TypeLang\Printer; -use TypeLang\Parser\Traverser; use TypeLang\Printer\Exception\NonPrintableNodeException; use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Attribute\AttributeGroupNode; @@ -44,16 +43,16 @@ class PrettyTypePrinter extends TypePrinter { - public const bool DEFAULT_WRAP_INTERSECTION_TYPE = true; + public const DEFAULT_WRAP_INTERSECTION_TYPE = true; - public const bool DEFAULT_WRAP_UNION_TYPE = false; + public const DEFAULT_WRAP_UNION_TYPE = false; - public const bool DEFAULT_WRAP_CALLABLE_RETURN_TYPE = true; + public const DEFAULT_WRAP_CALLABLE_RETURN_TYPE = true; /** * @var int<0, max> */ - public const int DEFAULT_MULTILINE_SHAPE = 1; + public const DEFAULT_MULTILINE_SHAPE = 1; public function __construct( string $newLine = self::DEFAULT_NEW_LINE_DELIMITER, @@ -472,27 +471,84 @@ protected function shouldWrapReturnType(TypeNode $type): bool return true; } - $visitor = Traverser::through( - visitor: new Traverser\ClassNameMatcherVisitor( - class: LogicalTypeNode::class, - break: static function (Node $node): bool { - // Break on non-empty template parameters. - $isInTemplate = $node instanceof NamedTypeNode - && $node->arguments !== null - && $node->arguments->items !== []; - - // Break on non-empty shape fields. - $isInShape = $node instanceof NamedTypeNode - && $node->fields !== null - && $node->fields->items !== []; - - return $isInTemplate || $isInShape; - }, - ), - nodes: [$type], - ); - - return $visitor->isFound; + return self::containsLogicalType($type); + } + + /** + * Searches for a {@see LogicalTypeNode} in depth: The search is stopped + * as soon as a type nesting its children into template arguments or + * shape fields is reached. + */ + private static function containsLogicalType(TypeNode $type): bool + { + $stack = [$type]; + + while ($stack !== []) { + $node = \array_pop($stack); + + if ($node instanceof LogicalTypeNode) { + return true; + } + + if (self::shouldStopSearch($node)) { + return false; + } + + $children = self::fetchChildNodes($node); + + for ($index = \count($children) - 1; $index >= 0; --$index) { + $stack[] = $children[$index]; + } + } + + return false; + } + + private static function shouldStopSearch(Node $node): bool + { + if (!$node instanceof NamedTypeNode) { + return false; + } + + // Stop on non-empty template parameters. + $isInTemplate = $node->arguments !== null + && $node->arguments->items !== []; + + // Stop on non-empty shape fields. + $isInShape = $node->fields !== null + && $node->fields->items !== []; + + return $isInTemplate || $isInShape; + } + + /** + * @return list + */ + private static function fetchChildNodes(Node $node): array + { + $result = []; + + foreach (\get_object_vars($node) as $value) { + if ($value instanceof Node) { + $result[] = $value; + + continue; + } + + if (!\is_iterable($value)) { + continue; + } + + foreach ($value as $child) { + if (!$child instanceof Node) { + break; + } + + $result[] = $child; + } + } + + return $result; } /** diff --git a/libs/printer/src/TypePrinter.php b/libs/printer/src/TypePrinter.php index 02edfc3..8fd7569 100644 --- a/libs/printer/src/TypePrinter.php +++ b/libs/printer/src/TypePrinter.php @@ -12,12 +12,12 @@ abstract class TypePrinter implements TypePrinterInterface /** * @var non-empty-string */ - protected const string DEFAULT_NEW_LINE_DELIMITER = "\n"; + protected const DEFAULT_NEW_LINE_DELIMITER = "\n"; /** * @var non-empty-string */ - protected const string DEFAULT_INDENTION = ' '; + protected const DEFAULT_INDENTION = ' '; /** * @var int<0, max> From b3018942fe67940245d6adf92a59b3e37ba20349 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 17:01:16 +0300 Subject: [PATCH 007/119] Lowering type-lang/parser package to PHP 8.1 --- libs/parser/.github/workflows/tests.yml | 2 +- libs/parser/README.md | 2 +- libs/parser/bin/build | 1 + libs/parser/composer.json | 4 +- libs/parser/phpstan.neon | 3 + libs/parser/resources/grammar.php | 15 +- libs/parser/resources/grammar/literals.pp2 | 14 +- libs/parser/src/Exception/ParseException.php | 12 +- .../src/Exception/SemanticException.php | 12 +- libs/parser/src/Internal/ExecutionContext.php | 14 +- libs/parser/src/Internal/NodeBuilder.php | 4 +- .../src/Internal/PatternSequenceFetcher.php | 58 +++++++ .../src/Internal/ScannerSequenceFetcher.php | 93 ++++++++++ .../src/Internal/SequenceFetcherInterface.php | 36 ++++ libs/parser/src/Internal/StringParser.php | 58 +++++++ libs/parser/src/Internal/UtfCharRenderer.php | 59 +++++++ libs/parser/src/Literal/IntLiteralParser.php | 63 +++++++ .../src/Literal/StringLiteralParser.php | 64 +++++++ libs/parser/src/ParsedResult.php | 6 +- libs/parser/src/Traverser.php | 4 +- libs/parser/src/Traverser/DumperVisitor.php | 2 +- libs/parser/src/Traverser/MatcherVisitor.php | 37 +++- .../SimplePropertyAccessor.php | 11 +- .../src/Traverser/StreamDumperVisitor.php | 2 +- .../src/Traverser/StringDumperVisitor.php | 2 +- libs/parser/src/TypeParser.php | 28 ++- libs/parser/src/TypeParserFeatures.php | 46 ++--- libs/parser/src/TypeResolver.php | 4 +- .../TypeResolver/PhpUseStatementsReader.php | 6 +- .../NamespaceFinder.php | 2 +- .../ReflectionSourcePrefixReader.php | 2 +- .../PhpUseStatementsTransformer.php | 4 +- .../tests/Internal/SequenceFetcherTest.php | 162 ++++++++++++++++++ .../tests/Internal/StringParserTest.php | 82 +++++++++ .../tests/Internal/UtfCharRendererTest.php | 83 +++++++++ .../tests/Literal/IntLiteralParserTest.php | 75 ++++++++ .../tests/Literal/StringLiteralParserTest.php | 138 +++++++++++++++ libs/parser/tests/TestCase.php | 6 +- .../PhpUseStatementsReaderTest.php | 6 +- 39 files changed, 1121 insertions(+), 101 deletions(-) create mode 100644 libs/parser/src/Internal/PatternSequenceFetcher.php create mode 100644 libs/parser/src/Internal/ScannerSequenceFetcher.php create mode 100644 libs/parser/src/Internal/SequenceFetcherInterface.php create mode 100644 libs/parser/src/Internal/StringParser.php create mode 100644 libs/parser/src/Internal/UtfCharRenderer.php create mode 100644 libs/parser/src/Literal/IntLiteralParser.php create mode 100644 libs/parser/src/Literal/StringLiteralParser.php create mode 100644 libs/parser/tests/Internal/SequenceFetcherTest.php create mode 100644 libs/parser/tests/Internal/StringParserTest.php create mode 100644 libs/parser/tests/Internal/UtfCharRendererTest.php create mode 100644 libs/parser/tests/Literal/IntLiteralParserTest.php create mode 100644 libs/parser/tests/Literal/StringLiteralParserTest.php diff --git a/libs/parser/.github/workflows/tests.yml b/libs/parser/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/parser/.github/workflows/tests.yml +++ b/libs/parser/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/parser/README.md b/libs/parser/README.md index f2a0433..308d97f 100644 --- a/libs/parser/README.md +++ b/libs/parser/README.md @@ -33,7 +33,7 @@ composer require type-lang/parser ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/parser/bin/build b/libs/parser/bin/build index 4ab89a7..9e453b2 100644 --- a/libs/parser/bin/build +++ b/libs/parser/bin/build @@ -36,6 +36,7 @@ $grammar = (new Compiler()) ->build() ->withClassReference('TypeLang\\Type') ->withClassReference('TypeLang\\Parser\\Exception') + ->withClassReference('TypeLang\\Parser\\Literal') ->generate(); file_put_contents(__DIR__ . '/../resources/grammar.php', $grammar); diff --git a/libs/parser/composer.json b/libs/parser/composer.json index bc78049..15480d0 100644 --- a/libs/parser/composer.json +++ b/libs/parser/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/parser/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/types": "^2.0", "phplrt/lexer": "^3.7", "phplrt/parser": "^3.7", @@ -24,7 +24,7 @@ "jetbrains/phpstorm-attributes": "^1.2", "phplrt/compiler": "^3.7.5", "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4" + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" }, "autoload-dev": { "psr-4": { diff --git a/libs/parser/phpstan.neon b/libs/parser/phpstan.neon index ba4ecb1..c899209 100644 --- a/libs/parser/phpstan.neon +++ b/libs/parser/phpstan.neon @@ -1,5 +1,8 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 treatPhpDocTypesAsCertain: false fileExtensions: - php diff --git a/libs/parser/resources/grammar.php b/libs/parser/resources/grammar.php index 408de44..3dc27b4 100644 --- a/libs/parser/resources/grammar.php +++ b/libs/parser/resources/grammar.php @@ -3,6 +3,7 @@ declare(strict_types=1); use TypeLang\Parser\Exception; +use TypeLang\Parser\Literal; use TypeLang\Type; /** @@ -327,19 +328,19 @@ // The "$token" variable is an auto-generated $token = $ctx->lastProcessedToken; - return Type\Literal\FloatLiteralNode::parse($token->getValue()); + return new Type\Literal\FloatLiteralNode((float) $token->getValue(), $token->getValue()); }, 23 => function (Phplrt\Parser\Context $ctx, $children) { // The "$token" variable is an auto-generated $token = $ctx->lastProcessedToken; - return $this->integerPool[$token] ??= Type\Literal\IntLiteralNode::parse($token->getValue()); + return $this->integerPool[$token] ??= Literal\IntLiteralParser::parse($token->getValue()); }, 24 => static function (Phplrt\Parser\Context $ctx, $children) { // The "$token" variable is an auto-generated $token = $ctx->lastProcessedToken; - return Type\Literal\BoolLiteralNode::parse($token->getValue()); + return new Type\Literal\BoolLiteralNode(\strtolower($token->getValue()) === 'true', $token->getValue()); }, 25 => static function (Phplrt\Parser\Context $ctx, $children) { return new Type\Literal\NullLiteralNode($children->getValue()); @@ -348,25 +349,25 @@ // The "$token" variable is an auto-generated $token = $ctx->lastProcessedToken; - return Type\Literal\VariableLiteralNode::parse($token->getValue()); + return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); }, 29 => static function (Phplrt\Parser\Context $ctx, $children) { // The "$token" variable is an auto-generated $token = $ctx->lastProcessedToken; - return Type\Literal\VariableLiteralNode::parse($token->getValue()); + return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); }, 30 => static function (Phplrt\Parser\Context $ctx, $children) { // The "$token" variable is an auto-generated $token = $ctx->lastProcessedToken; - return Type\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue()); + return Literal\StringLiteralParser::createFromDoubleQuotedString($token->getValue()); }, 31 => static function (Phplrt\Parser\Context $ctx, $children) { // The "$token" variable is an auto-generated $token = $ctx->lastProcessedToken; - return Type\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue()); + return Literal\StringLiteralParser::createFromSingleQuotedString($token->getValue()); }, 45 => function (Phplrt\Parser\Context $ctx, $children) { // The "$offset" variable is an auto-generated diff --git a/libs/parser/resources/grammar/literals.pp2 b/libs/parser/resources/grammar/literals.pp2 index 2a0e750..8536fad 100644 --- a/libs/parser/resources/grammar/literals.pp2 +++ b/libs/parser/resources/grammar/literals.pp2 @@ -19,14 +19,14 @@ RawLiteral -> { ; VariableLiteral -> { - return Type\Literal\VariableLiteralNode::parse($token->getValue()); + return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); } : | ; ThisLiteral -> { - return Type\Literal\VariableLiteralNode::parse($token->getValue()); + return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); } : ; @@ -37,19 +37,19 @@ StringLiteral -> { return $this->stringPool[$token] ??= $children; } ; DoubleQuotedStringLiteral -> { - return Type\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue()); + return Literal\StringLiteralParser::createFromDoubleQuotedString($token->getValue()); } : ; SingleQuotedStringLiteral -> { - return Type\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue()); + return Literal\StringLiteralParser::createFromSingleQuotedString($token->getValue()); } : ; FloatLiteral -> { - return Type\Literal\FloatLiteralNode::parse($token->getValue()); + return new Type\Literal\FloatLiteralNode((float) $token->getValue(), $token->getValue()); } : | @@ -57,7 +57,7 @@ FloatLiteral -> { ; IntLiteral -> { - return $this->integerPool[$token] ??= Type\Literal\IntLiteralNode::parse($token->getValue()); + return $this->integerPool[$token] ??= Literal\IntLiteralParser::parse($token->getValue()); } : | @@ -66,7 +66,7 @@ IntLiteral -> { ; BoolLiteral -> { - return Type\Literal\BoolLiteralNode::parse($token->getValue()); + return new Type\Literal\BoolLiteralNode(\strtolower($token->getValue()) === 'true', $token->getValue()); } : ; diff --git a/libs/parser/src/Exception/ParseException.php b/libs/parser/src/Exception/ParseException.php index 82f8b03..3df51ce 100644 --- a/libs/parser/src/Exception/ParseException.php +++ b/libs/parser/src/Exception/ParseException.php @@ -10,17 +10,17 @@ */ abstract class ParseException extends \LogicException implements ParserExceptionInterface { - final public const int ERROR_CODE_UNEXPECTED_TOKEN = 0x01; + final public const ERROR_CODE_UNEXPECTED_TOKEN = 0x01; - final public const int ERROR_CODE_UNRECOGNIZED_TOKEN = 0x02; + final public const ERROR_CODE_UNRECOGNIZED_TOKEN = 0x02; - final public const int ERROR_CODE_UNEXPECTED_SYNTAX_ERROR = 0x03; + final public const ERROR_CODE_UNEXPECTED_SYNTAX_ERROR = 0x03; - final public const int ERROR_CODE_INTERNAL_ERROR = 0x05; + final public const ERROR_CODE_INTERNAL_ERROR = 0x05; - final public const int ERROR_CODE_SEMANTIC_ERROR_BASE = 0x06; + final public const ERROR_CODE_SEMANTIC_ERROR_BASE = 0x06; - protected const int CODE_LAST = self::ERROR_CODE_SEMANTIC_ERROR_BASE; + protected const CODE_LAST = self::ERROR_CODE_SEMANTIC_ERROR_BASE; final public function __construct(string $message, int $code = 0, ?\Throwable $previous = null) { diff --git a/libs/parser/src/Exception/SemanticException.php b/libs/parser/src/Exception/SemanticException.php index 575241f..22847e8 100644 --- a/libs/parser/src/Exception/SemanticException.php +++ b/libs/parser/src/Exception/SemanticException.php @@ -10,17 +10,17 @@ */ abstract class SemanticException extends \LogicException implements ParserExceptionInterface { - final public const int ERROR_CODE_SHAPE_KEY_DUPLICATION = 0x01; + final public const ERROR_CODE_SHAPE_KEY_DUPLICATION = 0x01; - final public const int ERROR_CODE_SHAPE_KEY_MIX = 0x02; + final public const ERROR_CODE_SHAPE_KEY_MIX = 0x02; - final public const int ERROR_CODE_VARIADIC_WITH_DEFAULT = 0x03; + final public const ERROR_CODE_VARIADIC_WITH_DEFAULT = 0x03; - final public const int ERROR_CODE_VARIADIC_ALREADY_VARIADIC = 0x04; + final public const ERROR_CODE_VARIADIC_ALREADY_VARIADIC = 0x04; - final public const int ERROR_CODE_INVALID_OPERATOR = 0x05; + final public const ERROR_CODE_INVALID_OPERATOR = 0x05; - protected const int CODE_LAST = self::ERROR_CODE_INVALID_OPERATOR; + protected const CODE_LAST = self::ERROR_CODE_INVALID_OPERATOR; /** * @param int<0, max> $offset diff --git a/libs/parser/src/Internal/ExecutionContext.php b/libs/parser/src/Internal/ExecutionContext.php index a6a3358..46649f1 100644 --- a/libs/parser/src/Internal/ExecutionContext.php +++ b/libs/parser/src/Internal/ExecutionContext.php @@ -41,14 +41,14 @@ * @internal this is an internal library class, please do not use it in your code * @psalm-internal TypeLang\Parser */ -final readonly class ExecutionContext +final class ExecutionContext { /** * @var ParserRuntime */ - private ParserRuntime $parser; + private readonly ParserRuntime $parser; - private LexerRuntime $lexer; + private readonly LexerRuntime $lexer; /** * In-memory string literal pool. @@ -57,7 +57,7 @@ * * @var \WeakMap */ - protected \WeakMap $stringPool; + protected readonly \WeakMap $stringPool; /** * In-memory integer literal pool. @@ -66,17 +66,17 @@ * * @var \WeakMap */ - protected \WeakMap $integerPool; + protected readonly \WeakMap $integerPool; public function __construct( /** * @api this property is accessible inside the grammar reducers */ - protected TypeParserFeatures $features, + protected readonly TypeParserFeatures $features, /** * @api this property is accessible inside the grammar reducers */ - protected bool $tolerant, + protected readonly bool $tolerant, ) { /** @phpstan-var GrammarConfigArrayType $grammar */ $grammar = require __DIR__ . '/../../resources/grammar.php'; diff --git a/libs/parser/src/Internal/NodeBuilder.php b/libs/parser/src/Internal/NodeBuilder.php index 03173b3..f5dfcf2 100644 --- a/libs/parser/src/Internal/NodeBuilder.php +++ b/libs/parser/src/Internal/NodeBuilder.php @@ -12,13 +12,13 @@ * @internal this is an internal library class, please do not use it in your code * @psalm-internal TypeLang\Parser */ -final readonly class NodeBuilder implements BuilderInterface +final class NodeBuilder implements BuilderInterface { /** * @param array|non-empty-string, callable(Context, mixed):mixed> $reducers */ public function __construct( - private array $reducers, + private readonly array $reducers, ) {} public function build(Context $context, mixed $result): mixed diff --git a/libs/parser/src/Internal/PatternSequenceFetcher.php b/libs/parser/src/Internal/PatternSequenceFetcher.php new file mode 100644 index 0000000..9128870 --- /dev/null +++ b/libs/parser/src/Internal/PatternSequenceFetcher.php @@ -0,0 +1,58 @@ + $matches */ + foreach ($matches as $match) { + if (isset($result[$match[0]])) { + continue; + } + + // A unicode sequence, like a "\u{FFFF}" + if ($match[1] !== '') { + /** @var int<0, max> $code */ + $code = (int) \hexdec($match[1]); + + $result[$match[0]] = UtfCharRenderer::render($code); + + continue; + } + + // A hexadecimal sequence, like a "\xFF" + if (($match[2] ?? '') !== '') { + // @phpstan-ignore-next-line : A hexdec returns int<0, 255> + $result[$match[0]] = \chr((int) \hexdec($match[2] ?? '')); + + continue; + } + + // An octal sequence, like a "\101". Overflowed sequences + // (greater than a "\377") are truncated, like in PHP itself. + $result[$match[0]] = \chr(((int) \octdec($match[3] ?? '')) & 0xFF); + } + + return $result; + } +} diff --git a/libs/parser/src/Internal/ScannerSequenceFetcher.php b/libs/parser/src/Internal/ScannerSequenceFetcher.php new file mode 100644 index 0000000..11f4d0b --- /dev/null +++ b/libs/parser/src/Internal/ScannerSequenceFetcher.php @@ -0,0 +1,93 @@ + + $result[$sequence] ??= \chr((int) \hexdec(\substr($sequence, 2))); + $offset += $digits + 1; + + continue; + } + } elseif ($char === 'u') { + $digits = \strspn($body, self::HEX_DIGITS, $offset + 2); + + // An utf-8 sequence, like a "\u{FFFF}" + if ( + $digits !== 0 + && ($body[$offset + 1] ?? '') === '{' + && ($body[$offset + $digits + 2] ?? '') === '}' + ) { + $sequence = \substr($body, $position, $digits + 4); + + /** @var int<0, max> $code */ + $code = (int) \hexdec(\substr($sequence, 3, $digits)); + + $result[$sequence] ??= UtfCharRenderer::render($code); + $offset += $digits + 3; + + continue; + } + } elseif ($char >= '0' && $char <= '7') { + // An octal sequence, like a "\101". Overflowed sequences + // (greater than a "\377") are truncated, like in PHP itself. + $digits = \strspn($body, self::OCTAL_DIGITS, $offset, 3); + $sequence = \substr($body, $position, $digits + 1); + $result[$sequence] ??= \chr(((int) \octdec(\substr($sequence, 1))) & 0xFF); + $offset += $digits; + + continue; + } + + ++$offset; + } + + return $result; + } +} diff --git a/libs/parser/src/Internal/SequenceFetcherInterface.php b/libs/parser/src/Internal/SequenceFetcherInterface.php new file mode 100644 index 0000000..03af20b --- /dev/null +++ b/libs/parser/src/Internal/SequenceFetcherInterface.php @@ -0,0 +1,36 @@ + + */ + public const ESCAPED_CHARS = [ + '\n' => "\n", + '\r' => "\r", + '\t' => "\t", + '\v' => "\v", + '\e' => "\e", + '\f' => "\f", + '\$' => '$', + '\"' => '"', + '\\\\' => '\\', + ]; + + /** + * Returns a "sequence => replacement" map of all constant sequences along + * with each hexadecimal (like a "\xFF"), octal (like a "\101") and utf-8 + * (like a "\u{FFFF}") sequence occurred in the passed string. + * + * @link https://www.php.net/manual/en/language.types.string.php + * + * @return non-empty-array + */ + public static function fetch(string $body): array; +} diff --git a/libs/parser/src/Internal/StringParser.php b/libs/parser/src/Internal/StringParser.php new file mode 100644 index 0000000..54f445a --- /dev/null +++ b/libs/parser/src/Internal/StringParser.php @@ -0,0 +1,58 @@ + + */ + private const SCANNER_THRESHOLD = 4; + + /** + * Method for parsing and decode all escaped character sequences: Special + * chars (like a "\n"), hexadecimal (like a "\xFF"), octal (like a "\101") + * and utf-8 (like a "\u{FFFF}") ones. + * + * @link https://www.php.net/manual/en/language.types.string.php + */ + public static function parse(string $body): string + { + if (!\str_contains($body, '\\')) { + return $body; + } + + return \strtr($body, self::fetchReplacements($body)); + } + + /** + * Chooses a {@see SequenceFetcherInterface} implementation suitable for + * the passed string. + * + * @return non-empty-array + */ + private static function fetchReplacements(string $body): array + { + // Numeric sequences require an additional pass, which can be skipped + // in case of the string does not contain any of their prefixes. + if (@\preg_match(self::NUMERIC_PREFIX_PATTERN, $body) !== 1) { + return SequenceFetcherInterface::ESCAPED_CHARS; + } + + if (\substr_count($body, '\\') <= self::SCANNER_THRESHOLD) { + return ScannerSequenceFetcher::fetch($body); + } + + return PatternSequenceFetcher::fetch($body); + } +} diff --git a/libs/parser/src/Internal/UtfCharRenderer.php b/libs/parser/src/Internal/UtfCharRenderer.php new file mode 100644 index 0000000..ac82a9f --- /dev/null +++ b/libs/parser/src/Internal/UtfCharRenderer.php @@ -0,0 +1,59 @@ + + */ + private const MAX_CODE_POINT = 0x10FFFF; + + /** + * @var non-empty-string + */ + private const REPLACEMENT_CHAR = "\u{FFFD}"; + + /** + * Method for encoding an utf-8 character by its code. + * + * Codes above the last unicode code point are encoded as a replacement + * char, because such a sequence cannot be expressed in utf-8. + * + * @param int<0, max> $code + */ + public static function render(int $code): string + { + if ($code > self::MAX_CODE_POINT) { + return self::REPLACEMENT_CHAR; + } + + // @phpstan-ignore-next-line : PHPStan false-positive mb_chr evaluation + if (\function_exists('\\mb_chr') && ($result = \mb_chr($code)) !== false) { + return $result; + } + + if (0x80 > $code) { + // @phpstan-ignore-next-line : Code is valid + return \chr($code); + } + + if (0x800 > $code) { + return \chr(0xC0 | $code >> 6) + . \chr(0x80 | $code & 0x3F); + } + + if (0x10000 > $code) { + return \chr(0xE0 | $code >> 12) + . \chr(0x80 | $code >> 6 & 0x3F) + . \chr(0x80 | $code & 0x3F); + } + + return \chr(0xF0 | $code >> 18) + . \chr(0x80 | $code >> 12 & 0x3F) + . \chr(0x80 | $code >> 6 & 0x3F) + . \chr(0x80 | $code & 0x3F); + } +} diff --git a/libs/parser/src/Literal/IntLiteralParser.php b/libs/parser/src/Literal/IntLiteralParser.php new file mode 100644 index 0000000..d919b32 --- /dev/null +++ b/libs/parser/src/Literal/IntLiteralParser.php @@ -0,0 +1,63 @@ + \base_convert(\substr($literal, 2), 16, 10), + // binary + 'b', 'B' => \base_convert(\substr($literal, 2), 2, 10), + // octal + 'o', 'O' => \base_convert(\substr($literal, 2), 8, 10), + // octal (legacy) + default => \base_convert($literal, 8, 10), + }]; + } + + /** @var array{bool, numeric-string} */ + return [$negative, $literal]; + } +} diff --git a/libs/parser/src/Literal/StringLiteralParser.php b/libs/parser/src/Literal/StringLiteralParser.php new file mode 100644 index 0000000..b396889 --- /dev/null +++ b/libs/parser/src/Literal/StringLiteralParser.php @@ -0,0 +1,64 @@ + "'", + '\\\\' => '\\', + ]), + raw: $value, + ); + } +} diff --git a/libs/parser/src/ParsedResult.php b/libs/parser/src/ParsedResult.php index 70862ff..67d5473 100644 --- a/libs/parser/src/ParsedResult.php +++ b/libs/parser/src/ParsedResult.php @@ -6,15 +6,15 @@ use TypeLang\Type\TypeNode; -final readonly class ParsedResult +final class ParsedResult { public function __construct( - public TypeNode $type, + public readonly TypeNode $type, /** * Last processed token offset. * * @var int<0, max> */ - public int $offset, + public readonly int $offset, ) {} } diff --git a/libs/parser/src/Traverser.php b/libs/parser/src/Traverser.php index 6a728d3..755eb75 100644 --- a/libs/parser/src/Traverser.php +++ b/libs/parser/src/Traverser.php @@ -23,7 +23,9 @@ public function __construct( iterable $visitors = [], private PropertyAccessorInterface $propertyAccessor = new SimplePropertyAccessor(), ) { - $this->visitors = \iterator_to_array($visitors, false); + $this->visitors = \is_array($visitors) + ? \array_values($visitors) + : \iterator_to_array($visitors, false); } /** diff --git a/libs/parser/src/Traverser/DumperVisitor.php b/libs/parser/src/Traverser/DumperVisitor.php index 8d0131a..84780d1 100644 --- a/libs/parser/src/Traverser/DumperVisitor.php +++ b/libs/parser/src/Traverser/DumperVisitor.php @@ -11,7 +11,7 @@ abstract class DumperVisitor extends Visitor /** * @var non-empty-string */ - public const string DEFAULT_SIMPLIFIED_NODE_NAMESPACE = 'TypeLang\\Type\\'; + public const DEFAULT_SIMPLIFIED_NODE_NAMESPACE = 'TypeLang\\Type\\'; /** * @var int<0, max> diff --git a/libs/parser/src/Traverser/MatcherVisitor.php b/libs/parser/src/Traverser/MatcherVisitor.php index 9dba466..2771344 100644 --- a/libs/parser/src/Traverser/MatcherVisitor.php +++ b/libs/parser/src/Traverser/MatcherVisitor.php @@ -6,13 +6,19 @@ use TypeLang\Type\Node; +/** + * @property-read bool $isFound An alias of {@see isFound()} method. + */ class MatcherVisitor extends Visitor { - public private(set) ?Node $node = null; + /** + * @var list + */ + private const VIRTUAL_PROPERTIES = [ + 'isFound', + ]; - public bool $isFound { - get => $this->node !== null; - } + public ?Node $node = null; private bool $shouldContinue = false; @@ -25,6 +31,29 @@ public function __construct( private readonly ?\Closure $break = null, ) {} + /** + * Returns {@see true} in case of a node matching the criteria was found. + */ + public function isFound(): bool + { + return $this->node !== null; + } + + public function __get(string $name): mixed + { + return match ($name) { + 'isFound' => $this->isFound(), + default => throw new \OutOfRangeException( + message: \sprintf('Undefined property %s::$%s', static::class, $name), + ), + }; + } + + public function __isset(string $name): bool + { + return \in_array($name, self::VIRTUAL_PROPERTIES, true); + } + public function before(): void { $this->node = null; diff --git a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php index 685184e..fb214f9 100644 --- a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php +++ b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php @@ -4,15 +4,20 @@ namespace TypeLang\Parser\Traverser\PropertyAccessor; -final readonly class SimplePropertyAccessor implements PropertyAccessorInterface +final class SimplePropertyAccessor implements PropertyAccessorInterface { /** * Skips static and hooked properties */ private function shouldSkip(\ReflectionProperty $property): bool { - return $property->isStatic() - || $property->hasHooks(); + if ($property->isStatic()) { + return true; + } + + // Property hooks are available since PHP 8.4 + return \method_exists($property, 'hasHooks') + && $property->hasHooks(); } public function unwrap(object $object): iterable diff --git a/libs/parser/src/Traverser/StreamDumperVisitor.php b/libs/parser/src/Traverser/StreamDumperVisitor.php index 489ba98..1340265 100644 --- a/libs/parser/src/Traverser/StreamDumperVisitor.php +++ b/libs/parser/src/Traverser/StreamDumperVisitor.php @@ -6,7 +6,7 @@ final class StreamDumperVisitor extends DumperVisitor { - public const string DEFAULT_OUTPUT_STREAM = 'php://stderr'; + public const DEFAULT_OUTPUT_STREAM = 'php://stderr'; /** * @var resource diff --git a/libs/parser/src/Traverser/StringDumperVisitor.php b/libs/parser/src/Traverser/StringDumperVisitor.php index b336e74..4ca6951 100644 --- a/libs/parser/src/Traverser/StringDumperVisitor.php +++ b/libs/parser/src/Traverser/StringDumperVisitor.php @@ -6,7 +6,7 @@ final class StringDumperVisitor extends DumperVisitor { - public private(set) string $output = ''; + public string $output = ''; public function before(): void { diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php index d17fbdc..2e1e98b 100644 --- a/libs/parser/src/TypeParser.php +++ b/libs/parser/src/TypeParser.php @@ -24,13 +24,9 @@ final class TypeParser implements TypeParserInterface { - private ExecutionContext $strict { - get => $this->strict ??= new ExecutionContext($this->features, false); - } + private ?ExecutionContext $strict = null; - private ExecutionContext $tolerant { - get => $this->tolerant ??= new ExecutionContext($this->features, true); - } + private ?ExecutionContext $tolerant = null; public function __construct( public readonly TypeParserFeatures $features = new TypeParserFeatures(), @@ -57,14 +53,14 @@ public function withFeatures(bool ...$features): self public function parse(#[Language('PHP')] mixed $source): TypeNode { - $result = $this->execute($this->strict, $source); + $result = $this->execute($this->strictContext(), $source); return $result->type; } public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult { - return $this->execute($this->tolerant, $source); + return $this->execute($this->tolerantContext(), $source); } private function execute(ExecutionContext $context, mixed $source): ParsedResult @@ -150,4 +146,20 @@ private function internalError(\Throwable $e, ReadableInterface $source): ParseE e: $e, ); } + + /** + * Returns a lazily created execution context for the strict mode. + */ + private function strictContext(): ExecutionContext + { + return $this->strict ??= new ExecutionContext($this->features, false); + } + + /** + * Returns a lazily created execution context for the tolerant mode. + */ + private function tolerantContext(): ExecutionContext + { + return $this->tolerant ??= new ExecutionContext($this->features, true); + } } diff --git a/libs/parser/src/TypeParserFeatures.php b/libs/parser/src/TypeParserFeatures.php index 2c3dc3d..ab7bc36 100644 --- a/libs/parser/src/TypeParserFeatures.php +++ b/libs/parser/src/TypeParserFeatures.php @@ -20,65 +20,65 @@ * // => Error: Conditional expressions not allowed in ... * ``` */ -final readonly class TypeParserFeatures +final class TypeParserFeatures { - public const bool CONDITIONAL_FEATURES_DEFAULT_VALUE = true; - public const bool SHAPES_FEATURES_DEFAULT_VALUE = true; - public const bool CALLABLES_FEATURES_DEFAULT_VALUE = true; - public const bool LITERALS_FEATURES_DEFAULT_VALUE = true; - public const bool GENERICS_FEATURES_DEFAULT_VALUE = true; - public const bool UNION_FEATURES_DEFAULT_VALUE = true; - public const bool INTERSECTION_FEATURES_DEFAULT_VALUE = true; - public const bool LIST_FEATURES_DEFAULT_VALUE = true; - public const bool OFFSETS_FEATURES_DEFAULT_VALUE = true; - public const bool HINTS_FEATURES_DEFAULT_VALUE = true; - public const bool ATTRIBUTES_FEATURES_DEFAULT_VALUE = true; + public const CONDITIONAL_FEATURES_DEFAULT_VALUE = true; + public const SHAPES_FEATURES_DEFAULT_VALUE = true; + public const CALLABLES_FEATURES_DEFAULT_VALUE = true; + public const LITERALS_FEATURES_DEFAULT_VALUE = true; + public const GENERICS_FEATURES_DEFAULT_VALUE = true; + public const UNION_FEATURES_DEFAULT_VALUE = true; + public const INTERSECTION_FEATURES_DEFAULT_VALUE = true; + public const LIST_FEATURES_DEFAULT_VALUE = true; + public const OFFSETS_FEATURES_DEFAULT_VALUE = true; + public const HINTS_FEATURES_DEFAULT_VALUE = true; + public const ATTRIBUTES_FEATURES_DEFAULT_VALUE = true; public function __construct( /** * Enables or disables support for conditional types such as `T ? U : V` */ - public bool $conditions = self::CONDITIONAL_FEATURES_DEFAULT_VALUE, + public readonly bool $conditions = self::CONDITIONAL_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for shape types such as `T{key: U}` */ - public bool $shapes = self::SHAPES_FEATURES_DEFAULT_VALUE, + public readonly bool $shapes = self::SHAPES_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for callable types such as `fn(T, U): V` */ - public bool $callables = self::CALLABLES_FEATURES_DEFAULT_VALUE, + public readonly bool $callables = self::CALLABLES_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for literal types such as `42` or `'foo'` */ - public bool $literals = self::LITERALS_FEATURES_DEFAULT_VALUE, + public readonly bool $literals = self::LITERALS_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for generic type arguments such as `T` */ - public bool $generics = self::GENERICS_FEATURES_DEFAULT_VALUE, + public readonly bool $generics = self::GENERICS_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for union types such as `T | U` */ - public bool $unions = self::UNION_FEATURES_DEFAULT_VALUE, + public readonly bool $unions = self::UNION_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for intersection types such as `T & U` */ - public bool $intersections = self::INTERSECTION_FEATURES_DEFAULT_VALUE, + public readonly bool $intersections = self::INTERSECTION_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for short list types such as `T[]` */ - public bool $lists = self::LIST_FEATURES_DEFAULT_VALUE, + public readonly bool $lists = self::LIST_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for offset access types such as `T[U]` */ - public bool $offsets = self::OFFSETS_FEATURES_DEFAULT_VALUE, + public readonly bool $offsets = self::OFFSETS_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for generic variance hints such as `T` */ - public bool $hints = self::HINTS_FEATURES_DEFAULT_VALUE, + public readonly bool $hints = self::HINTS_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for attributes such as `#[attr]` */ - public bool $attributes = self::ATTRIBUTES_FEATURES_DEFAULT_VALUE, + public readonly bool $attributes = self::ATTRIBUTES_FEATURES_DEFAULT_VALUE, ) {} /** diff --git a/libs/parser/src/TypeResolver.php b/libs/parser/src/TypeResolver.php index 687c994..1a2490b 100644 --- a/libs/parser/src/TypeResolver.php +++ b/libs/parser/src/TypeResolver.php @@ -9,13 +9,13 @@ use TypeLang\Parser\TypeResolver\PhpUseStatementsTransformer; use TypeLang\Type\TypeNode; -final readonly class TypeResolver +final class TypeResolver { public function __construct( /** * @var array */ - private array $imports = [], + private readonly array $imports = [], ) {} /** diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php index 4b72cb2..32c9abb 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php @@ -7,10 +7,10 @@ use TypeLang\Parser\TypeResolver\PhpUseStatementsReader\NamespaceFinder; use TypeLang\Parser\TypeResolver\PhpUseStatementsReader\ReflectionSourcePrefixReader; -final readonly class PhpUseStatementsReader +final class PhpUseStatementsReader { - private ReflectionSourcePrefixReader $reader; - private NamespaceFinder $namespace; + private readonly ReflectionSourcePrefixReader $reader; + private readonly NamespaceFinder $namespace; public function __construct() { diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php index e4b7283..f67b96d 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php @@ -4,7 +4,7 @@ namespace TypeLang\Parser\TypeResolver\PhpUseStatementsReader; -final readonly class NamespaceFinder +final class NamespaceFinder { /** * @param \Iterator $tokens diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php index dfce2d1..186d7d5 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php @@ -4,7 +4,7 @@ namespace TypeLang\Parser\TypeResolver\PhpUseStatementsReader; -final readonly class ReflectionSourcePrefixReader +final class ReflectionSourcePrefixReader { /** * @param \ReflectionClass $class diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php b/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php index 5a2ae3a..61d40ac 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php @@ -41,12 +41,12 @@ * // > } * ``` */ -final readonly class PhpUseStatementsTransformer +final class PhpUseStatementsTransformer { /** * @var array */ - private array $replacements; + private readonly array $replacements; /** * @param iterable $replacements diff --git a/libs/parser/tests/Internal/SequenceFetcherTest.php b/libs/parser/tests/Internal/SequenceFetcherTest.php new file mode 100644 index 0000000..4fb88a0 --- /dev/null +++ b/libs/parser/tests/Internal/SequenceFetcherTest.php @@ -0,0 +1,162 @@ +}> + */ + public static function provideFetchers(): iterable + { + yield 'scanner' => [ScannerSequenceFetcher::class]; + yield 'pattern' => [PatternSequenceFetcher::class]; + } + + /** + * @return iterable + */ + public static function provideBodies(): iterable + { + yield 'empty' => ['']; + yield 'without sequences' => ['example']; + yield 'constant sequences' => ['a\nb\rc\td\ve\ff\$g\"h']; + yield 'backslash' => ['a\\\\b']; + yield 'backslash before escape sequence' => ['a\\\\nb']; + yield 'backslash before hexadecimal' => ['a\\\\x41b']; + yield 'hexadecimal' => ['\x41']; + yield 'uppercase hexadecimal prefix' => ['\X41']; + yield 'short hexadecimal' => ['\x9']; + yield 'null byte' => ['\x00']; + yield 'octal' => ['\101']; + yield 'octal null byte' => ['\0']; + yield 'octal overflow' => ['\777']; + yield 'unicode' => ['\u{48}']; + yield 'multibyte unicode' => ['\u{1F600}']; + yield 'last unicode code point' => ['\u{10FFFF}']; + yield 'code point above the unicode range' => ['\u{110000}']; + yield 'unknown sequence' => ['a\qb']; + yield 'incomplete unicode' => ['a\u{}b']; + yield 'incomplete hexadecimal' => ['a\xzb']; + yield 'trailing backslash' => ['a\\']; + yield 'repeated sequences' => ['\x41\x41\u{42}\u{42}\101\101']; + yield 'many sequences' => ['\x41\x42\u{43}\104\x45\u{46}\107\x48']; + yield 'raw null byte' => ["a\0b"]; + yield 'invalid utf8' => ["\xFF\xFE"]; + yield 'all kinds at once' => ['\x41\u{42}\103\n\\\\']; + } + + #[Test] + #[DataProvider('provideFetchers')] + public function fetcherImplementsTheContract(string $fetcher): void + { + self::assertTrue(\is_subclass_of($fetcher, SequenceFetcherInterface::class)); + } + + #[Test] + #[DataProvider('provideBodies')] + public function everyScannedSequenceIsAlsoMatchedByThePattern(string $body): void + { + $pattern = PatternSequenceFetcher::fetch($body); + + foreach (ScannerSequenceFetcher::fetch($body) as $sequence => $replacement) { + self::assertSame($replacement, $pattern[$sequence] ?? null, \sprintf( + 'A "%s" sequence replacement must be the same in both fetchers', + $sequence, + )); + } + } + + /** + * The scanner skips an escaped backslash together with the char behind it, + * so a sequence that is not applied does not get into the map at all. The + * pattern based one collects it, but {@see strtr()} never applies it: The + * "\\" replacement wins on that position because of being matched first. + */ + #[Test] + public function scannerSkipsSequencesBehindAnEscapedBackslash(): void + { + $body = 'a\\\\x41b'; + + self::assertArrayNotHasKey('\x41', ScannerSequenceFetcher::fetch($body)); + self::assertArrayHasKey('\x41', PatternSequenceFetcher::fetch($body)); + + self::assertSame('a\x41b', StringParser::parse($body)); + } + + #[Test] + #[DataProvider('provideBodies')] + public function bothFetchersDecodeTheBodyIdentically(string $body): void + { + $scanner = \strtr($body, ScannerSequenceFetcher::fetch($body)); + $pattern = \strtr($body, PatternSequenceFetcher::fetch($body)); + + self::assertSame($scanner, $pattern); + self::assertSame($scanner, StringParser::parse($body)); + } + + #[Test] + #[DataProvider('provideBodies')] + public function scannerAlwaysContainsConstantSequences(string $body): void + { + $result = ScannerSequenceFetcher::fetch($body); + + foreach (SequenceFetcherInterface::ESCAPED_CHARS as $sequence => $replacement) { + self::assertSame($replacement, $result[$sequence] ?? null); + } + } + + #[Test] + #[DataProvider('provideBodies')] + public function patternAlwaysContainsConstantSequences(string $body): void + { + $result = PatternSequenceFetcher::fetch($body); + + foreach (SequenceFetcherInterface::ESCAPED_CHARS as $sequence => $replacement) { + self::assertSame($replacement, $result[$sequence] ?? null); + } + } + + #[Test] + public function scannerFetchesNumericSequences(): void + { + $result = ScannerSequenceFetcher::fetch('\x41\u{42}\103'); + + self::assertSame('A', $result['\x41'] ?? null); + self::assertSame('B', $result['\u{42}'] ?? null); + self::assertSame('C', $result['\103'] ?? null); + } + + #[Test] + public function patternFetchesNumericSequences(): void + { + $result = PatternSequenceFetcher::fetch('\x41\u{42}\103'); + + self::assertSame('A', $result['\x41'] ?? null); + self::assertSame('B', $result['\u{42}'] ?? null); + self::assertSame('C', $result['\103'] ?? null); + } + + #[Test] + #[DataProvider('provideFetchers')] + public function fetcherReturnsOnlyConstantSequencesForAPlainString(string $fetcher): void + { + self::assertSame(SequenceFetcherInterface::ESCAPED_CHARS, $fetcher::fetch('example')); + } + + #[Test] + public function escapedCharsContainTheBackslashItself(): void + { + self::assertSame('\\', SequenceFetcherInterface::ESCAPED_CHARS['\\\\'] ?? null); + } +} diff --git a/libs/parser/tests/Internal/StringParserTest.php b/libs/parser/tests/Internal/StringParserTest.php new file mode 100644 index 0000000..455110e --- /dev/null +++ b/libs/parser/tests/Internal/StringParserTest.php @@ -0,0 +1,82 @@ + + */ + public static function provideBodies(): iterable + { + yield 'empty' => ['', '']; + yield 'without sequences' => ['example', 'example']; + yield 'line feed' => ['a\nb', "a\nb"]; + yield 'carriage return' => ['a\rb', "a\rb"]; + yield 'tab' => ['a\tb', "a\tb"]; + yield 'vertical tab' => ['a\vb', "a\vb"]; + yield 'escape' => ['a\eb', "a\eb"]; + yield 'form feed' => ['a\fb', "a\fb"]; + yield 'dollar sign' => ['a\$b', 'a$b']; + yield 'double quote' => ['a\"b', 'a"b']; + yield 'backslash' => ['a\\\\b', 'a\\b']; + yield 'backslash before escape sequence' => ['a\\\\nb', 'a\nb']; + yield 'two backslashes' => ['a\\\\\\\\b', 'a\\\\b']; + yield 'hexadecimal' => ['\x41', 'A']; + yield 'uppercase hexadecimal prefix' => ['\X41', 'A']; + yield 'short hexadecimal' => ['\x9', "\x09"]; + yield 'null byte' => ['\x00', "\0"]; + yield 'unicode' => ['\u{48}', 'H']; + yield 'multibyte unicode' => ['\u{1F600}', "\u{1F600}"]; + yield 'unicode null byte' => ['a\u{0}b', "a\0b"]; + yield 'binary sequence before unicode' => ['\xFF\u{42}', "\xFF" . 'B']; + yield 'backslash before hexadecimal' => ['a\\\\x41b', 'a\x41b']; + yield 'octal' => ['\101', 'A']; + yield 'octal null byte' => ['\0', "\0"]; + yield 'octal overflow is truncated' => ['\777', "\xFF"]; + yield 'backslash before octal' => ['a\\\\101b', 'a\101b']; + yield 'code point above the unicode range' => ['\u{110000}', "\u{FFFD}"]; + yield 'huge code point' => ['\u{FFFFFFF}', "\u{FFFD}"]; + yield 'last unicode code point' => ['\u{10FFFF}', "\u{10FFFF}"]; + yield 'unknown sequence' => ['a\qb', 'a\qb']; + yield 'incomplete unicode sequence' => ['a\u{}b', 'a\u{}b']; + yield 'trailing backslash' => ['a\\', 'a\\']; + yield 'repeated sequences' => ['\x41\x41\u{42}\u{42}', 'AABB']; + yield 'several kinds at once' => ['\x41\u{42}\n\\\\', "AB\n" . '\\']; + } + + #[Test] + #[DataProvider('provideBodies')] + public function bodyIsDecoded(string $body, string $expected): void + { + self::assertSame($expected, StringParser::parse($body)); + } + + #[Test] + public function rawNullByteIsNotTouched(): void + { + self::assertSame("a\0b", StringParser::parse("a\0b")); + self::assertSame("a\0\\b", StringParser::parse("a\0" . '\\\\b')); + } + + #[Test] + public function invalidUtf8IsNotTouched(): void + { + self::assertSame("\xFF\xFE", StringParser::parse("\xFF\xFE")); + } + + #[Test] + public function decodingIsIdempotentForStringsWithoutBackslashes(): void + { + $decoded = StringParser::parse('a\nb'); + + self::assertSame($decoded, StringParser::parse($decoded)); + } +} diff --git a/libs/parser/tests/Internal/UtfCharRendererTest.php b/libs/parser/tests/Internal/UtfCharRendererTest.php new file mode 100644 index 0000000..5b0e962 --- /dev/null +++ b/libs/parser/tests/Internal/UtfCharRendererTest.php @@ -0,0 +1,83 @@ +, string}> + */ + public static function provideCodePoints(): iterable + { + yield 'null byte' => [0x00, "\0"]; + yield 'ascii' => [0x41, 'A']; + yield 'last one byte char' => [0x7F, "\x7F"]; + yield 'first two bytes char' => [0x80, "\u{80}"]; + yield 'last two bytes char' => [0x7FF, "\u{7FF}"]; + yield 'first three bytes char' => [0x800, "\u{800}"]; + yield 'last three bytes char' => [0xFFFF, "\u{FFFF}"]; + yield 'first four bytes char' => [0x10000, "\u{10000}"]; + yield 'emoji' => [0x1F600, "\u{1F600}"]; + yield 'last unicode code point' => [0x10FFFF, "\u{10FFFF}"]; + } + + #[Test] + #[DataProvider('provideCodePoints')] + public function codePointIsEncoded(int $code, string $expected): void + { + self::assertSame($expected, UtfCharRenderer::render($code)); + } + + /** + * @return iterable, int<1, 4>}> + */ + public static function provideCodePointLengths(): iterable + { + yield 'one byte' => [0x7F, 1]; + yield 'two bytes' => [0x7FF, 2]; + yield 'three bytes' => [0xFFFF, 3]; + yield 'four bytes' => [0x10FFFF, 4]; + } + + #[Test] + #[DataProvider('provideCodePointLengths')] + public function encodedCharHasExpectedLength(int $code, int $length): void + { + self::assertSame($length, \strlen(UtfCharRenderer::render($code))); + } + + /** + * @return iterable}> + */ + public static function provideOutOfRangeCodePoints(): iterable + { + yield 'first code point above the range' => [0x110000]; + yield 'huge code point' => [0xFFFFFFF]; + yield 'php int max' => [\PHP_INT_MAX]; + } + + #[Test] + #[DataProvider('provideOutOfRangeCodePoints')] + public function outOfRangeCodePointIsEncodedAsReplacementChar(int $code): void + { + self::assertSame("\u{FFFD}", UtfCharRenderer::render($code)); + } + + #[Test] + public function everyEncodedCharIsAValidUtf8Sequence(): void + { + foreach ([0x00, 0x41, 0x7F, 0x80, 0x7FF, 0x800, 0xFFFF, 0x10000, 0x10FFFF] as $code) { + self::assertTrue( + \mb_check_encoding(UtfCharRenderer::render($code), 'UTF-8'), + \sprintf('A code point 0x%X must be encoded as a valid utf-8 sequence', $code), + ); + } + } +} diff --git a/libs/parser/tests/Literal/IntLiteralParserTest.php b/libs/parser/tests/Literal/IntLiteralParserTest.php new file mode 100644 index 0000000..5a3cef2 --- /dev/null +++ b/libs/parser/tests/Literal/IntLiteralParserTest.php @@ -0,0 +1,75 @@ + + */ + public static function provideIntegers(): iterable + { + yield 'zero' => ['0', 0, '0']; + yield 'decimal' => ['42', 42, '42']; + yield 'negative decimal' => ['-42', -42, '-42']; + yield 'hexadecimal' => ['0x1F', 31, '31']; + yield 'uppercase hexadecimal prefix' => ['0X1F', 31, '31']; + yield 'negative hexadecimal' => ['-0x10', -16, '-16']; + yield 'binary' => ['0b1010', 10, '10']; + yield 'uppercase binary prefix' => ['0B1010', 10, '10']; + yield 'octal' => ['0o17', 15, '15']; + yield 'uppercase octal prefix' => ['0O17', 15, '15']; + yield 'legacy octal' => ['017', 15, '15']; + yield 'underscored' => ['1_000_000', 1000000, '1000000']; + yield 'underscored hexadecimal' => ['0xFF_FF', 65535, '65535']; + } + + #[Test] + #[DataProvider('provideIntegers')] + public function integerIsParsedToItsDecimalValue(string $literal, int $value, string $decimal): void + { + $node = IntLiteralParser::parse($literal); + + self::assertSame($value, $node->value()); + self::assertSame($decimal, $node->decimal); + } + + #[Test] + #[DataProvider('provideIntegers')] + public function integerParsingKeepsTheOriginalRepresentation(string $literal, int $value, string $decimal): void + { + self::assertSame($literal, IntLiteralParser::parse($literal)->raw()); + } + + #[Test] + public function integerParsingSupportsPhpIntMin(): void + { + $node = IntLiteralParser::parse((string) \PHP_INT_MIN); + + self::assertSame(\PHP_INT_MIN, $node->value()); + } + + #[Test] + public function integerParsingSupportsPhpIntMax(): void + { + $node = IntLiteralParser::parse((string) \PHP_INT_MAX); + + self::assertSame(\PHP_INT_MAX, $node->value()); + } + + #[Test] + public function negativeZeroIsParsedAsZero(): void + { + $node = IntLiteralParser::parse('-0'); + + self::assertSame(0, $node->value()); + } +} diff --git a/libs/parser/tests/Literal/StringLiteralParserTest.php b/libs/parser/tests/Literal/StringLiteralParserTest.php new file mode 100644 index 0000000..7c818e6 --- /dev/null +++ b/libs/parser/tests/Literal/StringLiteralParserTest.php @@ -0,0 +1,138 @@ + + */ + public static function provideDoubleQuotedStrings(): iterable + { + yield 'empty' => ['""', '']; + yield 'plain' => ['"example"', 'example']; + yield 'escaped quote' => ['"a\"b"', 'a"b']; + yield 'newline' => ['"a\nb"', "a\nb"]; + yield 'carriage return' => ['"a\rb"', "a\rb"]; + yield 'tab' => ['"a\tb"', "a\tb"]; + yield 'vertical tab' => ['"a\vb"', "a\vb"]; + yield 'escape' => ['"a\eb"', "a\eb"]; + yield 'form feed' => ['"a\fb"', "a\fb"]; + yield 'dollar sign' => ['"a\$b"', 'a$b']; + yield 'escaped backslash' => ['"a\\\\b"', 'a\\b']; + yield 'hexadecimal sequence' => ['"\x41"', 'A']; + yield 'uppercase hexadecimal sequence' => ['"\xFF"', "\xFF"]; + yield 'short hexadecimal sequence' => ['"\x9"', "\x09"]; + yield 'unicode sequence' => ['"\u{48}"', 'H']; + yield 'multibyte unicode sequence' => ['"\u{1F600}"', "\u{1F600}"]; + yield 'unknown escape is kept as is' => ['"a\qb"', 'a\qb']; + yield 'several sequences' => ['"\x41\u{42}\n"', "AB\n"]; + yield 'null byte hexadecimal sequence' => ['"\x00"', "\0"]; + yield 'null byte unicode sequence' => ['"a\u{0}b"', "a\0b"]; + yield 'binary sequence does not break the next one' => ['"\xFF\u{42}"', "\xFF" . 'B']; + yield 'raw null byte is kept as is' => ["\"a\0\\\\b\"", "a\0" . '\\' . 'b']; + yield 'escaped backslash before escaped quote' => ['"a\\\\\"b"', 'a\"b']; + yield 'escaped backslash before escape sequence' => ['"a\\\\nb"', 'a\nb']; + yield 'octal sequence' => ['"\101"', 'A']; + yield 'octal null byte' => ['"\0"', "\0"]; + yield 'octal overflow is truncated' => ['"\777"', "\xFF"]; + yield 'escaped backslash before octal sequence' => ['"a\\\\101b"', 'a\101b']; + yield 'code point above the unicode range' => ['"\u{110000}"', "\u{FFFD}"]; + } + + #[Test] + #[DataProvider('provideDoubleQuotedStrings')] + public function doubleQuotedStringIsDecoded(string $literal, string $value): void + { + $node = StringLiteralParser::parse($literal); + + self::assertSame($value, $node->value()); + self::assertSame($literal, $node->raw()); + } + + #[Test] + #[DataProvider('provideDoubleQuotedStrings')] + public function doubleQuotedFactoryIsTheSameAsParsing(string $literal, string $value): void + { + self::assertSame($value, StringLiteralParser::createFromDoubleQuotedString($literal)->value()); + } + + /** + * @return iterable + */ + public static function provideSingleQuotedStrings(): iterable + { + yield 'empty' => ["''", '']; + yield 'plain' => ["'example'", 'example']; + yield 'escaped quote' => ["'a\'b'", "a'b"]; + yield 'escaped backslash' => ["'a\\\\b'", 'a\\b']; + yield 'escape sequences are not decoded' => ["'a\\nb'", 'a\\nb']; + yield 'hexadecimal sequence is not decoded' => ["'a\\x41b'", 'a\\x41b']; + yield 'dollar sign is not decoded' => ["'a\$b'", 'a$b']; + } + + #[Test] + #[DataProvider('provideSingleQuotedStrings')] + public function singleQuotedStringIsDecoded(string $literal, string $value): void + { + $node = StringLiteralParser::parse($literal); + + self::assertSame($value, $node->value()); + self::assertSame($literal, $node->raw()); + } + + #[Test] + #[DataProvider('provideSingleQuotedStrings')] + public function singleQuotedFactoryIsTheSameAsParsing(string $literal, string $value): void + { + self::assertSame($value, StringLiteralParser::createFromSingleQuotedString($literal)->value()); + } + + #[Test] + public function derivedRawValueCanBeParsedBack(): void + { + $node = new StringLiteralNode('a"b'); + + self::assertSame($node->value(), StringLiteralParser::parse($node->raw())->value()); + } + + #[Test] + public function stringParsingThrowsOnEmptyLiteral(): void + { + $this->expectException(\InvalidArgumentException::class); + + StringLiteralParser::parse(''); + } + + #[Test] + public function stringParsingThrowsOnSingleCharacterLiteral(): void + { + $this->expectException(\InvalidArgumentException::class); + + StringLiteralParser::parse('"'); + } + + #[Test] + public function doubleQuotedFactoryThrowsOnTooShortLiteral(): void + { + $this->expectException(\InvalidArgumentException::class); + + StringLiteralParser::createFromDoubleQuotedString('"'); + } + + #[Test] + public function singleQuotedFactoryThrowsOnTooShortLiteral(): void + { + $this->expectException(\InvalidArgumentException::class); + + StringLiteralParser::createFromSingleQuotedString("'"); + } +} diff --git a/libs/parser/tests/TestCase.php b/libs/parser/tests/TestCase.php index c93689e..0de06aa 100644 --- a/libs/parser/tests/TestCase.php +++ b/libs/parser/tests/TestCase.php @@ -33,9 +33,7 @@ #[Group('unit'), Group('type-lang/parser')] abstract class TestCase extends BaseTestCase { - protected TypeParserInterface $parser { - get => $this->parser ??= new TypeParser(); - } + private ?TypeParserInterface $parser = null; /** * @param ParserOptionsType $options @@ -43,7 +41,7 @@ abstract class TestCase extends BaseTestCase protected function parser(array $options = []): TypeParserInterface { if ($options === []) { - return $this->parser; + return $this->parser ??= new TypeParser(); } return new TypeParser(new TypeParserFeatures(...$options)); diff --git a/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php b/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php index 187bdfa..1682cd1 100644 --- a/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php +++ b/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php @@ -20,9 +20,7 @@ final class PhpUseStatementsReaderTest extends TypeResolverTestCase { - private PhpUseStatementsReader $reader { - get => $this->reader ??= new PhpUseStatementsReader(); - } + private ?PhpUseStatementsReader $reader = null; /** * @param class-string $class @@ -31,7 +29,7 @@ final class PhpUseStatementsReaderTest extends TypeResolverTestCase */ private function read(string $class): array { - return $this->reader->getClassUseStatements(new \ReflectionClass($class)); + return ($this->reader ??= new PhpUseStatementsReader())->getClassUseStatements(new \ReflectionClass($class)); } public function testReadsPlainAndAliasedImports(): void From 8c8f789bf9e5eda4084dfcc29cf20b30fafb0eb9 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:00:15 +0300 Subject: [PATCH 008/119] Improve AST node names (improve BC) --- .../src/Callable/CallableParameterNode.php | 11 + libs/types/src/Identifier.php | 30 -- libs/types/src/Literal/LiteralNode.php | 21 +- .../src/Literal/LiteralNodeInterface.php | 17 +- libs/types/src/Name.php | 257 +++++++++--------- libs/types/src/Node.php | 4 +- libs/types/src/NodeInterface.php | 15 +- libs/types/src/NodeList.php | 28 +- libs/types/src/Shape/ClassConstFieldNode.php | 2 +- .../src/Shape/ClassConstMaskFieldNode.php | 2 +- libs/types/src/Shape/ConstMaskFieldNode.php | 2 +- libs/types/src/Shape/ExplicitFieldNode.php | 26 +- libs/types/src/Shape/FieldNode.php | 11 + libs/types/src/Shape/NamedFieldNode.php | 2 +- libs/types/src/Shape/NumericFieldNode.php | 2 +- libs/types/src/Shape/StringNamedFieldNode.php | 2 +- .../AttributeArgumentsListNodeTest.php | 8 +- .../Attribute/AttributeGroupNodeTest.php | 8 +- .../Attribute/AttributeGroupsListNodeTest.php | 8 +- .../CallableParametersListNodeTest.php | 8 +- libs/types/tests/IdentifierTest.php | 72 +---- libs/types/tests/Literal/LiteralNodeTest.php | 32 +-- libs/types/tests/LogicalTypeNodeTest.php | 4 +- libs/types/tests/NameTest.php | 152 ++--------- libs/types/tests/NodeListTest.php | 47 +--- libs/types/tests/NodeTest.php | 6 +- .../tests/Shape/ClassConstFieldNodeTest.php | 4 +- .../Shape/ClassConstMaskFieldNodeTest.php | 4 +- .../tests/Shape/ConstMaskFieldNodeTest.php | 4 +- .../tests/Shape/ExplicitFieldNodeTest.php | 47 +--- libs/types/tests/Shape/FieldsListNodeTest.php | 8 +- libs/types/tests/Shape/NamedFieldNodeTest.php | 2 +- .../tests/Shape/NumericFieldNodeTest.php | 4 +- .../tests/Shape/StringNamedFieldNodeTest.php | 2 +- .../TemplateArgumentsListNodeTest.php | 8 +- 35 files changed, 268 insertions(+), 592 deletions(-) diff --git a/libs/types/src/Callable/CallableParameterNode.php b/libs/types/src/Callable/CallableParameterNode.php index aa1e23a..f5c69c2 100644 --- a/libs/types/src/Callable/CallableParameterNode.php +++ b/libs/types/src/Callable/CallableParameterNode.php @@ -28,6 +28,17 @@ public function __construct( )); } + /** + * Returns {@see true} in case of the parameter is an instance of the + * passed class. + * + * @param class-string $class + */ + public function is(string $class): bool + { + return $this instanceof $class; + } + public function __toString(): string { $result = []; diff --git a/libs/types/src/Identifier.php b/libs/types/src/Identifier.php index 9b39ba2..3a01c58 100644 --- a/libs/types/src/Identifier.php +++ b/libs/types/src/Identifier.php @@ -6,22 +6,9 @@ /** * @phpstan-consistent-constructor - * - * @property-read bool $isVirtual An alias of {@see isVirtual()} method. - * @property-read bool $isSpecial An alias of {@see isSpecial()} method. - * @property-read bool $isBuiltin An alias of {@see isBuiltin()} method. */ final class Identifier extends Node implements \Stringable { - /** - * @var list - */ - private const VIRTUAL_PROPERTIES = [ - 'isVirtual', - 'isSpecial', - 'isBuiltin', - ]; - /** * @var list */ @@ -121,23 +108,6 @@ public function isBuiltin(): bool return self::isLooksLikeBuiltin($this->value); } - public function __get(string $name): mixed - { - return match ($name) { - 'isVirtual' => $this->isVirtual(), - 'isSpecial' => $this->isSpecial(), - 'isBuiltin' => $this->isBuiltin(), - default => throw new \OutOfRangeException( - message: \sprintf('Undefined property %s::$%s', self::class, $name), - ), - }; - } - - public function __isset(string $name): bool - { - return \in_array($name, self::VIRTUAL_PROPERTIES, true); - } - /** * Returns name as string. * diff --git a/libs/types/src/Literal/LiteralNode.php b/libs/types/src/Literal/LiteralNode.php index e83b230..60f8ecd 100644 --- a/libs/types/src/Literal/LiteralNode.php +++ b/libs/types/src/Literal/LiteralNode.php @@ -10,35 +10,20 @@ * @template TValue of mixed = mixed * * @template-implements LiteralNodeInterface + * + * @property-read TValue $value Gets a PHP representation of the literal value. + * @property-read string $raw Gets the original literal value specified in the token. */ abstract class LiteralNode extends TypeNode implements LiteralNodeInterface { public function __construct( /** - * An alias of {@see value()} method. - * * @var TValue */ public readonly mixed $value, - /** - * An alias of {@see raw()} method. - */ public readonly string $raw, ) {} - /** - * @return TValue - */ - public function value(): mixed - { - return $this->value; - } - - public function raw(): string - { - return $this->raw; - } - public function __toString(): string { return $this->raw; diff --git a/libs/types/src/Literal/LiteralNodeInterface.php b/libs/types/src/Literal/LiteralNodeInterface.php index 40bc5f1..fc68feb 100644 --- a/libs/types/src/Literal/LiteralNodeInterface.php +++ b/libs/types/src/Literal/LiteralNodeInterface.php @@ -6,23 +6,14 @@ /** * @template-covariant TValue of mixed = mixed + * + * @property-read TValue $value Gets a PHP representation of the literal value. + * @property-read string $raw Gets the original literal value specified in the token. */ interface LiteralNodeInterface extends \Stringable { /** - * Gets a PHP representation of the literal value. - * - * @return TValue - */ - public function value(): mixed; - - /** - * Gets the original literal value specified in the token. - */ - public function raw(): string; - - /** - * Returns the processed ({@see value()}) literal value as a string. + * Returns the processed ({@see $value}) literal value as a string. */ public function __toString(): string; } diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index c26e340..c9a444e 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -8,26 +8,9 @@ * @phpstan-consistent-constructor * * @template-implements \IteratorAggregate - * - * @property-read Identifier $first An alias of {@see first()} method. - * @property-read Identifier $last An alias of {@see last()} method. - * @property-read bool $isSimple An alias of {@see isSimple()} method. - * @property-read bool $isSpecial An alias of {@see isSpecial()} method. - * @property-read bool $isBuiltin An alias of {@see isBuiltin()} method. */ final class Name extends Node implements \IteratorAggregate, \Countable, \Stringable { - /** - * @var list - */ - private const VIRTUAL_PROPERTIES = [ - 'first', - 'last', - 'isSimple', - 'isSpecial', - 'isBuiltin', - ]; - /** * @var non-empty-string */ @@ -38,66 +21,117 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String /** * @var non-empty-list */ - public array $segments; + public readonly array $parts; /** - * @param iterable $segments + * @param iterable $parts */ public function __construct( - iterable $segments, + iterable $parts, public readonly bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, ) { - $segments = match (true) { - $segments instanceof \Traversable => \iterator_to_array($segments, false), - \array_is_list($segments) => $segments, - default => \array_values($segments), + $parts = match (true) { + $parts instanceof \Traversable => \iterator_to_array($parts, false), + \array_is_list($parts) => $parts, + default => \array_values($parts), }; - \assert($segments !== [], new \InvalidArgumentException('Name segments count can not be empty')); + \assert($parts !== [], new \InvalidArgumentException('Name parts count can not be empty')); + + $this->parts = $parts; + } + + public static function createFromString(string|\Stringable $name): self + { + $name = (string) $name; + $parts = []; + + foreach (\explode(self::NAMESPACE_DELIMITER, $name) as $segment) { + if ($segment === '') { + continue; + } + + $parts[] = Identifier::createFromString($segment); + } - $this->segments = $segments; + return new self($parts, \str_starts_with($name, self::NAMESPACE_DELIMITER)); } /** - * @param iterable $segments + * Gets the first segment of a name */ - public static function createFromStringSegments( - iterable $segments, - bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, - ): self { - $identifiers = []; + public function getFirstPart(): Identifier + { + return $this->parts[0]; + } - foreach ($segments as $segment) { - $identifiers[] = Identifier::createFromString($segment); - } + /** + * Gets the first segment of a name as a string + * + * @return non-empty-string + */ + public function getFirstPartAsString(): string + { + $identifier = $this->getFirstPart(); - return new self($identifiers, $isFullyQualified); + return $identifier->toString(); } /** - * @param non-empty-string|\Stringable $name + * Gets the last segment of a name */ - public static function createFromString(string|\Stringable $name): self + public function getLastPart(): Identifier { - $name = (string) $name; - $unqualified = \trim($name, self::NAMESPACE_DELIMITER); + return $this->parts[\count($this->parts) - 1]; + } + + /** + * Gets the last segment of a name as a string + * + * @return non-empty-string + */ + public function getLastPartAsString(): string + { + $identifier = $this->getLastPart(); - $segments = []; + return $identifier->toString(); + } - foreach (\explode(self::NAMESPACE_DELIMITER, $unqualified) as $segment) { - if ($segment === '') { - continue; - } + /** + * Gets whether the name is simple. + */ + public function isSimple(): bool + { + return \count($this->parts) === 1; + } - $segments[] = $segment; - } + /** + * Gets {@see true} in case of name contains special class reference. + */ + public function isSpecial(): bool + { + return $this->isSimple() && $this->getFirstPart()->isSpecial(); + } - return self::createFromStringSegments( - segments: $segments, - isFullyQualified: \str_starts_with($name, self::NAMESPACE_DELIMITER), - ); + /** + * Gets {@see true} in case of name contains builtin type name. + */ + public function isBuiltin(): bool + { + return $this->isSimple() && $this->getFirstPart()->isBuiltin(); } + /** + * Gets {@see true} in case of name is fully qualified. + * + * @deprecated Use the {@see $isFullyQualified} property instead. + */ + public function isFullQualified(): bool + { + return $this->isFullyQualified; + } + + /** * @param int<0, max> $offset * @param int<0, max>|null $length @@ -105,7 +139,7 @@ public static function createFromString(string|\Stringable $name): self public function slice(int $offset = 0, ?int $length = null): self { return new self( - segments: \array_slice($this->segments, $offset, $length), + parts: \array_slice($this->parts, $offset, $length), isFullyQualified: $this->isFullyQualified, ); } @@ -126,8 +160,8 @@ public function slice(int $offset = 0, ?int $length = null): self public function withAdded(self $name): self { return new self([ - ...$this->segments, - ...$name->segments, + ...$this->parts, + ...$name->parts, ], $this->isFullyQualified); } @@ -169,8 +203,8 @@ public function withAdded(self $name): self public function mergeWith(self $name): self { return new self([ - ...$this->segments, - ...\array_slice($name->segments, 1), + ...$this->parts, + ...\array_slice($name->parts, 1), ], $this->isFullyQualified); } @@ -183,7 +217,7 @@ public function toFullQualified(): self return clone $this; } - return new self($this->segments, true); + return new self($this->parts, true); } /** @@ -192,20 +226,38 @@ public function toFullQualified(): self public function toUnqualified(): self { if ($this->isFullyQualified) { - return new self($this->segments, false); + return new self($this->parts, false); } return clone $this; } + /** + * @return non-empty-list + */ + public function toArray(): array + { + return $this->parts; + } + + /** + * @deprecated Use the {@see $parts} property instead. + * + * @return non-empty-list + */ + public function getParts(): array + { + return $this->parts; + } + /** * @return non-empty-list */ - public function toStringArray(): array + public function toArrayStrings(): array { $result = []; - foreach ($this->segments as $identifier) { + foreach ($this->parts as $identifier) { $result[] = $identifier->toString(); } @@ -213,13 +265,23 @@ public function toStringArray(): array } /** + * @deprecated Use the {@see toArrayStrings()} method instead. + * * @return non-empty-list */ - public function toLowercaseStringArray(): array + public function getPartsAsString(): array + { + return $this->toArrayStrings(); + } + + /** + * @return non-empty-list + */ + public function toArrayLowercaseStrings(): array { $result = []; - foreach ($this->segments as $identifier) { + foreach ($this->parts as $identifier) { $result[] = $identifier->toLowerString(); } @@ -247,7 +309,7 @@ public function toString(): string */ public function toUnqualifiedString(): string { - return \implode(self::NAMESPACE_DELIMITER, $this->toStringArray()); + return \implode(self::NAMESPACE_DELIMITER, $this->toArrayStrings()); } /** @@ -258,7 +320,7 @@ public function toUnqualifiedString(): string public function toFullQualifiedString(): string { return self::NAMESPACE_DELIMITER - . \implode(self::NAMESPACE_DELIMITER, $this->toStringArray()); + . \implode(self::NAMESPACE_DELIMITER, $this->toArrayStrings()); } /** @@ -296,7 +358,7 @@ public function toFullQualifiedLowerString(): string */ public function getIterator(): \Traversable { - return new \ArrayIterator($this->segments); + return new \ArrayIterator($this->parts); } /** @@ -304,66 +366,7 @@ public function getIterator(): \Traversable */ public function count(): int { - return \count($this->segments); - } - - /** - * Gets the first segment of a name - */ - public function first(): Identifier - { - return $this->segments[0]; - } - - /** - * Gets the last segment of a name - */ - public function last(): Identifier - { - return $this->segments[\count($this->segments) - 1]; - } - - /** - * Gets whether the name is simple. - */ - public function isSimple(): bool - { - return \count($this->segments) === 1; - } - - /** - * Gets {@see true} in case of name contains special class reference. - */ - public function isSpecial(): bool - { - return $this->isSimple() && $this->first()->isSpecial(); - } - - /** - * Gets {@see true} in case of name contains builtin type name. - */ - public function isBuiltin(): bool - { - return $this->isSimple() && $this->first()->isBuiltin(); - } - - public function __get(string $name): mixed - { - return match ($name) { - 'first' => $this->first(), - 'last' => $this->last(), - 'isSimple' => $this->isSimple(), - 'isSpecial' => $this->isSpecial(), - 'isBuiltin' => $this->isBuiltin(), - default => throw new \OutOfRangeException( - message: \sprintf('Undefined property %s::$%s', self::class, $name), - ), - }; - } - - public function __isset(string $name): bool - { - return \in_array($name, self::VIRTUAL_PROPERTIES, true); + return \count($this->parts); } /** @@ -379,7 +382,7 @@ public function __toString(): string */ public function __serialize(): array { - return [$this->segments, $this->offset, $this->isFullyQualified]; + return [$this->parts, $this->offset, $this->isFullyQualified]; } /** @@ -388,7 +391,7 @@ public function __serialize(): array */ public function __unserialize(array $data): void { - $this->segments = $data[0] ?? throw new \UnexpectedValueException( + $this->parts = $data[0] ?? throw new \UnexpectedValueException( message: 'Unable to unserialize Name segments', ); diff --git a/libs/types/src/Node.php b/libs/types/src/Node.php index 98e371c..34e3e03 100644 --- a/libs/types/src/Node.php +++ b/libs/types/src/Node.php @@ -7,13 +7,13 @@ abstract class Node implements NodeInterface { /** - * An alias of {@see offset()} method. + * Token offset defined in the source code. * * @var int<0, max> */ public int $offset = 0; - public function offset(): int + public function getOffset(): int { return $this->offset; } diff --git a/libs/types/src/NodeInterface.php b/libs/types/src/NodeInterface.php index fc2d056..84b8b49 100644 --- a/libs/types/src/NodeInterface.php +++ b/libs/types/src/NodeInterface.php @@ -9,20 +9,7 @@ interface NodeInterface /** * Gets token offset defined in the source code. * - * It is recommended to use the `phplrt/position` package to determine - * the line and column from this information: - * - * ```php - * $position = Phplrt\Position\Position::fromOffset( - * source: \file_get_contents($filename), - * offset: $node->offset(), - * ); - * - * echo 'line: ' . $position->getLine() . "\n" - * 'column: ' . $position->getColumn(); - * ``` - * * @return int<0, max> */ - public function offset(): int; + public function getOffset(): int; } diff --git a/libs/types/src/NodeList.php b/libs/types/src/NodeList.php index 049fcbb..36bd600 100644 --- a/libs/types/src/NodeList.php +++ b/libs/types/src/NodeList.php @@ -9,23 +9,12 @@ * * @template-implements \IteratorAggregate * @template-implements \ArrayAccess, TNode> - * - * @property-read TNode|null $first An alias of {@see first()} method. - * @property-read TNode|null $last An alias of {@see last()} method. */ abstract class NodeList extends Node implements \IteratorAggregate, \ArrayAccess, \Countable { - /** - * @var list - */ - private const VIRTUAL_PROPERTIES = [ - 'first', - 'last', - ]; - /** * @var list */ @@ -110,6 +99,7 @@ public function offsetUnset(mixed $offset): void throw new \InvalidArgumentException('A NodeList index must be an int<0, max>|null'); } + // @phpstan-ignore-next-line unset($this->items[$offset]); if (!\array_is_list($this->items)) { @@ -155,20 +145,4 @@ public function last(): ?Node return $this->items[$key]; } - - public function __get(string $name): mixed - { - return match ($name) { - 'first' => $this->first(), - 'last' => $this->last(), - default => throw new \OutOfRangeException( - message: \sprintf('Undefined property %s::$%s', static::class, $name), - ), - }; - } - - public function __isset(string $name): bool - { - return \in_array($name, self::VIRTUAL_PROPERTIES, true); - } } diff --git a/libs/types/src/Shape/ClassConstFieldNode.php b/libs/types/src/Shape/ClassConstFieldNode.php index 0151c65..d418c16 100644 --- a/libs/types/src/Shape/ClassConstFieldNode.php +++ b/libs/types/src/Shape/ClassConstFieldNode.php @@ -27,7 +27,7 @@ public function __construct( ); } - public function index(): string + public function getIndex(): string { return \vsprintf('%s::%s', [ $this->key->class->toString(), diff --git a/libs/types/src/Shape/ClassConstMaskFieldNode.php b/libs/types/src/Shape/ClassConstMaskFieldNode.php index 0c50a73..e1fc75f 100644 --- a/libs/types/src/Shape/ClassConstMaskFieldNode.php +++ b/libs/types/src/Shape/ClassConstMaskFieldNode.php @@ -27,7 +27,7 @@ public function __construct( ); } - public function index(): string + public function getIndex(): string { return \vsprintf('%s::%s*', [ $this->key->class->toString(), diff --git a/libs/types/src/Shape/ConstMaskFieldNode.php b/libs/types/src/Shape/ConstMaskFieldNode.php index b2d4847..6136a2b 100644 --- a/libs/types/src/Shape/ConstMaskFieldNode.php +++ b/libs/types/src/Shape/ConstMaskFieldNode.php @@ -27,7 +27,7 @@ public function __construct( ); } - public function index(): string + public function getIndex(): string { return (string) $this->key; } diff --git a/libs/types/src/Shape/ExplicitFieldNode.php b/libs/types/src/Shape/ExplicitFieldNode.php index fbd933a..8bd7eae 100644 --- a/libs/types/src/Shape/ExplicitFieldNode.php +++ b/libs/types/src/Shape/ExplicitFieldNode.php @@ -9,18 +9,9 @@ /** * @template TKey of mixed - * - * @property-read string $index An alias of {@see index()} method. */ abstract class ExplicitFieldNode extends FieldNode { - /** - * @var list - */ - private const VIRTUAL_PROPERTIES = [ - 'index', - ]; - public function __construct( /** * @var TKey @@ -40,20 +31,5 @@ public function __construct( /** * Gets a pretty-printed string representation of the key */ - abstract public function index(): string; - - public function __get(string $name): mixed - { - return match ($name) { - 'index' => $this->index(), - default => throw new \OutOfRangeException( - message: \sprintf('Undefined property %s::$%s', static::class, $name), - ), - }; - } - - public function __isset(string $name): bool - { - return \in_array($name, self::VIRTUAL_PROPERTIES, true); - } + abstract public function getIndex(): string; } diff --git a/libs/types/src/Shape/FieldNode.php b/libs/types/src/Shape/FieldNode.php index 6165712..ce177da 100644 --- a/libs/types/src/Shape/FieldNode.php +++ b/libs/types/src/Shape/FieldNode.php @@ -16,6 +16,17 @@ public function __construct( public ?AttributeGroupListNode $attributes = null, ) {} + /** + * Returns {@see true} in case of the field is an instance of the + * passed class. + * + * @param class-string $class + */ + public function is(string $class): bool + { + return $this instanceof $class; + } + public function __toString(): string { return $this->isOptional ? 'optional' : 'required'; diff --git a/libs/types/src/Shape/NamedFieldNode.php b/libs/types/src/Shape/NamedFieldNode.php index d13896c..3fe63ca 100644 --- a/libs/types/src/Shape/NamedFieldNode.php +++ b/libs/types/src/Shape/NamedFieldNode.php @@ -27,7 +27,7 @@ public function __construct( ); } - public function index(): string + public function getIndex(): string { return $this->key->toString(); } diff --git a/libs/types/src/Shape/NumericFieldNode.php b/libs/types/src/Shape/NumericFieldNode.php index 410c94c..351cc0f 100644 --- a/libs/types/src/Shape/NumericFieldNode.php +++ b/libs/types/src/Shape/NumericFieldNode.php @@ -27,7 +27,7 @@ public function __construct( ); } - public function index(): string + public function getIndex(): string { return (string) $this->key->value; } diff --git a/libs/types/src/Shape/StringNamedFieldNode.php b/libs/types/src/Shape/StringNamedFieldNode.php index 1f03dbc..6362e2b 100644 --- a/libs/types/src/Shape/StringNamedFieldNode.php +++ b/libs/types/src/Shape/StringNamedFieldNode.php @@ -27,7 +27,7 @@ public function __construct( ); } - public function index(): string + public function getIndex(): string { return $this->key->value; } diff --git a/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php b/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php index 6d6c4fd..233f5eb 100644 --- a/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php +++ b/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php @@ -24,8 +24,8 @@ public function emptyListByDefault(): void $list = new AttributeArgumentListNode(); self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -45,8 +45,8 @@ public function firstAndLastProperties(): void $b = $this->makeArg('int'); $list = new AttributeArgumentListNode([$a, $b]); - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); + self::assertSame($a, $list->first()); + self::assertSame($b, $list->last()); } #[Test] diff --git a/libs/types/tests/Attribute/AttributeGroupNodeTest.php b/libs/types/tests/Attribute/AttributeGroupNodeTest.php index ae1d731..29b28fa 100644 --- a/libs/types/tests/Attribute/AttributeGroupNodeTest.php +++ b/libs/types/tests/Attribute/AttributeGroupNodeTest.php @@ -23,8 +23,8 @@ public function emptyGroupByDefault(): void $group = new AttributeGroupNode(); self::assertCount(0, $group); - self::assertNull($group->first); - self::assertNull($group->last); + self::assertNull($group->first()); + self::assertNull($group->last()); } #[Test] @@ -35,8 +35,8 @@ public function constructorAcceptsAttributes(): void $group = new AttributeGroupNode([$a, $b]); self::assertCount(2, $group); - self::assertSame($a, $group->first); - self::assertSame($b, $group->last); + self::assertSame($a, $group->first()); + self::assertSame($b, $group->last()); } #[Test] diff --git a/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php b/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php index 40ae29c..9ae6b1b 100644 --- a/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php +++ b/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php @@ -29,8 +29,8 @@ public function emptyListByDefault(): void $list = new AttributeGroupListNode(); self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -41,8 +41,8 @@ public function constructorAcceptsGroups(): void $list = new AttributeGroupListNode([$g1, $g2]); self::assertCount(2, $list); - self::assertSame($g1, $list->first); - self::assertSame($g2, $list->last); + self::assertSame($g1, $list->first()); + self::assertSame($g2, $list->last()); } #[Test] diff --git a/libs/types/tests/Callable/CallableParametersListNodeTest.php b/libs/types/tests/Callable/CallableParametersListNodeTest.php index 06559e6..7eb1a13 100644 --- a/libs/types/tests/Callable/CallableParametersListNodeTest.php +++ b/libs/types/tests/Callable/CallableParametersListNodeTest.php @@ -24,8 +24,8 @@ public function emptyListByDefault(): void $list = new CallableParameterListNode(); self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -36,8 +36,8 @@ public function constructorAcceptsParameters(): void $list = new CallableParameterListNode([$a, $b]); self::assertCount(2, $list); - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); + self::assertSame($a, $list->first()); + self::assertSame($b, $list->last()); } #[Test] diff --git a/libs/types/tests/IdentifierTest.php b/libs/types/tests/IdentifierTest.php index 766c7ea..dad1a1a 100644 --- a/libs/types/tests/IdentifierTest.php +++ b/libs/types/tests/IdentifierTest.php @@ -48,7 +48,7 @@ public function isVirtualIsTrueForHyphenatedName(): void { $id = new Identifier('non-empty-string'); - self::assertTrue($id->isVirtual); + self::assertTrue($id->isVirtual()); } #[Test] @@ -56,7 +56,7 @@ public function isVirtualIsFalseForNormalName(): void { $id = new Identifier('string'); - self::assertFalse($id->isVirtual); + self::assertFalse($id->isVirtual()); } #[Test] @@ -65,7 +65,7 @@ public function isSpecialIsTrueForSpecialClassNames(string $name): void { $id = new Identifier($name); - self::assertTrue($id->isSpecial); + self::assertTrue($id->isSpecial()); } public static function provideSpecialNames(): iterable @@ -78,7 +78,7 @@ public function isSpecialIsFalseForRegularName(): void { $id = new Identifier('MyClass'); - self::assertFalse($id->isSpecial); + self::assertFalse($id->isSpecial()); } #[Test] @@ -87,7 +87,7 @@ public function isBuiltinIsTrueForBuiltinTypes(string $name): void { $id = new Identifier($name); - self::assertTrue($id->isBuiltin); + self::assertTrue($id->isBuiltin()); } public static function provideBuiltinNames(): iterable @@ -105,7 +105,7 @@ public function isBuiltinIsFalseForCustomName(): void { $id = new Identifier('MyClass'); - self::assertFalse($id->isBuiltin); + self::assertFalse($id->isBuiltin()); } #[Test] @@ -193,62 +193,6 @@ public function serializeAndUnserializeRoundtrip(): void self::assertSame(42, $restored->offset); } - #[Test] - public function isVirtualMethodIsAnAliasOfProperty(): void - { - $id = new Identifier('non-empty-string'); - - self::assertTrue($id->isVirtual()); - self::assertSame($id->isVirtual(), $id->isVirtual); - } - - #[Test] - public function isSpecialMethodIsAnAliasOfProperty(): void - { - $id = new Identifier('self'); - - self::assertTrue($id->isSpecial()); - self::assertSame($id->isSpecial(), $id->isSpecial); - } - - #[Test] - public function isBuiltinMethodIsAnAliasOfProperty(): void - { - $id = new Identifier('int'); - - self::assertTrue($id->isBuiltin()); - self::assertSame($id->isBuiltin(), $id->isBuiltin); - } - - #[Test] - public function issetReturnsTrueForVirtualProperties(): void - { - $id = new Identifier('Example'); - - self::assertTrue(isset($id->isVirtual)); - self::assertTrue(isset($id->isSpecial)); - self::assertTrue(isset($id->isBuiltin)); - } - - #[Test] - public function issetReturnsFalseForUnknownProperty(): void - { - $id = new Identifier('Example'); - - self::assertFalse(isset($id->unknown)); - } - - #[Test] - public function readingUnknownPropertyThrows(): void - { - $id = new Identifier('Example'); - - $this->expectException(\OutOfRangeException::class); - - /** @phpstan-ignore-next-line */ - $id->unknown; - } - #[Test] public function specialNamesAreCaseInsensitive(): void { @@ -337,7 +281,7 @@ public function offsetMethodIsAnAliasOfProperty(): void $id = new Identifier('Example'); $id->offset = 17; - self::assertSame(17, $id->offset()); + self::assertSame(17, $id->offset); } #[Test] @@ -358,7 +302,7 @@ public function serializationRoundtripPreservesOffset(): void /** @var Identifier $restored */ $restored = \unserialize(\serialize($id)); - self::assertSame(5, $restored->offset()); + self::assertSame(5, $restored->offset); self::assertSame('Example', $restored->value); } diff --git a/libs/types/tests/Literal/LiteralNodeTest.php b/libs/types/tests/Literal/LiteralNodeTest.php index cbf0184..fda8533 100644 --- a/libs/types/tests/Literal/LiteralNodeTest.php +++ b/libs/types/tests/Literal/LiteralNodeTest.php @@ -38,7 +38,7 @@ public static function provideLiterals(): iterable public function valueMethodIsAnAliasOfValueProperty(LiteralNode $node, mixed $value, string $raw): void { self::assertSame($value, $node->value); - self::assertSame($value, $node->value()); + self::assertSame($value, $node->value); } #[Test] @@ -46,7 +46,7 @@ public function valueMethodIsAnAliasOfValueProperty(LiteralNode $node, mixed $va public function rawMethodIsAnAliasOfRawProperty(LiteralNode $node, mixed $value, string $raw): void { self::assertSame($raw, $node->raw); - self::assertSame($raw, $node->raw()); + self::assertSame($raw, $node->raw); } #[Test] @@ -68,21 +68,21 @@ public function everyLiteralImplementsItsContracts(LiteralNode $node, mixed $val #[Test] public function boolLiteralDerivesRawFromValue(): void { - self::assertSame('true', (new BoolLiteralNode(true))->raw()); - self::assertSame('false', (new BoolLiteralNode(false))->raw()); + self::assertSame('true', (new BoolLiteralNode(true))->raw); + self::assertSame('false', (new BoolLiteralNode(false))->raw); } #[Test] public function nullLiteralDerivesRawFromValue(): void { - self::assertSame('null', (new NullLiteralNode())->raw()); - self::assertNull((new NullLiteralNode())->value()); + self::assertSame('null', (new NullLiteralNode())->raw); + self::assertNull((new NullLiteralNode())->value); } #[Test] public function stringLiteralDerivesRawFromValue(): void { - self::assertSame('"example"', (new StringLiteralNode('example'))->raw()); + self::assertSame('"example"', (new StringLiteralNode('example'))->raw); } #[Test] @@ -90,8 +90,8 @@ public function intLiteralDerivesRawFromValue(): void { $node = new IntLiteralNode(42); - self::assertSame(42, $node->value()); - self::assertSame('42', $node->raw()); + self::assertSame(42, $node->value); + self::assertSame('42', $node->raw); self::assertSame('42', $node->decimal); } @@ -100,7 +100,7 @@ public function negativeIntLiteralDerivesRawFromValue(): void { $node = new IntLiteralNode(-42); - self::assertSame('-42', $node->raw()); + self::assertSame('-42', $node->raw); self::assertSame('-42', $node->decimal); } @@ -109,8 +109,8 @@ public function floatLiteralDerivesRawFromValue(): void { $node = new FloatLiteralNode(0.5); - self::assertSame(0.5, $node->value()); - self::assertSame('0.5', $node->raw()); + self::assertSame(0.5, $node->value); + self::assertSame('0.5', $node->raw); } #[Test] @@ -118,8 +118,8 @@ public function variableLiteralValueDoesNotContainDollarSign(): void { $node = new VariableLiteralNode('example'); - self::assertSame('example', $node->value()); - self::assertSame('$example', $node->raw()); + self::assertSame('example', $node->value); + self::assertSame('$example', $node->raw); } #[Test] @@ -128,7 +128,7 @@ public function literalValueAndRawArePartOfTheNodeState(): void $node = new StringLiteralNode('example'); $node->offset = 5; - self::assertSame(5, $node->offset()); - self::assertSame('example', $node->value()); + self::assertSame(5, $node->offset); + self::assertSame('example', $node->value); } } diff --git a/libs/types/tests/LogicalTypeNodeTest.php b/libs/types/tests/LogicalTypeNodeTest.php index 6f1c365..b3ccffd 100644 --- a/libs/types/tests/LogicalTypeNodeTest.php +++ b/libs/types/tests/LogicalTypeNodeTest.php @@ -145,7 +145,7 @@ public function serializationRoundtripPreservesStatementsAndOffset(string $class $restored = \unserialize(\serialize($node)); self::assertInstanceOf($class, $restored); - self::assertSame(13, $restored->offset()); + self::assertSame(13, $restored->offset); self::assertCount(2, $restored); self::assertSame('A', $restored->statements[0]->name->toString()); self::assertSame('B', $restored->statements[1]->name->toString()); @@ -189,6 +189,6 @@ public function defaultOffsetIsZero(string $class): void { $node = new $class($this->type('A'), $this->type('B')); - self::assertSame(0, $node->offset()); + self::assertSame(0, $node->offset); } } diff --git a/libs/types/tests/NameTest.php b/libs/types/tests/NameTest.php index 575d8b3..f51f5ea 100644 --- a/libs/types/tests/NameTest.php +++ b/libs/types/tests/NameTest.php @@ -54,7 +54,7 @@ public function firstPropertyReturnsFirstSegment(): void { $name = new Name([$this->id('Foo'), $this->id('Bar')]); - self::assertSame('Foo', $name->first->value); + self::assertSame('Foo', $name->getFirstPart()->value); } #[Test] @@ -62,7 +62,7 @@ public function lastPropertyReturnsLastSegment(): void { $name = new Name([$this->id('Foo'), $this->id('Bar')]); - self::assertSame('Bar', $name->last->value); + self::assertSame('Bar', $name->getLastPart()->value); } #[Test] @@ -70,7 +70,7 @@ public function isSimpleIsTrueForSingleSegment(): void { $name = new Name([$this->id('Foo')]); - self::assertTrue($name->isSimple); + self::assertTrue($name->isSimple()); } #[Test] @@ -78,7 +78,7 @@ public function isSimpleIsFalseForMultipleSegments(): void { $name = new Name([$this->id('Foo'), $this->id('Bar')]); - self::assertFalse($name->isSimple); + self::assertFalse($name->isSimple()); } #[Test] @@ -86,7 +86,7 @@ public function isSpecialIsTrueForSpecialSingleSegment(): void { $name = new Name([$this->id('self')]); - self::assertTrue($name->isSpecial); + self::assertTrue($name->isSpecial()); } #[Test] @@ -94,7 +94,7 @@ public function isSpecialIsFalseForMultiSegmentNameStartingWithSpecial(): void { $name = new Name([$this->id('self'), $this->id('Foo')]); - self::assertFalse($name->isSpecial); + self::assertFalse($name->isSpecial()); } #[Test] @@ -102,7 +102,7 @@ public function isBuiltinIsTrueForBuiltinSingleSegment(): void { $name = new Name([$this->id('int')]); - self::assertTrue($name->isBuiltin); + self::assertTrue($name->isBuiltin()); } #[Test] @@ -110,7 +110,7 @@ public function isBuiltinIsFalseForMultiSegmentName(): void { $name = new Name([$this->id('int'), $this->id('Foo')]); - self::assertFalse($name->isBuiltin); + self::assertFalse($name->isBuiltin()); } #[Test] @@ -140,14 +140,6 @@ public function createFromStringDetectsFullyQualified(): void self::assertSame('\Foo\Bar', $name->toString()); } - #[Test] - public function createFromStringSegments(): void - { - $name = Name::createFromStringSegments(['Foo', 'Bar']); - - self::assertSame('Foo\Bar', $name->toString()); - } - #[Test] public function sliceReturnsSubName(): void { @@ -222,7 +214,7 @@ public function toStringArrayReturnsSegmentStrings(): void { $name = Name::createFromString('A\B\C'); - self::assertSame(['A', 'B', 'C'], $name->toStringArray()); + self::assertSame(['A', 'B', 'C'], $name->toArrayStrings()); } #[Test] @@ -230,7 +222,7 @@ public function toLowerStringArrayReturnsLowercasedSegments(): void { $name = Name::createFromString('Foo\Bar'); - self::assertSame(['foo', 'bar'], $name->toLowercaseStringArray()); + self::assertSame(['foo', 'bar'], $name->toArrayLowercaseStrings()); } #[Test] @@ -286,7 +278,7 @@ public function serializeAndUnserializeRoundtrip(): void $restored = \unserialize(\serialize($name)); self::assertInstanceOf(Name::class, $restored); - self::assertSame(['Foo', 'Bar'], $restored->toStringArray()); + self::assertSame(['Foo', 'Bar'], $restored->toArrayStrings()); self::assertSame(10, $restored->offset); } @@ -300,95 +292,6 @@ public function constructorThrowsOnEmptySegmentsArray(): void new Name([]); } - #[Test] - public function firstMethodIsAnAliasOfProperty(): void - { - $name = new Name([$this->id('Foo'), $this->id('Bar')]); - - self::assertSame('Foo', $name->first()->value); - self::assertSame($name->first(), $name->first); - } - - #[Test] - public function lastMethodIsAnAliasOfProperty(): void - { - $name = new Name([$this->id('Foo'), $this->id('Bar')]); - - self::assertSame('Bar', $name->last()->value); - self::assertSame($name->last(), $name->last); - } - - #[Test] - public function isSimpleMethodIsAnAliasOfProperty(): void - { - $name = new Name([$this->id('Foo')]); - - self::assertTrue($name->isSimple()); - self::assertSame($name->isSimple(), $name->isSimple); - } - - #[Test] - public function isSpecialMethodIsAnAliasOfProperty(): void - { - $name = new Name([$this->id('self')]); - - self::assertTrue($name->isSpecial()); - self::assertSame($name->isSpecial(), $name->isSpecial); - } - - #[Test] - public function isBuiltinMethodIsAnAliasOfProperty(): void - { - $name = new Name([$this->id('int')]); - - self::assertTrue($name->isBuiltin()); - self::assertSame($name->isBuiltin(), $name->isBuiltin); - } - - #[Test] - public function virtualPropertiesReflectSegmentMutations(): void - { - $name = new Name([$this->id('Foo')]); - - self::assertTrue($name->isSimple); - - $name->segments[] = $this->id('Bar'); - - self::assertFalse($name->isSimple, 'Virtual properties must be recalculated on each access'); - self::assertSame('Bar', $name->last->value); - } - - #[Test] - public function issetReturnsTrueForVirtualProperties(): void - { - $name = new Name([$this->id('Foo')]); - - self::assertTrue(isset($name->first)); - self::assertTrue(isset($name->last)); - self::assertTrue(isset($name->isSimple)); - self::assertTrue(isset($name->isSpecial)); - self::assertTrue(isset($name->isBuiltin)); - } - - #[Test] - public function issetReturnsFalseForUnknownProperty(): void - { - $name = new Name([$this->id('Foo')]); - - self::assertFalse(isset($name->unknown)); - } - - #[Test] - public function readingUnknownPropertyThrows(): void - { - $name = new Name([$this->id('Foo')]); - - $this->expectException(\OutOfRangeException::class); - - /** @phpstan-ignore-next-line */ - $name->unknown; - } - #[Test] public function fullyQualifiedDefaultValueConstantIsFalse(): void { @@ -436,7 +339,7 @@ public function createFromStringIgnoresRepeatedDelimiters(): void { $name = Name::createFromString('Foo\\\\Bar'); - self::assertSame(['Foo', 'Bar'], $name->toStringArray()); + self::assertSame(['Foo', 'Bar'], $name->toArrayStrings()); } #[Test] @@ -444,7 +347,7 @@ public function createFromStringIgnoresTrailingDelimiter(): void { $name = Name::createFromString('Foo\Bar\\'); - self::assertSame(['Foo', 'Bar'], $name->toStringArray()); + self::assertSame(['Foo', 'Bar'], $name->toArrayStrings()); self::assertFalse($name->isFullyQualified); } @@ -458,16 +361,7 @@ public function __toString(): string } }; - self::assertSame(['Foo', 'Bar'], Name::createFromString($stringable)->toStringArray()); - } - - #[Test] - public function createFromStringSegmentsAcceptsStrings(): void - { - $name = Name::createFromStringSegments(['Foo', 'Bar'], true); - - self::assertSame(['Foo', 'Bar'], $name->toStringArray()); - self::assertTrue($name->isFullyQualified); + self::assertSame(['Foo', 'Bar'], Name::createFromString($stringable)->toArrayStrings()); } #[Test] @@ -497,7 +391,7 @@ public function sliceKeepsQualificationFlag(): void $sliced = $name->slice(1); - self::assertSame(['Bar', 'Baz'], $sliced->toStringArray()); + self::assertSame(['Bar', 'Baz'], $sliced->toArrayStrings()); self::assertTrue($sliced->isFullyQualified); } @@ -508,7 +402,7 @@ public function sliceDoesNotModifyTheOriginalName(): void $name->slice(1); - self::assertSame(['Foo', 'Bar', 'Baz'], $name->toStringArray()); + self::assertSame(['Foo', 'Bar', 'Baz'], $name->toArrayStrings()); } #[Test] @@ -518,7 +412,7 @@ public function withAddedKeepsQualificationFlagOfTheReceiver(): void $result = $name->withAdded(Name::createFromString('Test\Class')); - self::assertSame(['Some', 'Any', 'Test', 'Class'], $result->toStringArray()); + self::assertSame(['Some', 'Any', 'Test', 'Class'], $result->toArrayStrings()); self::assertTrue($result->isFullyQualified); } @@ -530,8 +424,8 @@ public function withAddedDoesNotModifyArguments(): void $name->withAdded($added); - self::assertSame(['Some', 'Any'], $name->toStringArray()); - self::assertSame(['Test'], $added->toStringArray()); + self::assertSame(['Some', 'Any'], $name->toArrayStrings()); + self::assertSame(['Test'], $added->toArrayStrings()); } #[Test] @@ -562,7 +456,7 @@ public function toFullQualifiedKeepsSegments(): void $result = $name->toFullQualified(); self::assertTrue($result->isFullyQualified); - self::assertSame(['Foo', 'Bar'], $result->toStringArray()); + self::assertSame(['Foo', 'Bar'], $result->toArrayStrings()); self::assertFalse($name->isFullyQualified, 'The original name must not be modified'); } @@ -574,7 +468,7 @@ public function toUnqualifiedKeepsSegments(): void $result = $name->toUnqualified(); self::assertFalse($result->isFullyQualified); - self::assertSame(['Foo', 'Bar'], $result->toStringArray()); + self::assertSame(['Foo', 'Bar'], $result->toArrayStrings()); self::assertTrue($name->isFullyQualified, 'The original name must not be modified'); } @@ -607,7 +501,7 @@ public function serializePayloadContainsSegmentsOffsetAndFqnFlag(): void $name = Name::createFromString('Foo'); $name->offset = 3; - self::assertSame([$name->segments, 3, false], $name->__serialize()); + self::assertSame([$name->parts, 3, false], $name->__serialize()); } #[Test] @@ -626,7 +520,7 @@ public function offsetMethodIsAnAliasOfProperty(): void $name = Name::createFromString('Foo'); $name->offset = 9; - self::assertSame(9, $name->offset()); + self::assertSame(9, $name->offset); } #[Test] diff --git a/libs/types/tests/NodeListTest.php b/libs/types/tests/NodeListTest.php index 87f2a87..007aaae 100644 --- a/libs/types/tests/NodeListTest.php +++ b/libs/types/tests/NodeListTest.php @@ -69,8 +69,8 @@ public function firstAndLastAreNullWhenEmpty(): void self::assertNull($list->first()); self::assertNull($list->last()); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -95,17 +95,6 @@ public function firstAndLastAreTheSameNodeInSingleItemList(): void self::assertSame($a, $list->last()); } - #[Test] - public function virtualPropertiesAreAliasesOfMethods(): void - { - $a = $this->node('A'); - $b = $this->node('B'); - $list = $this->list([$a, $b]); - - self::assertSame($list->first(), $list->first); - self::assertSame($list->last(), $list->last); - } - #[Test] public function virtualPropertiesReflectListMutations(): void { @@ -115,35 +104,7 @@ public function virtualPropertiesReflectListMutations(): void $list->items[] = $b; - self::assertSame($b, $list->last, 'The $last property must be recalculated on each access'); - } - - #[Test] - public function issetReturnsTrueForVirtualProperties(): void - { - $list = $this->list(); - - self::assertTrue(isset($list->first)); - self::assertTrue(isset($list->last)); - } - - #[Test] - public function issetReturnsFalseForUnknownProperty(): void - { - $list = $this->list(); - - self::assertFalse(isset($list->unknown)); - } - - #[Test] - public function readingUnknownPropertyThrows(): void - { - $list = $this->list(); - - $this->expectException(\OutOfRangeException::class); - - /** @phpstan-ignore-next-line */ - $list->unknown; + self::assertSame($b, $list->last(), 'The $last property must be recalculated on each access'); } #[Test] @@ -282,6 +243,6 @@ public function countReflectsNumberOfNodes(): void #[Test] public function defaultOffsetIsZero(): void { - self::assertSame(0, $this->list()->offset()); + self::assertSame(0, $this->list()->offset); } } diff --git a/libs/types/tests/NodeTest.php b/libs/types/tests/NodeTest.php index 93c3e94..e70c4b0 100644 --- a/libs/types/tests/NodeTest.php +++ b/libs/types/tests/NodeTest.php @@ -152,7 +152,7 @@ public function everyNodeImplementsNodeInterface(Node $node): void public function everyNodeHasZeroOffsetByDefault(Node $node): void { self::assertSame(0, $node->offset); - self::assertSame(0, $node->offset()); + self::assertSame(0, $node->offset); } #[Test] @@ -162,7 +162,7 @@ public function everyNodeOffsetIsWritableAndReflectedByMethod(Node $node): void $node->offset = 42; self::assertSame(42, $node->offset); - self::assertSame(42, $node->offset(), 'The offset() method must be an alias of the $offset property'); + self::assertSame(42, $node->offset, 'The offset() method must be an alias of the $offset property'); } /** @@ -216,7 +216,7 @@ public function abstractNodeOffsetDefaultsToZero(): void { $node = new class extends Node {}; - self::assertSame(0, $node->offset()); + self::assertSame(0, $node->offset); } #[Test] diff --git a/libs/types/tests/Shape/ClassConstFieldNodeTest.php b/libs/types/tests/Shape/ClassConstFieldNodeTest.php index 0c59191..9630959 100644 --- a/libs/types/tests/Shape/ClassConstFieldNodeTest.php +++ b/libs/types/tests/Shape/ClassConstFieldNodeTest.php @@ -36,7 +36,7 @@ public function indexFormatsAsClassDoubleColonConst(): void $key = $this->makeKey('MyEnum', 'ACTIVE'); $node = new ClassConstFieldNode($key, new NamedTypeNode(Name::createFromString('string'))); - self::assertSame('MyEnum::ACTIVE', $node->index); + self::assertSame('MyEnum::ACTIVE', $node->getIndex()); } #[Test] @@ -45,7 +45,7 @@ public function indexWithQualifiedClassName(): void $key = $this->makeKey('Vendor\Package\Status', 'OK'); $node = new ClassConstFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('Vendor\Package\Status::OK', $node->index); + self::assertSame('Vendor\Package\Status::OK', $node->getIndex()); } #[Test] diff --git a/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php b/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php index dfd1e71..9aa194b 100644 --- a/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php +++ b/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php @@ -31,7 +31,7 @@ public function indexWithConstantSuffix(): void $key = new ClassConstMaskNode(Name::createFromString('MyEnum'), new Identifier('STATUS')); $node = new ClassConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('MyEnum::STATUS*', $node->index); + self::assertSame('MyEnum::STATUS*', $node->getIndex()); } #[Test] @@ -40,7 +40,7 @@ public function indexWithNullConstant(): void $key = new ClassConstMaskNode(Name::createFromString('MyEnum')); $node = new ClassConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('MyEnum::*', $node->index); + self::assertSame('MyEnum::*', $node->getIndex()); } #[Test] diff --git a/libs/types/tests/Shape/ConstMaskFieldNodeTest.php b/libs/types/tests/Shape/ConstMaskFieldNodeTest.php index c973946..2df1c38 100644 --- a/libs/types/tests/Shape/ConstMaskFieldNodeTest.php +++ b/libs/types/tests/Shape/ConstMaskFieldNodeTest.php @@ -30,7 +30,7 @@ public function indexReturnsConstMaskNodeAsString(): void $key = new ConstMaskNode(Name::createFromString('MY_CONST')); $node = new ConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('MY_CONST*', $node->index); + self::assertSame('MY_CONST*', $node->getIndex()); } #[Test] @@ -39,7 +39,7 @@ public function indexWithQualifiedName(): void $key = new ConstMaskNode(Name::createFromString('Vendor\Pkg\STATUS')); $node = new ConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('Vendor\Pkg\STATUS*', $node->index); + self::assertSame('Vendor\Pkg\STATUS*', $node->getIndex()); } #[Test] diff --git a/libs/types/tests/Shape/ExplicitFieldNodeTest.php b/libs/types/tests/Shape/ExplicitFieldNodeTest.php index a685792..347d9f0 100644 --- a/libs/types/tests/Shape/ExplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ExplicitFieldNodeTest.php @@ -80,38 +80,7 @@ public static function provideExplicitFields(): iterable #[DataProvider('provideExplicitFields')] public function indexMethodReturnsPrettyPrintedKey(ExplicitFieldNode $field, string $index): void { - self::assertSame($index, $field->index()); - } - - #[Test] - #[DataProvider('provideExplicitFields')] - public function indexPropertyIsAnAliasOfIndexMethod(ExplicitFieldNode $field, string $index): void - { - self::assertSame($field->index(), $field->index); - } - - #[Test] - #[DataProvider('provideExplicitFields')] - public function issetReturnsTrueForIndexProperty(ExplicitFieldNode $field, string $index): void - { - self::assertTrue(isset($field->index)); - } - - #[Test] - #[DataProvider('provideExplicitFields')] - public function issetReturnsFalseForUnknownProperty(ExplicitFieldNode $field, string $index): void - { - self::assertFalse(isset($field->unknown)); - } - - #[Test] - #[DataProvider('provideExplicitFields')] - public function readingUnknownPropertyThrows(ExplicitFieldNode $field, string $index): void - { - $this->expectException(\OutOfRangeException::class); - - /** @phpstan-ignore-next-line */ - $field->unknown; + self::assertSame($index, $field->getIndex()); } #[Test] @@ -119,12 +88,12 @@ public function indexIsRecalculatedAfterKeyMutation(): void { $field = new NamedFieldNode(new Identifier('key'), self::type()); - self::assertSame('key', $field->index()); + self::assertSame('key', $field->getIndex()); $field->key = new Identifier('other'); - self::assertSame('other', $field->index(), 'The index must be derived from the current key'); - self::assertSame('other', $field->index); + self::assertSame('other', $field->getIndex(), 'The index must be derived from the current key'); + self::assertSame('other', $field->getIndex()); } #[Test] @@ -186,7 +155,7 @@ public function classConstMaskFieldWithoutConstantIsStringified(): void self::type(), ); - self::assertSame('Vendor\Status::*', $field->index()); + self::assertSame('Vendor\Status::*', $field->getIndex()); } #[Test] @@ -194,7 +163,7 @@ public function numericFieldIndexOfZero(): void { $field = new NumericFieldNode(new IntLiteralNode(0), self::type()); - self::assertSame('0', $field->index()); + self::assertSame('0', $field->getIndex()); } #[Test] @@ -202,7 +171,7 @@ public function numericFieldIndexOfNegativeKey(): void { $field = new NumericFieldNode(new IntLiteralNode(-1), self::type()); - self::assertSame('-1', $field->index()); + self::assertSame('-1', $field->getIndex()); } #[Test] @@ -210,7 +179,7 @@ public function stringNamedFieldIndexIsTheDecodedValue(): void { $field = new StringNamedFieldNode(new StringLiteralNode("a\nb"), self::type()); - self::assertSame("a\nb", $field->index()); + self::assertSame("a\nb", $field->getIndex()); } #[Test] diff --git a/libs/types/tests/Shape/FieldsListNodeTest.php b/libs/types/tests/Shape/FieldsListNodeTest.php index 4f04c54..2d3e834 100644 --- a/libs/types/tests/Shape/FieldsListNodeTest.php +++ b/libs/types/tests/Shape/FieldsListNodeTest.php @@ -24,8 +24,8 @@ public function emptyListByDefault(): void $list = new FieldsListNode(); self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -52,8 +52,8 @@ public function constructorAcceptsFields(): void $list = new FieldsListNode([$a, $b]); self::assertCount(2, $list); - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); + self::assertSame($a, $list->first()); + self::assertSame($b, $list->last()); } #[Test] diff --git a/libs/types/tests/Shape/NamedFieldNodeTest.php b/libs/types/tests/Shape/NamedFieldNodeTest.php index 75d59ae..592c0ee 100644 --- a/libs/types/tests/Shape/NamedFieldNodeTest.php +++ b/libs/types/tests/Shape/NamedFieldNodeTest.php @@ -30,7 +30,7 @@ public function indexReturnsKeyValue(): void $key = new Identifier('myField'); $node = new NamedFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('myField', $node->index); + self::assertSame('myField', $node->getIndex()); } #[Test] diff --git a/libs/types/tests/Shape/NumericFieldNodeTest.php b/libs/types/tests/Shape/NumericFieldNodeTest.php index c3e12c7..f7a5d46 100644 --- a/libs/types/tests/Shape/NumericFieldNodeTest.php +++ b/libs/types/tests/Shape/NumericFieldNodeTest.php @@ -30,7 +30,7 @@ public function indexReturnsStringifiedKeyValue(): void $key = new IntLiteralNode(42); $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('42', $node->index); + self::assertSame('42', $node->getIndex()); } #[Test] @@ -39,7 +39,7 @@ public function indexForZeroKey(): void $key = new IntLiteralNode(0); $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('0', $node->index); + self::assertSame('0', $node->getIndex()); } #[Test] diff --git a/libs/types/tests/Shape/StringNamedFieldNodeTest.php b/libs/types/tests/Shape/StringNamedFieldNodeTest.php index 3fe395b..3aa01ce 100644 --- a/libs/types/tests/Shape/StringNamedFieldNodeTest.php +++ b/libs/types/tests/Shape/StringNamedFieldNodeTest.php @@ -30,7 +30,7 @@ public function indexReturnsKeyValue(): void $key = new StringLiteralNode('field-name'); $node = new StringNamedFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('field-name', $node->index); + self::assertSame('field-name', $node->getIndex()); } #[Test] diff --git a/libs/types/tests/Template/TemplateArgumentsListNodeTest.php b/libs/types/tests/Template/TemplateArgumentsListNodeTest.php index dc541c6..ec230f4 100644 --- a/libs/types/tests/Template/TemplateArgumentsListNodeTest.php +++ b/libs/types/tests/Template/TemplateArgumentsListNodeTest.php @@ -24,8 +24,8 @@ public function emptyListByDefault(): void $list = new TemplateArgumentListNode(); self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -36,8 +36,8 @@ public function constructorAcceptsArguments(): void $list = new TemplateArgumentListNode([$a, $b]); self::assertCount(2, $list); - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); + self::assertSame($a, $list->first()); + self::assertSame($b, $list->last()); } #[Test] From 2b5bcdd43d972c38cf82ba1573f200b0ce0aa404 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:00:35 +0300 Subject: [PATCH 009/119] Improve reader behaviour --- libs/reader/src/ReflectionReader.php | 2 +- libs/reader/tests/ReflectionReaderTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libs/reader/src/ReflectionReader.php b/libs/reader/src/ReflectionReader.php index 804e7e1..e0fe256 100644 --- a/libs/reader/src/ReflectionReader.php +++ b/libs/reader/src/ReflectionReader.php @@ -160,7 +160,7 @@ private function convertNonNullNamedType(\ReflectionNamedType $type): TypeNode $name = Name::createFromString($literal); - if ($type->isBuiltin() || $name->isSpecial || $name->isBuiltin) { + if ($type->isBuiltin() || $name->isSpecial() || $name->isBuiltin()) { return new NamedTypeNode($name); } diff --git a/libs/reader/tests/ReflectionReaderTest.php b/libs/reader/tests/ReflectionReaderTest.php index 09c69f5..0a7abcc 100644 --- a/libs/reader/tests/ReflectionReaderTest.php +++ b/libs/reader/tests/ReflectionReaderTest.php @@ -19,9 +19,9 @@ public function testGetTypeConvertsNamedType(): void $reader = new ReflectionReader(); $type = $reader->getType( - new \ReflectionProperty(new class { + (new \ReflectionProperty(new class { public int $value; - }, 'value')->getType(), + }, 'value'))->getType(), ); self::assertSameType(self::builtin('int'), $type); @@ -50,7 +50,7 @@ public function __toString(): string self::fail('Expected an UnrecognizedTypeException to be thrown'); } catch (UnrecognizedTypeException $e) { self::assertInstanceOf(UnrecognizedReflectionTypeException::class, $e); - self::assertTrue(new \ReflectionClass($e)->isFinal()); + self::assertTrue((new \ReflectionClass($e))->isFinal()); } } @@ -67,9 +67,9 @@ public function testMixedTypeIsNotNullable(): void $reader = new ReflectionReader(); $type = $reader->getType( - new \ReflectionProperty(new class { + (new \ReflectionProperty(new class { public mixed $value; - }, 'value')->getType(), + }, 'value'))->getType(), ); self::assertSameType(self::builtin('mixed'), $type); @@ -89,7 +89,7 @@ public function testContextualExceptionWrapsAndChainsRootCause(): void ); self::assertInstanceOf(UnrecognizedReflectionTypeException::class, $exception); - self::assertTrue(new \ReflectionClass($exception)->isFinal()); + self::assertTrue((new \ReflectionClass($exception))->isFinal()); self::assertSame($previous, $exception->getPrevious()); self::assertStringContainsString('SINGLE', $exception->getMessage()); } From a9da7aa32b3eb2ac703d0a99ef94b8d28556c1d5 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:00:46 +0300 Subject: [PATCH 010/119] Improve printer behaviour --- libs/printer/src/PrettyTypePrinter.php | 2 +- libs/printer/tests/NativeTypePrinterTest.php | 50 ++++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libs/printer/src/PrettyTypePrinter.php b/libs/printer/src/PrettyTypePrinter.php index 9f3aafa..2948dfa 100644 --- a/libs/printer/src/PrettyTypePrinter.php +++ b/libs/printer/src/PrettyTypePrinter.php @@ -234,7 +234,7 @@ protected function printAttributeGroup(AttributeGroupNode $group): string { $result = '#['; - $last = $group->last; + $last = $group->last(); foreach ($group as $attribute) { $result .= $attribute->name->toString(); diff --git a/libs/printer/tests/NativeTypePrinterTest.php b/libs/printer/tests/NativeTypePrinterTest.php index 93127ee..31cdd24 100644 --- a/libs/printer/tests/NativeTypePrinterTest.php +++ b/libs/printer/tests/NativeTypePrinterTest.php @@ -15,125 +15,125 @@ final class NativeTypePrinterTest extends TestCase { public function testPrintTypesListAsIterable(): void { - self::assertSame('iterable', new NativeTypePrinter()->print(self::parse('int[]'))); + self::assertSame('iterable', (new NativeTypePrinter())->print(self::parse('int[]'))); } public function testPrintShapeAsPlainName(): void { - self::assertSame('array', new NativeTypePrinter()->print(self::parse('array{foo: int}'))); + self::assertSame('array', (new NativeTypePrinter())->print(self::parse('array{foo: int}'))); } public function testPrintTemplateTypeUsesNameOnly(): void { - self::assertSame('array', new NativeTypePrinter()->print(self::parse('array'))); + self::assertSame('array', (new NativeTypePrinter())->print(self::parse('array'))); } public function testPrintUnionTypeWithoutWhitespaces(): void { - self::assertSame('int|string', new NativeTypePrinter()->print(self::parse('int|string'))); + self::assertSame('int|string', (new NativeTypePrinter())->print(self::parse('int|string'))); } public function testPrintUnionCollapsesMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('int|mixed'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('int|mixed'))); } public function testPrintUnionCollapsesTrueAndFalseToBool(): void { - self::assertSame('bool', new NativeTypePrinter()->print(self::parse('true|false'))); + self::assertSame('bool', (new NativeTypePrinter())->print(self::parse('true|false'))); } public function testPrintUnionCollapsesBoolPairKeepingOtherMembers(): void { - self::assertSame('bool|int', new NativeTypePrinter()->print(self::parse('true|false|int'))); + self::assertSame('bool|int', (new NativeTypePrinter())->print(self::parse('true|false|int'))); } public function testPrintUnionCollapsesNamedTrueAndFalseToBool(): void { - self::assertSame('bool', new NativeTypePrinter()->print(self::parse('\true|\false'))); + self::assertSame('bool', (new NativeTypePrinter())->print(self::parse('\true|\false'))); } public function testPrintUnionCollapsesNamedBoolPairKeepingOtherMembers(): void { - self::assertSame('int|bool', new NativeTypePrinter()->print(self::parse('\true|\false|int'))); + self::assertSame('int|bool', (new NativeTypePrinter())->print(self::parse('\true|\false|int'))); } public function testPrintUnionDeduplicatesMembers(): void { - self::assertSame('int', new NativeTypePrinter()->print(self::parse('int|int'))); + self::assertSame('int', (new NativeTypePrinter())->print(self::parse('int|int'))); } public function testPrintNestedLogicalTypeIsParenthesized(): void { - self::assertSame('int|(string&Foo)', new NativeTypePrinter() + self::assertSame('int|(string&Foo)', (new NativeTypePrinter()) ->print(self::parse('int|(string&Foo)'))); } public function testPrintDeeplyNestedUnionIsFlattened(): void { - self::assertSame('A|B|C|D', new NativeTypePrinter() + self::assertSame('A|B|C|D', (new NativeTypePrinter()) ->print(self::parse('((A | B) | C) | D'))); } public function testPrintDeeplyNestedIntersectionIsFlattened(): void { - self::assertSame('A&B&C&D', new NativeTypePrinter() + self::assertSame('A&B&C&D', (new NativeTypePrinter()) ->print(self::parse('((A & B) & C) & D'))); } public function testPrintDeeplyNestedMixedLogicalTypeKeepsParentheses(): void { - self::assertSame('((A|B)&C)|D', new NativeTypePrinter() + self::assertSame('((A|B)&C)|D', (new NativeTypePrinter()) ->print(self::parse('((A | B) & C) | D'))); } public function testPrintIntersectionTypeWithoutWhitespaces(): void { - self::assertSame('int&string', new NativeTypePrinter()->print(self::parse('int&string'))); + self::assertSame('int&string', (new NativeTypePrinter())->print(self::parse('int&string'))); } public function testPrintCallableTypeAsName(): void { - self::assertSame('callable', new NativeTypePrinter() + self::assertSame('callable', (new NativeTypePrinter()) ->print(self::parse('callable(int, string): void'))); } public function testPrintTernaryAsUnionOfBranches(): void { - self::assertSame('string|bool', new NativeTypePrinter() + self::assertSame('string|bool', (new NativeTypePrinter()) ->print(self::parse('($x is int ? string : bool)'))); } public function testPrintClassConstantAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('Foo::BAR'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('Foo::BAR'))); } public function testPrintClassConstantMaskAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('Foo::*'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('Foo::*'))); } public function testPrintConstantMaskAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('FOO_*'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('FOO_*'))); } public function testPrintTypeOffsetAccessAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('Foo[Bar]'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('Foo[Bar]'))); } public function testPrintThisVariableAsSelf(): void { - self::assertSame('self', new NativeTypePrinter() + self::assertSame('self', (new NativeTypePrinter()) ->print(self::parse('($this is int ? $this : $this)'))); } #[DataProvider('literalProvider')] public function testPrintLiteralAsNativeType(string $type, string $expected): void { - self::assertSame($expected, new NativeTypePrinter()->print(self::parse($type))); + self::assertSame($expected, (new NativeTypePrinter())->print(self::parse($type))); } /** @@ -151,7 +151,7 @@ public static function literalProvider(): iterable public function testPrintUsesPreloadedAlias(): void { - self::assertSame('int', new NativeTypePrinter()->print(self::parse('positive-int'))); + self::assertSame('int', (new NativeTypePrinter())->print(self::parse('positive-int'))); } public function testAddTypeAlias(): void @@ -199,6 +199,6 @@ public function testPrintThrowsOnNonPrintableNode(): void $this->expectException(NonPrintableNodeException::class); - new NativeTypePrinter()->print($node); + (new NativeTypePrinter())->print($node); } } From 07604c08f326710be31bf23c5bb0d6697c269bc8 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:01:33 +0300 Subject: [PATCH 011/119] Fix phpdoc PHP 8.1 compatibility using Rector downgrade --- libs/phpdoc/.github/workflows/tests.yml | 2 +- libs/phpdoc/README.md | 2 +- libs/phpdoc/composer.json | 4 +- libs/phpdoc/phpstan.neon | 3 + .../Combinator/AuthorNameCombinator.php | 4 +- .../Combinator/CallableTypeCombinator.php | 4 +- .../Combinator/DescriptionCombinator.php | 6 +- .../DocBlock/Combinator/EmailCombinator.php | 4 +- .../Combinator/FlowTypeCombinator.php | 4 +- .../DocBlock/Combinator/IntegerCombinator.php | 6 +- .../Combinator/IssueNameCombinator.php | 6 +- .../DocBlock/Combinator/NameCombinator.php | 4 +- .../Combinator/ReferenceCombinator.php | 4 +- .../DocBlock/Combinator/TypeCombinator.php | 10 +- .../src/DocBlock/Combinator/UriCombinator.php | 4 +- .../src/DocBlock/Combinator/UrlCombinator.php | 4 +- .../Combinator/VariableCombinator.php | 4 +- .../DocBlock/Combinator/VersionCombinator.php | 6 +- .../Combinator/VisibilityCombinator.php | 4 +- .../src/DocBlock/Description/Description.php | 4 +- .../Description/TaggedDescription.php | 12 ++- libs/phpdoc/src/DocBlock/DocBlock.php | 14 +-- .../Reference/ClassConstantReference.php | 6 +- .../Reference/ClassMethodReference.php | 6 +- .../Reference/ClassPropertyReference.php | 6 +- .../src/DocBlock/Reference/CodeReference.php | 4 +- .../DocBlock/Reference/FunctionReference.php | 4 +- .../DocBlock/Reference/ReferenceInterface.php | 10 +- .../DocBlock/Reference/SymbolReference.php | 4 +- .../src/DocBlock/Reference/TypeReference.php | 8 +- .../src/DocBlock/Reference/UriReference.php | 6 +- .../src/DocBlock/Reference/UrlReference.php | 2 +- .../DocBlock/Reference/VariableReference.php | 4 +- .../Tag/AbstractTag/AbstractTagDefinition.php | 2 +- .../Tag/AccessTag/AccessTagDefinition.php | 2 +- .../AllowPrivateMutationTagDefinition.php | 2 +- .../DocBlock/Tag/ApiTag/ApiTagDefinition.php | 2 +- .../AssertIfFalseTagDefinition.php | 2 +- .../AssertIfTrueTagDefinition.php | 2 +- .../Tag/AssertTag/AssertTagDefinition.php | 2 +- .../Tag/AuthorTag/AuthorTagDefinition.php | 2 +- .../Tag/CategoryTag/CategoryTagDefinition.php | 2 +- .../Tag/CheckTypeTag/CheckTypeTag.php | 9 +- .../PsalmCheckTypeExactTagDefinition.php | 2 +- .../PsalmCheckTypeTagDefinition.php | 2 +- .../CodingStandardsIgnoreEndTagDefinition.php | 2 +- ...CodingStandardsIgnoreFileTagDefinition.php | 2 +- ...CodingStandardsIgnoreLineTagDefinition.php | 2 +- ...odingStandardsIgnoreStartTagDefinition.php | 2 +- .../CodingStandardsTagDefinition.php | 2 +- .../ConsistentConstructorTagDefinition.php | 2 +- .../CopyrightTag/CopyrightTagDefinition.php | 2 +- .../DeprecatedTag/DeprecatedTagDefinition.php | 2 +- .../Tag/ExampleTag/ExampleTagDefinition.php | 2 +- .../ExpectedExceptionTagDefinition.php | 2 +- .../FilesourceTag/FilesourceTagDefinition.php | 2 +- .../Tag/FinalTag/FinalTagDefinition.php | 2 +- .../FormatterOffTagDefinition.php | 2 +- .../FormatterOnTagDefinition.php | 2 +- .../src/DocBlock/Tag/GenericTagDefinition.php | 2 +- .../Tag/GlobalTag/GlobalTagDefinition.php | 2 +- .../Tag/IgnoreTag/IgnoreTagDefinition.php | 2 +- .../ImmutableTag/ImmutableTagDefinition.php | 2 +- .../ImportTypeAliasTag/ImportTypeAliasTag.php | 9 +- .../ImportTypeAliasTagDefinition.php | 2 +- .../InheritDocTag/InheritDocTagDefinition.php | 2 +- .../InheritanceTag/ExtendsTagDefinition.php | 2 +- .../ImplementsTagDefinition.php | 2 +- .../Tag/InheritanceTag/UseTagDefinition.php | 2 +- .../Tag/InternalTag/InternalTagDefinition.php | 2 +- .../Tag/LanguageTag/LanguageTagDefinition.php | 2 +- .../Tag/LicenseTag/LicenseTagDefinition.php | 2 +- .../Tag/LinkTag/LinkTagDefinition.php | 2 +- .../Tag/MethodTag/MethodTagDefinition.php | 2 +- .../Tag/MixinTag/MixinTagDefinition.php | 2 +- .../Tag/NameTag/NameTagDefinition.php | 2 +- .../NoNamedArgumentsTagDefinition.php | 2 +- .../NoinspectionTagDefinition.php | 2 +- .../NotDeprecatedTagDefinition.php | 2 +- .../Tag/OverrideTag/OverrideTagDefinition.php | 2 +- .../Tag/PackageTag/PackageTagDefinition.php | 2 +- .../ParamClosureThisTagDefinition.php | 2 +- ...mmediatelyInvokedCallableTagDefinition.php | 2 +- ...ParamLaterInvokedCallableTagDefinition.php | 2 +- .../Tag/ParamOutTag/ParamOutTagDefinition.php | 2 +- .../Tag/ParamTag/ParamTagDefinition.php | 2 +- .../PhanClosureScopeTagDefinition.php | 2 +- ...tructorUsedForSideEffectsTagDefinition.php | 2 +- .../PhanFileSuppressTagDefinition.php | 2 +- ...bidUndeclaredMagicMethodsTagDefinition.php | 2 +- ...UndeclaredMagicPropertiesTagDefinition.php | 2 +- .../PhanHardcodeReturnTypeTagDefinition.php | 2 +- .../PhanOutputReferenceTagDefinition.php | 2 +- .../PhanRealReturnTagDefinition.php | 2 +- .../PhanSideEffectFreeTagDefinition.php | 2 +- .../PhanSuppressCurrentLineTagDefinition.php | 2 +- .../PhanSuppressNextLineTagDefinition.php | 2 +- .../PhanSuppressNextNextLineTagDefinition.php | 2 +- .../PhanSuppressPreviousLineTagDefinition.php | 2 +- .../PhanTransientTagDefinition.php | 2 +- .../PhanWriteOnlyTagDefinition.php | 2 +- .../PhpStanIgnoreLineTagDefinition.php | 2 +- .../PhpStanIgnoreNextLineTagDefinition.php | 2 +- .../PhpStanIgnoreTagDefinition.php | 2 +- .../PhpStanImpureTagDefinition.php | 2 +- .../PhpcsSuppressTagDefinition.php | 2 +- .../PropertyTag/PropertyReadTagDefinition.php | 2 +- .../Tag/PropertyTag/PropertyTagDefinition.php | 2 +- .../PropertyWriteTagDefinition.php | 2 +- .../PsalmAssertUntaintedTagDefinition.php | 2 +- .../PsalmConsistentTemplatesTagDefinition.php | 2 +- ...PsalmExternalMutationFreeTagDefinition.php | 2 +- .../PsalmFlowTag/PsalmFlowTagDefinition.php | 2 +- .../PsalmIfThisIsTagDefinition.php | 2 +- ...PsalmIgnoreFalsableReturnTagDefinition.php | 2 +- ...PsalmIgnoreNullableReturnTagDefinition.php | 2 +- .../PsalmIgnoreVarTagDefinition.php | 2 +- ...PsalmIgnoreVariableMethodTagDefinition.php | 2 +- ...almIgnoreVariablePropertyTagDefinition.php | 2 +- .../PsalmInheritorsTagDefinition.php | 2 +- .../PsalmMutationFreeTagDefinition.php | 2 +- .../PsalmNoSealMethodsTagDefinition.php | 2 +- .../PsalmNoSealPropertiesTagDefinition.php | 2 +- ...mOverrideMethodVisibilityTagDefinition.php | 2 +- ...verridePropertyVisibilityTagDefinition.php | 2 +- .../PsalmScopeThisTagDefinition.php | 2 +- .../PsalmStubOverrideTagDefinition.php | 2 +- .../PsalmTaintEscapeTagDefinition.php | 2 +- .../PsalmTaintSinkTagDefinition.php | 2 +- .../PsalmTaintSourceTagDefinition.php | 2 +- .../PsalmTaintSpecializeTagDefinition.php | 2 +- .../PsalmTaintUnescapeTagDefinition.php | 2 +- .../PsalmTraceTag/PsalmTraceTagDefinition.php | 2 +- .../PsalmVariadicTagDefinition.php | 2 +- .../Tag/PureTag/PureTagDefinition.php | 2 +- ...ureUnlessCallableIsImpureTagDefinition.php | 2 +- ...donlyAllowPrivateMutationTagDefinition.php | 2 +- .../Tag/ReadonlyTag/ReadonlyTagDefinition.php | 2 +- .../DocBlock/Tag/ReferencedTagInterface.php | 10 +- .../RequireExtendsTagDefinition.php | 2 +- .../RequireImplementsTagDefinition.php | 2 +- .../Tag/ReturnTag/ReturnTagDefinition.php | 2 +- .../SealMethodsTagDefinition.php | 2 +- .../SealPropertiesTagDefinition.php | 2 +- .../DocBlock/Tag/SeeTag/SeeTagDefinition.php | 2 +- .../Tag/SelfOutTag/SelfOutTagDefinition.php | 2 +- .../Tag/SinceTag/SinceTagDefinition.php | 2 +- .../Tag/SourceTag/SourceTagDefinition.php | 2 +- .../Tag/StaticTag/StaticTagDefinition.php | 2 +- .../StaticVarTag/StaticVarTagDefinition.php | 2 +- .../SubpackageTag/SubpackageTagDefinition.php | 2 +- .../Tag/SuppressTag/SuppressTagDefinition.php | 2 +- libs/phpdoc/src/DocBlock/Tag/TagInterface.php | 22 +---- .../TemplateContravariantTagDefinition.php | 2 +- .../TemplateCovariantTagDefinition.php | 2 +- .../Tag/TemplateTag/TemplateTagDefinition.php | 2 +- .../Tag/ThrowsTag/ThrowsTagDefinition.php | 2 +- .../Tag/TodoTag/TodoTagDefinition.php | 2 +- .../Tag/TypeAliasTag/TypeAliasTag.php | 9 +- .../TypeAliasTag/TypeAliasTagDefinition.php | 2 +- libs/phpdoc/src/DocBlock/Tag/TypedTag.php | 9 +- .../src/DocBlock/Tag/TypedTagInterface.php | 8 +- .../UnusedParamTagDefinition.php | 2 +- .../Tag/UsedByTag/UsedByTagDefinition.php | 2 +- .../Tag/UsesTag/UsesTagDefinition.php | 2 +- .../DocBlock/Tag/VarTag/VarTagDefinition.php | 2 +- .../src/DocBlock/Tag/VariableTagInterface.php | 10 +- .../Tag/VersionTag/VersionTagDefinition.php | 2 +- .../DocBlock/Tag/VersionedTagInterface.php | 10 +- .../Tag/YieldTag/YieldTagDefinition.php | 2 +- .../src/DocBlock/TagDefinition/Spec.php | 2 +- .../TagDefinition/TagDefinitionInterface.php | 31 +------ .../src/DocBlock/TagDefinition/TagPayload.php | 4 +- .../DocBlock/TagDefinition/TagPlacement.php | 2 +- libs/phpdoc/src/DocBlockParser.php | 41 +++++--- .../Exception/ParsingExceptionInterface.php | 18 +--- .../Description/BalancedBraceAwareParser.php | 12 +-- libs/phpdoc/src/Parser/DocBlockAnalyzer.php | 4 +- libs/phpdoc/src/Parser/Grammar/Context.php | 4 +- libs/phpdoc/src/Parser/Grammar/Cursor.php | 93 ++++++++++++------- libs/phpdoc/src/Parser/Grammar/Grammar.php | 4 +- .../Parser/Grammar/Rule/AlternationRule.php | 4 +- .../src/Parser/Grammar/Rule/LiteralRule.php | 6 +- .../src/Parser/Grammar/Rule/MatchRule.php | 8 +- .../Parser/Grammar/Rule/OptionalityRule.php | 4 +- .../Parser/Grammar/Rule/RepetitionRule.php | 8 +- .../Parser/Grammar/Rule/SequencingRule.php | 4 +- .../Parser/Grammar/Rule/TerminalInterface.php | 11 +-- libs/phpdoc/src/Parser/RawDocBlock.php | 6 +- .../src/Parser/Splitter/StringSplitter.php | 16 ++-- .../phpdoc/src/Parser/Tag/StringTagParser.php | 8 +- libs/phpdoc/src/Parser/TagFactory.php | 6 +- libs/phpdoc/src/Parser/TagRegistry.php | 6 +- .../src/Parser/TagSpecificationParser.php | 6 +- libs/phpdoc/src/Platform/PhanPlatform.php | 14 +-- .../src/Platform/PhpCodeSnifferPlatform.php | 7 +- .../src/Platform/PhpDocumentorPlatform.php | 14 +-- libs/phpdoc/src/Platform/PhpStanPlatform.php | 14 +-- libs/phpdoc/src/Platform/PhpStormPlatform.php | 7 +- libs/phpdoc/src/Platform/Platform.php | 21 +++-- .../phpdoc/src/Platform/PlatformInterface.php | 18 ++-- libs/phpdoc/src/Platform/PsalmPlatform.php | 21 +++-- libs/phpdoc/src/Platform/StandardPlatform.php | 21 +++-- .../DocBlock/Description/DescriptionTest.php | 2 +- .../Description/TaggedDescriptionTest.php | 4 +- libs/phpdoc/tests/DocBlock/DocBlockTest.php | 8 +- .../Grammar/DescriptionGrammarRuleTest.php | 4 +- .../Grammar/ReferenceGrammarRuleTest.php | 4 +- .../DocBlock/Grammar/TypeGrammarRuleTest.php | 4 +- .../Grammar/VariableGrammarRuleTest.php | 4 +- .../phpdoc/tests/DocBlock/Tag/FlagTagTest.php | 4 +- .../tests/DocBlock/Tag/InvalidTagTest.php | 2 +- libs/phpdoc/tests/DocBlock/Tag/TagTest.php | 6 +- .../tests/Parser/Grammar/CursorTest.php | 26 +++--- .../tests/Parser/Grammar/DefinitionTest.php | 2 +- libs/phpdoc/tests/TestCase.php | 6 +- 216 files changed, 515 insertions(+), 533 deletions(-) diff --git a/libs/phpdoc/.github/workflows/tests.yml b/libs/phpdoc/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/phpdoc/.github/workflows/tests.yml +++ b/libs/phpdoc/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/phpdoc/README.md b/libs/phpdoc/README.md index a7b74f4..b19cfa8 100644 --- a/libs/phpdoc/README.md +++ b/libs/phpdoc/README.md @@ -28,7 +28,7 @@ composer require type-lang/phpdoc ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/phpdoc/composer.json b/libs/phpdoc/composer.json index 663f4f6..83daccd 100644 --- a/libs/phpdoc/composer.json +++ b/libs/phpdoc/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/phpdoc/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/parser": "^2.0" }, "autoload": { @@ -20,7 +20,7 @@ "require-dev": { "phpstan/phpdoc-parser": "^2.3", "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4" + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" }, "autoload-dev": { "psr-4": { diff --git a/libs/phpdoc/phpstan.neon b/libs/phpdoc/phpstan.neon index cb1db62..f2d4b90 100644 --- a/libs/phpdoc/phpstan.neon +++ b/libs/phpdoc/phpstan.neon @@ -1,5 +1,8 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 treatPhpDocTypesAsCertain: false fileExtensions: - php diff --git a/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php index 4d38ec9..c4784b6 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class AuthorNameCombinator implements CombinatorInterface +final class AuthorNameCombinator implements CombinatorInterface { - public const string NAME = 'AuthorName'; + public const NAME = 'AuthorName'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php index 67e2938..43e0e86 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php @@ -13,9 +13,9 @@ * Reads a type and accepts it only when it is a callable, so that a plain word * (which would parse as a type) is left for the following combinators. */ -final readonly class CallableTypeCombinator extends TypeCombinator +final class CallableTypeCombinator extends TypeCombinator { - public const string NAME = 'CallableType'; + public const NAME = 'CallableType'; #[\Override] public function __invoke(Cursor $cursor): TypeReference diff --git a/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php index 0e90bd7..85e4513 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php @@ -19,12 +19,12 @@ * * @template-implements CombinatorInterface */ -final readonly class DescriptionCombinator implements CombinatorInterface +final class DescriptionCombinator implements CombinatorInterface { - public const string NAME = 'Description'; + public const NAME = 'Description'; public function __construct( - private DescriptionParserInterface $descriptionParser, + private readonly DescriptionParserInterface $descriptionParser, ) {} public function __invoke(Cursor $cursor): DescriptionInterface diff --git a/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php index f7dd923..5710218 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class EmailCombinator implements CombinatorInterface +final class EmailCombinator implements CombinatorInterface { - public const string NAME = 'Email'; + public const NAME = 'Email'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php index ec98809..4583140 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php @@ -14,9 +14,9 @@ * * @template-implements CombinatorInterface */ -final readonly class FlowTypeCombinator implements CombinatorInterface +final class FlowTypeCombinator implements CombinatorInterface { - public const string NAME = 'FlowType'; + public const NAME = 'FlowType'; public function __invoke(Cursor $cursor): FlowType { diff --git a/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php index 72f8a89..8a3b7f9 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php @@ -13,11 +13,11 @@ * * @template-implements CombinatorInterface> */ -final readonly class IntegerCombinator implements CombinatorInterface +final class IntegerCombinator implements CombinatorInterface { - public const string NAME = 'Integer'; + public const NAME = 'Integer'; - private const string DIGITS = '0123456789'; + private const DIGITS = '0123456789'; /** * @return int<0, max> diff --git a/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php index 319a1bf..3221002 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php @@ -14,11 +14,11 @@ * * @template-implements CombinatorInterface */ -final readonly class IssueNameCombinator implements CombinatorInterface +final class IssueNameCombinator implements CombinatorInterface { - public const string NAME = 'IssueName'; + public const NAME = 'IssueName'; - private const string CHARS = 'abcdefghijklmnopqrstuvwxyz' + private const CHARS = 'abcdefghijklmnopqrstuvwxyz' . 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . '0123456789_.-'; diff --git a/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php index 4111e7f..eadd1ca 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class NameCombinator implements CombinatorInterface +final class NameCombinator implements CombinatorInterface { - public const string NAME = 'Name'; + public const NAME = 'Name'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php index 4b9bafa..1993635 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php @@ -19,9 +19,9 @@ * Reads a reference to a code element: a class, a function, a class method, a * class constant, a class property or a variable. */ -final readonly class ReferenceCombinator implements CombinatorInterface +final class ReferenceCombinator implements CombinatorInterface { - public const string NAME = 'Reference'; + public const NAME = 'Reference'; public function __invoke(Cursor $cursor): CodeReference { diff --git a/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php index 389a83a..57b3455 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php @@ -18,17 +18,17 @@ * * @template-implements CombinatorInterface */ -readonly class TypeCombinator implements CombinatorInterface +class TypeCombinator implements CombinatorInterface { - public const string NAME = 'Type'; + public const NAME = 'Type'; public function __construct( - private TypeParserInterface $typeParser + private readonly TypeParserInterface $typeParser ) {} public function __invoke(Cursor $cursor): TypeReference { - $start = $cursor->position; + $start = $cursor->getPosition(); $source = $cursor->readRemainder(); if ($source === '') { @@ -40,7 +40,7 @@ public function __invoke(Cursor $cursor): TypeReference // text (e.g. a description) rather than at the end of the buffer. $result = $this->typeParser->parseTolerant($source); - $cursor->position = $start + $result->offset; + $cursor->moveTo($start + $result->offset); // The tolerant offset also covers the whitespace up to the next token, // so the trailing run is trimmed off the preserved type text. diff --git a/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php index 492ef98..719b13b 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php @@ -15,9 +15,9 @@ * * @template-implements CombinatorInterface */ -final readonly class UriCombinator implements CombinatorInterface +final class UriCombinator implements CombinatorInterface { - public const string NAME = 'URI'; + public const NAME = 'URI'; public function __invoke(Cursor $cursor): UriReference { diff --git a/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php index 40f66a4..32196ea 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php @@ -17,9 +17,9 @@ * * @template-implements CombinatorInterface */ -final readonly class UrlCombinator implements CombinatorInterface +final class UrlCombinator implements CombinatorInterface { - public const string NAME = 'URL'; + public const NAME = 'URL'; public function __invoke(Cursor $cursor): UrlReference { diff --git a/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php index 4aa5db5..213f788 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class VariableCombinator implements CombinatorInterface +final class VariableCombinator implements CombinatorInterface { - public const string NAME = 'Variable'; + public const NAME = 'Variable'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php index 7c39d8d..106613a 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php @@ -14,11 +14,11 @@ * * @template-implements CombinatorInterface */ -final readonly class VersionCombinator implements CombinatorInterface +final class VersionCombinator implements CombinatorInterface { - public const string NAME = 'Version'; + public const NAME = 'Version'; - private const string DIGITS = '0123456789'; + private const DIGITS = '0123456789'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php index 285e5dc..730eea3 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php @@ -15,9 +15,9 @@ * * @template-implements CombinatorInterface */ -final readonly class VisibilityCombinator implements CombinatorInterface +final class VisibilityCombinator implements CombinatorInterface { - public const string NAME = 'Visibility'; + public const NAME = 'Visibility'; public function __invoke(Cursor $cursor): Visibility { diff --git a/libs/phpdoc/src/DocBlock/Description/Description.php b/libs/phpdoc/src/DocBlock/Description/Description.php index 37b280a..14e642d 100644 --- a/libs/phpdoc/src/DocBlock/Description/Description.php +++ b/libs/phpdoc/src/DocBlock/Description/Description.php @@ -4,10 +4,10 @@ namespace TypeLang\PhpDoc\DocBlock\Description; -final readonly class Description implements DescriptionInterface +final class Description implements DescriptionInterface { public function __construct( - public string $value = '', + public readonly string $value = '', ) {} public static function createIfNotEmpty(string $value): ?self diff --git a/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php b/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php index 1a19392..689f4c7 100644 --- a/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php +++ b/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php @@ -30,20 +30,22 @@ final class TaggedDescription implements public readonly array $components; /** - * Gets a list of all tags within a description + * A list of all tags within a description * * @var list */ - public array $tags { - get => $this->tags ??= $this->only(TagInterface::class); - } + public readonly array $tags; /** * @param iterable $components */ public function __construct(iterable $components = []) { - $this->components = \iterator_to_array($components, false); + $this->components = \is_array($components) + ? \array_values($components) + : \iterator_to_array($components, false); + + $this->tags = $this->only(TagInterface::class); } /** diff --git a/libs/phpdoc/src/DocBlock/DocBlock.php b/libs/phpdoc/src/DocBlock/DocBlock.php index b5fbd1d..961dccb 100644 --- a/libs/phpdoc/src/DocBlock/DocBlock.php +++ b/libs/phpdoc/src/DocBlock/DocBlock.php @@ -33,7 +33,7 @@ * @template-implements \ArrayAccess * @template-implements \IteratorAggregate */ -final readonly class DocBlock implements +final class DocBlock implements ComponentInterface, \IteratorAggregate, \ArrayAccess, @@ -48,7 +48,7 @@ * value is empty. * * ``` - * foreach ($block->tags as $tag) { + * foreach ($block->getTags() as $tag) { * printf('@%s => %s', $tag->name, $tag->description); * } * ``` @@ -58,7 +58,7 @@ * * @var list */ - public array $tags; + public readonly array $tags; /** * @param iterable $tags list of all tags contained @@ -86,10 +86,12 @@ public function __construct( * echo $block->description; // "Sends a notification..." * ``` */ - public ?DescriptionInterface $description = null, + public readonly ?DescriptionInterface $description = null, iterable $tags = [], ) { - $this->tags = \iterator_to_array($tags, false); + $this->tags = \is_array($tags) + ? \array_values($tags) + : \iterator_to_array($tags, false); } public function offsetExists(mixed $offset): bool @@ -156,7 +158,7 @@ public function __toString(): string $result[] = (string) $this->description; } - foreach ($this->tags as $tag) { + foreach ($this->getTags() as $tag) { $result[] = (string) $tag; } diff --git a/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php b/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php index d833bcd..ad67490 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php @@ -7,7 +7,7 @@ /** * A reference to a constant of a class. */ -final readonly class ClassConstantReference extends CodeReference +final class ClassConstantReference extends CodeReference { public function __construct( /** @@ -15,13 +15,13 @@ public function __construct( * * @var non-empty-string */ - public string $class, + public readonly string $class, /** * The name of the referenced constant. * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php b/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php index 30ba4b4..ac84c11 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php @@ -7,7 +7,7 @@ /** * A reference to a method of a class. */ -final readonly class ClassMethodReference extends CodeReference +final class ClassMethodReference extends CodeReference { public function __construct( /** @@ -15,13 +15,13 @@ public function __construct( * * @var non-empty-string */ - public string $class, + public readonly string $class, /** * The name of the referenced method. * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php b/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php index b11a0b4..d3a40b5 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php @@ -7,7 +7,7 @@ /** * A reference to a property of a class. */ -final readonly class ClassPropertyReference extends CodeReference +final class ClassPropertyReference extends CodeReference { public function __construct( /** @@ -15,13 +15,13 @@ public function __construct( * * @var non-empty-string */ - public string $class, + public readonly string $class, /** * The name of the referenced property, without the leading "$". * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/CodeReference.php b/libs/phpdoc/src/DocBlock/Reference/CodeReference.php index c69b75e..1efe5a4 100644 --- a/libs/phpdoc/src/DocBlock/Reference/CodeReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/CodeReference.php @@ -11,9 +11,9 @@ * References of this kind are always considered internal, so they are never * external. */ -abstract readonly class CodeReference implements ReferenceInterface +abstract class CodeReference implements ReferenceInterface { - public bool $isExternal; + public readonly bool $isExternal; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php b/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php index 17eb0e1..3c61f19 100644 --- a/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php @@ -7,7 +7,7 @@ /** * A reference to a function. */ -final readonly class FunctionReference extends CodeReference +final class FunctionReference extends CodeReference { public function __construct( /** @@ -15,7 +15,7 @@ public function __construct( * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php b/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php index ceca2fa..a8069c4 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php +++ b/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php @@ -9,14 +9,10 @@ * * The target may either be an element of the described codebase or an * external resource located outside of it. + * + * @property-read bool $isExternal Gets {@see true} in case of the reference points outside of the described + * codebase, such as an external web page. */ interface ReferenceInterface extends \Stringable { - /** - * Gets {@see true} when the reference points to a resource located - * outside of the described codebase, such as an external web page. - */ - public bool $isExternal { - get; - } } diff --git a/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php b/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php index 4e1ca57..1ad3449 100644 --- a/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php @@ -8,7 +8,7 @@ * A reference to a named symbol, such as a class, interface, trait, enum * or a global constant. */ -final readonly class SymbolReference extends CodeReference +final class SymbolReference extends CodeReference { public function __construct( /** @@ -16,7 +16,7 @@ public function __construct( * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/TypeReference.php b/libs/phpdoc/src/DocBlock/Reference/TypeReference.php index 035584a..ab443cc 100644 --- a/libs/phpdoc/src/DocBlock/Reference/TypeReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/TypeReference.php @@ -9,18 +9,18 @@ /** * A parsed type together with the exact source text it was read from. */ -final readonly class TypeReference implements ReferenceInterface +final class TypeReference implements ReferenceInterface { - public bool $isExternal; + public readonly bool $isExternal; public function __construct( - public TypeNode $type, + public readonly TypeNode $type, /** * The original type text, exactly as it appeared in the source. * * @var non-empty-string */ - public string $source, + public readonly string $source, ) { $this->isExternal = false; } diff --git a/libs/phpdoc/src/DocBlock/Reference/UriReference.php b/libs/phpdoc/src/DocBlock/Reference/UriReference.php index 0c2a6b4..3a1deab 100644 --- a/libs/phpdoc/src/DocBlock/Reference/UriReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/UriReference.php @@ -11,9 +11,9 @@ * Such a reference always points outside the described codebase, so it * is always external. */ -readonly class UriReference implements ReferenceInterface +class UriReference implements ReferenceInterface { - public bool $isExternal; + public readonly bool $isExternal; public function __construct( /** @@ -21,7 +21,7 @@ public function __construct( * * @var non-empty-string */ - public string $uri, + public readonly string $uri, ) { $this->isExternal = true; } diff --git a/libs/phpdoc/src/DocBlock/Reference/UrlReference.php b/libs/phpdoc/src/DocBlock/Reference/UrlReference.php index 370504a..97228bd 100644 --- a/libs/phpdoc/src/DocBlock/Reference/UrlReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/UrlReference.php @@ -8,4 +8,4 @@ * A reference to an external resource identified by a URL, that is a URI that * carries a scheme. */ -final readonly class UrlReference extends UriReference {} +final class UrlReference extends UriReference {} diff --git a/libs/phpdoc/src/DocBlock/Reference/VariableReference.php b/libs/phpdoc/src/DocBlock/Reference/VariableReference.php index f5ad74c..5a1da14 100644 --- a/libs/phpdoc/src/DocBlock/Reference/VariableReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/VariableReference.php @@ -7,7 +7,7 @@ /** * A reference to a variable. */ -final readonly class VariableReference extends CodeReference +final class VariableReference extends CodeReference { public function __construct( /** @@ -15,7 +15,7 @@ public function __construct( * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php index 1a46a0d..b7e8ff3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php @@ -20,7 +20,7 @@ */ final class AbstractTagDefinition extends TagDefinition { - public const string NAME = 'abstract'; + public const NAME = 'abstract'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php index 67793e9..c062479 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php @@ -22,7 +22,7 @@ */ final class AccessTagDefinition extends TagDefinition { - public const string NAME = 'access'; + public const NAME = 'access'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php index 788323a..de0db8f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php @@ -21,7 +21,7 @@ */ final class AllowPrivateMutationTagDefinition extends TagDefinition { - public const string NAME = 'allow-private-mutation'; + public const NAME = 'allow-private-mutation'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php index 0753cd5..9ef3a79 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php @@ -21,7 +21,7 @@ */ final class ApiTagDefinition extends TagDefinition { - public const string NAME = 'api'; + public const NAME = 'api'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php index 5268326..8bcd4a3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php @@ -24,7 +24,7 @@ */ final class AssertIfFalseTagDefinition extends TagDefinition { - public const string NAME = 'assert-if-false'; + public const NAME = 'assert-if-false'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php index 742049d..ec92dcb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php @@ -24,7 +24,7 @@ */ final class AssertIfTrueTagDefinition extends TagDefinition { - public const string NAME = 'assert-if-true'; + public const NAME = 'assert-if-true'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php index 0f3d007..44a85ca 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php @@ -24,7 +24,7 @@ */ final class AssertTagDefinition extends TagDefinition { - public const string NAME = 'assert'; + public const NAME = 'assert'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php index 92c97b9..95f3c3d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php @@ -23,7 +23,7 @@ */ final class AuthorTagDefinition extends TagDefinition { - public const string NAME = 'author'; + public const NAME = 'author'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php index 84eb4b4..9d67223 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php @@ -20,7 +20,7 @@ */ final class CategoryTagDefinition extends TagDefinition { - public const string NAME = 'category'; + public const NAME = 'category'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php index 8d7da6a..a019ba7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php @@ -13,9 +13,10 @@ */ abstract class CheckTypeTag extends Tag { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -30,6 +31,8 @@ public function __construct( */ protected readonly TypeReference $statement, ) { + $this->type = $statement->type; + parent::__construct($name); } diff --git a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php index c13319f..8c486f0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php @@ -16,7 +16,7 @@ */ final class PsalmCheckTypeExactTagDefinition extends CheckTypeTagDefinition { - public const string NAME = 'psalm-check-type-exact'; + public const NAME = 'psalm-check-type-exact'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php index 9adeda8..952cc2c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php @@ -16,7 +16,7 @@ */ final class PsalmCheckTypeTagDefinition extends CheckTypeTagDefinition { - public const string NAME = 'psalm-check-type'; + public const NAME = 'psalm-check-type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php index 65185d4..d6d98e6 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php @@ -22,7 +22,7 @@ */ final class CodingStandardsIgnoreEndTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreEnd'; + public const NAME = 'codingStandardsIgnoreEnd'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php index 5104d5b..9ccf066 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php @@ -21,7 +21,7 @@ */ final class CodingStandardsIgnoreFileTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreFile'; + public const NAME = 'codingStandardsIgnoreFile'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php index fb4c8ef..cbea03d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php @@ -21,7 +21,7 @@ */ final class CodingStandardsIgnoreLineTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreLine'; + public const NAME = 'codingStandardsIgnoreLine'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php index c5670a1..e882441 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php @@ -22,7 +22,7 @@ */ final class CodingStandardsIgnoreStartTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreStart'; + public const NAME = 'codingStandardsIgnoreStart'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php index 3d918f3..8ad792e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php @@ -21,7 +21,7 @@ */ final class CodingStandardsTagDefinition extends TagDefinition { - public const string NAME = 'codingStandards'; + public const NAME = 'codingStandards'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php index 2fed870..918a432 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php @@ -21,7 +21,7 @@ */ final class ConsistentConstructorTagDefinition extends TagDefinition { - public const string NAME = 'consistent-constructor'; + public const NAME = 'consistent-constructor'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php index 96aaa45..bdf5dfb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php @@ -20,7 +20,7 @@ */ final class CopyrightTagDefinition extends TagDefinition { - public const string NAME = 'copyright'; + public const NAME = 'copyright'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php index fbd8989..8a44383 100644 --- a/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php @@ -22,7 +22,7 @@ */ final class DeprecatedTagDefinition extends TagDefinition { - public const string NAME = 'deprecated'; + public const NAME = 'deprecated'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php index 6f5a1d8..b7b072f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php @@ -25,7 +25,7 @@ */ final class ExampleTagDefinition extends TagDefinition { - public const string NAME = 'example'; + public const NAME = 'example'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php index 3aa3b74..40fb4e3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php @@ -23,7 +23,7 @@ */ final class ExpectedExceptionTagDefinition extends TagDefinition { - public const string NAME = 'expectedException'; + public const NAME = 'expectedException'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php index 423a45a..234af14 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php @@ -21,7 +21,7 @@ */ final class FilesourceTagDefinition extends TagDefinition { - public const string NAME = 'filesource'; + public const NAME = 'filesource'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php index a8308f3..b98ff05 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php @@ -21,7 +21,7 @@ */ final class FinalTagDefinition extends TagDefinition { - public const string NAME = 'final'; + public const NAME = 'final'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php index 543e06b..bec728d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php @@ -22,7 +22,7 @@ */ final class FormatterOffTagDefinition extends TagDefinition { - public const string NAME = 'formatter:off'; + public const NAME = 'formatter:off'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php index 6720214..f8c5024 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php @@ -21,7 +21,7 @@ */ final class FormatterOnTagDefinition extends TagDefinition { - public const string NAME = 'formatter:on'; + public const NAME = 'formatter:on'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php index bda1c5e..aef8434 100644 --- a/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php @@ -13,7 +13,7 @@ final class GenericTagDefinition extends TagDefinition { - public const string NAME = ''; + public const NAME = ''; /** * @param TagPlacement $placement generic (unknown) tag has no dedicated diff --git a/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php index 2c1d04e..566a026 100644 --- a/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php @@ -23,7 +23,7 @@ */ final class GlobalTagDefinition extends TagDefinition { - public const string NAME = 'global'; + public const NAME = 'global'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php index 523215f..e6d2829 100644 --- a/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php @@ -21,7 +21,7 @@ */ final class IgnoreTagDefinition extends TagDefinition { - public const string NAME = 'ignore'; + public const NAME = 'ignore'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php index b0f94dd..20f9b6a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php @@ -21,7 +21,7 @@ */ final class ImmutableTagDefinition extends TagDefinition { - public const string NAME = 'immutable'; + public const NAME = 'immutable'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php index 556274d..0aa46f5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php @@ -14,9 +14,10 @@ */ final class ImportTypeAliasTag extends Tag { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -37,6 +38,8 @@ public function __construct( */ public readonly ?string $as = null, ) { + $this->type = $statement->type; + parent::__construct($name); } diff --git a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php index c8b3219..cd1a2d7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php @@ -22,7 +22,7 @@ */ final class ImportTypeAliasTagDefinition extends TagDefinition { - public const string NAME = 'import-type'; + public const NAME = 'import-type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php index dfb02b9..1b99d3c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php @@ -20,7 +20,7 @@ */ final class InheritDocTagDefinition extends TagDefinition { - public const string NAME = 'inheritDoc'; + public const NAME = 'inheritDoc'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php index 230c40f..c2b6790 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php @@ -19,7 +19,7 @@ */ final class ExtendsTagDefinition extends InheritanceTagDefinition { - public const string NAME = 'extends'; + public const NAME = 'extends'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php index bb3ddd6..fbedd34 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php @@ -18,7 +18,7 @@ */ final class ImplementsTagDefinition extends InheritanceTagDefinition { - public const string NAME = 'implements'; + public const NAME = 'implements'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php index 83b4756..cf59e52 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php @@ -18,7 +18,7 @@ */ final class UseTagDefinition extends InheritanceTagDefinition { - public const string NAME = 'use'; + public const NAME = 'use'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php index 3f65f81..a0bd5b5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php @@ -21,7 +21,7 @@ */ final class InternalTagDefinition extends TagDefinition { - public const string NAME = 'internal'; + public const NAME = 'internal'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php index 9a8d454..9a2d664 100644 --- a/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php @@ -23,7 +23,7 @@ */ final class LanguageTagDefinition extends TagDefinition { - public const string NAME = 'language'; + public const NAME = 'language'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php index 1023555..684316b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php @@ -24,7 +24,7 @@ */ final class LicenseTagDefinition extends TagDefinition { - public const string NAME = 'license'; + public const NAME = 'license'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php index 1326ceb..2b2133e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php @@ -30,7 +30,7 @@ */ final class LinkTagDefinition extends TagDefinition { - public const string NAME = 'link'; + public const NAME = 'link'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php index 7a96379..60aa619 100644 --- a/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php @@ -28,7 +28,7 @@ */ final class MethodTagDefinition extends TagDefinition { - public const string NAME = 'method'; + public const NAME = 'method'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php index 7f710b6..5a7fcf2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php @@ -23,7 +23,7 @@ */ final class MixinTagDefinition extends TagDefinition { - public const string NAME = 'mixin'; + public const NAME = 'mixin'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php index f23cbcb..021bf23 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php @@ -21,7 +21,7 @@ */ final class NameTagDefinition extends TagDefinition { - public const string NAME = 'name'; + public const NAME = 'name'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php index 3f90e21..09d804b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php @@ -21,7 +21,7 @@ */ final class NoNamedArgumentsTagDefinition extends TagDefinition { - public const string NAME = 'no-named-arguments'; + public const NAME = 'no-named-arguments'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php index e4fb5ae..3ba4c21 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php @@ -22,7 +22,7 @@ */ final class NoinspectionTagDefinition extends TagDefinition { - public const string NAME = 'noinspection'; + public const NAME = 'noinspection'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php index e07ddb6..bf6a953 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php @@ -21,7 +21,7 @@ */ final class NotDeprecatedTagDefinition extends TagDefinition { - public const string NAME = 'not-deprecated'; + public const NAME = 'not-deprecated'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php index 4c825d0..d4c95e2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php @@ -20,7 +20,7 @@ */ final class OverrideTagDefinition extends TagDefinition { - public const string NAME = 'override'; + public const NAME = 'override'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php index 32218eb..73224c0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php @@ -20,7 +20,7 @@ */ final class PackageTagDefinition extends TagDefinition { - public const string NAME = 'package'; + public const NAME = 'package'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php index e5fed4f..e3ea085 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php @@ -24,7 +24,7 @@ */ final class ParamClosureThisTagDefinition extends TagDefinition { - public const string NAME = 'param-closure-this'; + public const NAME = 'param-closure-this'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php index b55daa2..25ab37f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php @@ -16,7 +16,7 @@ */ final class ParamImmediatelyInvokedCallableTagDefinition extends ParamInvokedCallableTagDefinition { - public const string NAME = 'param-immediately-invoked-callable'; + public const NAME = 'param-immediately-invoked-callable'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php index a5493bd..223bfb7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php @@ -16,7 +16,7 @@ */ final class ParamLaterInvokedCallableTagDefinition extends ParamInvokedCallableTagDefinition { - public const string NAME = 'param-later-invoked-callable'; + public const NAME = 'param-later-invoked-callable'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php index 2e604e4..a4e9765 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php @@ -24,7 +24,7 @@ */ final class ParamOutTagDefinition extends TagDefinition { - public const string NAME = 'param-out'; + public const NAME = 'param-out'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php index 9eb794b..b736349 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php @@ -26,7 +26,7 @@ */ final class ParamTagDefinition extends TagDefinition { - public const string NAME = 'param'; + public const NAME = 'param'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php index 40c0ed7..9269a5a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php @@ -24,7 +24,7 @@ */ final class PhanClosureScopeTagDefinition extends TagDefinition { - public const string NAME = 'phan-closure-scope'; + public const NAME = 'phan-closure-scope'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php index 85db0ba..fb71dc0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanConstructorUsedForSideEffectsTagDefinition extends TagDefinition { - public const string NAME = 'phan-constructor-used-for-side-effects'; + public const NAME = 'phan-constructor-used-for-side-effects'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php index 1f0b868..29bc1c1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanFileSuppressTagDefinition extends TagDefinition { - public const string NAME = 'phan-file-suppress'; + public const NAME = 'phan-file-suppress'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php index c5ba0ba..ab65384 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanForbidUndeclaredMagicMethodsTagDefinition extends TagDefinition { - public const string NAME = 'phan-forbid-undeclared-magic-methods'; + public const NAME = 'phan-forbid-undeclared-magic-methods'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php index fac4c0b..9fb76d0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanForbidUndeclaredMagicPropertiesTagDefinition extends TagDefinition { - public const string NAME = 'phan-forbid-undeclared-magic-properties'; + public const NAME = 'phan-forbid-undeclared-magic-properties'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php index 7f14d6d..77c74d5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php @@ -23,7 +23,7 @@ */ final class PhanHardcodeReturnTypeTagDefinition extends TagDefinition { - public const string NAME = 'phan-hardcode-return-type'; + public const NAME = 'phan-hardcode-return-type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php index cf21336..f3d3a4e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanOutputReferenceTagDefinition extends TagDefinition { - public const string NAME = 'phan-output-reference'; + public const NAME = 'phan-output-reference'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php index e7808f2..919b5bc 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php @@ -23,7 +23,7 @@ */ final class PhanRealReturnTagDefinition extends TagDefinition { - public const string NAME = 'phan-real-return'; + public const NAME = 'phan-real-return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php index 869975a..5c2ed04 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanSideEffectFreeTagDefinition extends TagDefinition { - public const string NAME = 'phan-side-effect-free'; + public const NAME = 'phan-side-effect-free'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php index 4b2ec7e..523c22b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressCurrentLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-current-line'; + public const NAME = 'phan-suppress-current-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php index 6b6cabe..63d7fbb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressNextLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-next-line'; + public const NAME = 'phan-suppress-next-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php index f35469f..4f4d7f5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressNextNextLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-next-next-line'; + public const NAME = 'phan-suppress-next-next-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php index dd853d1..d6a7384 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressPreviousLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-previous-line'; + public const NAME = 'phan-suppress-previous-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php index 0790961..58ba93f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanTransientTagDefinition extends TagDefinition { - public const string NAME = 'phan-transient'; + public const NAME = 'phan-transient'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php index a2d8f99..6308893 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanWriteOnlyTagDefinition extends TagDefinition { - public const string NAME = 'phan-write-only'; + public const NAME = 'phan-write-only'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php index c5af3c7..cd861db 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhpStanIgnoreLineTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-ignore-line'; + public const NAME = 'phpstan-ignore-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php index d150313..c6f1643 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhpStanIgnoreNextLineTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-ignore-next-line'; + public const NAME = 'phpstan-ignore-next-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php index 00441a8..bf8584c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhpStanIgnoreTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-ignore'; + public const NAME = 'phpstan-ignore'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php index e1606a8..6c23088 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhpStanImpureTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-impure'; + public const NAME = 'phpstan-impure'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php index aace17f..77fd03c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhpcsSuppressTagDefinition extends TagDefinition { - public const string NAME = 'phpcsSuppress'; + public const NAME = 'phpcsSuppress'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php index 1cf0bb8..f0ccc8b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php @@ -16,7 +16,7 @@ */ final class PropertyReadTagDefinition extends MagicPropertyTagDefinition { - public const string NAME = 'property-read'; + public const NAME = 'property-read'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php index 7882269..0a9d315 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php @@ -17,7 +17,7 @@ */ final class PropertyTagDefinition extends MagicPropertyTagDefinition { - public const string NAME = 'property'; + public const NAME = 'property'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php index 1aba310..222cbdb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php @@ -17,7 +17,7 @@ */ final class PropertyWriteTagDefinition extends MagicPropertyTagDefinition { - public const string NAME = 'property-write'; + public const NAME = 'property-write'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php index 8937731..fa18884 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmAssertUntaintedTagDefinition extends TagDefinition { - public const string NAME = 'psalm-assert-untainted'; + public const NAME = 'psalm-assert-untainted'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php index 0a7f6fc..073c954 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmConsistentTemplatesTagDefinition extends TagDefinition { - public const string NAME = 'psalm-consistent-templates'; + public const NAME = 'psalm-consistent-templates'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php index 36880b6..39087a9 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmExternalMutationFreeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-external-mutation-free'; + public const NAME = 'psalm-external-mutation-free'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php index cb952ea..1ec95f0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmFlowTagDefinition extends TagDefinition { - public const string NAME = 'psalm-flow'; + public const NAME = 'psalm-flow'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php index f344eb0..363b4e8 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php @@ -23,7 +23,7 @@ */ final class PsalmIfThisIsTagDefinition extends TagDefinition { - public const string NAME = 'psalm-if-this-is'; + public const NAME = 'psalm-if-this-is'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php index 22eff7a..c3dc99a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreFalsableReturnTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-falsable-return'; + public const NAME = 'psalm-ignore-falsable-return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php index 561675d..d1e92a6 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreNullableReturnTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-nullable-return'; + public const NAME = 'psalm-ignore-nullable-return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php index 484db1a..c23ad07 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmIgnoreVarTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-var'; + public const NAME = 'psalm-ignore-var'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php index 70cd375..3274744 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreVariableMethodTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-variable-method'; + public const NAME = 'psalm-ignore-variable-method'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php index e23fd15..b1054e8 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreVariablePropertyTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-variable-property'; + public const NAME = 'psalm-ignore-variable-property'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php index a5584c0..c7ea26c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php @@ -23,7 +23,7 @@ */ final class PsalmInheritorsTagDefinition extends TagDefinition { - public const string NAME = 'psalm-inheritors'; + public const NAME = 'psalm-inheritors'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php index 9553454..21d3950 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmMutationFreeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-mutation-free'; + public const NAME = 'psalm-mutation-free'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php index d68c2ac..bfcc2bf 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmNoSealMethodsTagDefinition extends TagDefinition { - public const string NAME = 'psalm-no-seal-methods'; + public const NAME = 'psalm-no-seal-methods'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php index a946906..fc4ccba 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmNoSealPropertiesTagDefinition extends TagDefinition { - public const string NAME = 'psalm-no-seal-properties'; + public const NAME = 'psalm-no-seal-properties'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php index 351cc6f..79775b0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmOverrideMethodVisibilityTagDefinition extends TagDefinition { - public const string NAME = 'psalm-override-method-visibility'; + public const NAME = 'psalm-override-method-visibility'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php index 225eaff..ff52009 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmOverridePropertyVisibilityTagDefinition extends TagDefinition { - public const string NAME = 'psalm-override-property-visibility'; + public const NAME = 'psalm-override-property-visibility'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php index 369f048..ee5d871 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php @@ -24,7 +24,7 @@ */ final class PsalmScopeThisTagDefinition extends TagDefinition { - public const string NAME = 'psalm-scope-this'; + public const NAME = 'psalm-scope-this'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php index 56ecc02..6265e48 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmStubOverrideTagDefinition extends TagDefinition { - public const string NAME = 'psalm-stub-override'; + public const NAME = 'psalm-stub-override'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php index 875ab70..40b5e05 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTaintEscapeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-escape'; + public const NAME = 'psalm-taint-escape'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php index 7773e8c..de3ddc1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmTaintSinkTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-sink'; + public const NAME = 'psalm-taint-sink'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php index a8f1786..11c6b09 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTaintSourceTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-source'; + public const NAME = 'psalm-taint-source'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php index 6035945..e388169 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTaintSpecializeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-specialize'; + public const NAME = 'psalm-taint-specialize'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php index b79bd9e..512dde1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php @@ -23,7 +23,7 @@ */ final class PsalmTaintUnescapeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-unescape'; + public const NAME = 'psalm-taint-unescape'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php index 8e10a62..74fe186 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTraceTagDefinition extends TagDefinition { - public const string NAME = 'psalm-trace'; + public const NAME = 'psalm-trace'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php index dfe234a..2f5ebd3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmVariadicTagDefinition extends TagDefinition { - public const string NAME = 'psalm-variadic'; + public const NAME = 'psalm-variadic'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php index 37dc3fb..85ac240 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php @@ -22,7 +22,7 @@ */ final class PureTagDefinition extends TagDefinition { - public const string NAME = 'pure'; + public const NAME = 'pure'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php index 60910a0..b32df9a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php @@ -21,7 +21,7 @@ */ final class PureUnlessCallableIsImpureTagDefinition extends TagDefinition { - public const string NAME = 'pure-unless-callable-is-impure'; + public const NAME = 'pure-unless-callable-is-impure'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php index 388b6d6..8a1eb86 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php @@ -22,7 +22,7 @@ */ final class ReadonlyAllowPrivateMutationTagDefinition extends TagDefinition { - public const string NAME = 'readonly-allow-private-mutation'; + public const NAME = 'readonly-allow-private-mutation'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php index 41a3255..6ce91fb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php @@ -21,7 +21,7 @@ */ final class ReadonlyTagDefinition extends TagDefinition { - public const string NAME = 'readonly'; + public const NAME = 'readonly'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php index a947398..39c53ad 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php @@ -10,15 +10,9 @@ * A tag that points to another element or an external resource. * * @template-covariant TReference of ReferenceInterface = ReferenceInterface + * + * @property-read ReferenceInterface $reference The reference the tag points to. */ interface ReferencedTagInterface extends TagInterface { - /** - * The target the tag points to. - * - * @var TReference - */ - public ReferenceInterface $reference { - get; - } } diff --git a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php index 0347f7f..275f2fb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php @@ -17,7 +17,7 @@ */ final class RequireExtendsTagDefinition extends RequireInheritanceTagDefinition { - public const string NAME = 'require-extends'; + public const NAME = 'require-extends'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php index ec58c89..e3e1ab1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php @@ -17,7 +17,7 @@ */ final class RequireImplementsTagDefinition extends RequireInheritanceTagDefinition { - public const string NAME = 'require-implements'; + public const NAME = 'require-implements'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php index 6531469..b07ad9e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php @@ -26,7 +26,7 @@ */ final class ReturnTagDefinition extends TagDefinition { - public const string NAME = 'return'; + public const NAME = 'return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php index c7ed113..2059ae1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php @@ -21,7 +21,7 @@ */ final class SealMethodsTagDefinition extends TagDefinition { - public const string NAME = 'seal-methods'; + public const NAME = 'seal-methods'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php index a8d797c..46786be 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php @@ -21,7 +21,7 @@ */ final class SealPropertiesTagDefinition extends TagDefinition { - public const string NAME = 'seal-properties'; + public const NAME = 'seal-properties'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php index 81ef1d9..e98154b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php @@ -37,7 +37,7 @@ */ final class SeeTagDefinition extends TagDefinition { - public const string NAME = 'see'; + public const NAME = 'see'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php index 2ce9d78..eeaff05 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php @@ -24,7 +24,7 @@ */ final class SelfOutTagDefinition extends TagDefinition { - public const string NAME = 'self-out'; + public const NAME = 'self-out'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php index e17d06a..f6caae5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php @@ -22,7 +22,7 @@ */ final class SinceTagDefinition extends TagDefinition { - public const string NAME = 'since'; + public const NAME = 'since'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php index 01559b5..829d85c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php @@ -22,7 +22,7 @@ */ final class SourceTagDefinition extends TagDefinition { - public const string NAME = 'source'; + public const NAME = 'source'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php index 4030a94..bc0005d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php @@ -20,7 +20,7 @@ */ final class StaticTagDefinition extends TagDefinition { - public const string NAME = 'static'; + public const NAME = 'static'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php index 57a69c8..d8655f9 100644 --- a/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php @@ -24,7 +24,7 @@ */ final class StaticVarTagDefinition extends TagDefinition { - public const string NAME = 'staticvar'; + public const NAME = 'staticvar'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php index 30d0672..f2d313a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php @@ -21,7 +21,7 @@ */ final class SubpackageTagDefinition extends TagDefinition { - public const string NAME = 'subpackage'; + public const NAME = 'subpackage'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php index aa48547..983ccfe 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php @@ -22,7 +22,7 @@ */ final class SuppressTagDefinition extends TagDefinition { - public const string NAME = 'suppress'; + public const NAME = 'suppress'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TagInterface.php b/libs/phpdoc/src/DocBlock/Tag/TagInterface.php index 6a37d71..f88cf09 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/TagInterface.php @@ -9,25 +9,11 @@ /** * Representation of the phpdoc tag. + * + * @property-read non-empty-string $name Gets tag name string without the "@" prefix. + * @property-read ?DescriptionInterface $description Gets an optional description object or {@see null} in case of + * description is not defined in the entry. */ interface TagInterface extends ComponentInterface { - /** - * Gets tag name string without the '@' prefix. - * - * That is, can contain all the characters that can match the PHP FQN - * (Fully Qualified Name), as well as the '-' character. - */ - public string $name { - get; - } - - /** - * Gets an optional description object which can be represented as - * a {@see string} and contains additional information or {@see null} - * in case of description is not defined in the entry. - */ - public ?DescriptionInterface $description { - get; - } } diff --git a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php index ecacc8a..0783371 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php @@ -17,7 +17,7 @@ */ final class TemplateContravariantTagDefinition extends TypeParameterTagDefinition { - public const string NAME = 'template-contravariant'; + public const NAME = 'template-contravariant'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php index d32bbfb..b7a2100 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php @@ -16,7 +16,7 @@ */ final class TemplateCovariantTagDefinition extends TypeParameterTagDefinition { - public const string NAME = 'template-covariant'; + public const NAME = 'template-covariant'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php index 855e9e0..1cde344 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php @@ -16,7 +16,7 @@ */ final class TemplateTagDefinition extends TypeParameterTagDefinition { - public const string NAME = 'template'; + public const NAME = 'template'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php index 864be59..75e78b5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php @@ -26,7 +26,7 @@ */ final class ThrowsTagDefinition extends TagDefinition { - public const string NAME = 'throws'; + public const NAME = 'throws'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php index e7285fd..4d22eba 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php @@ -20,7 +20,7 @@ */ final class TodoTagDefinition extends TagDefinition { - public const string NAME = 'todo'; + public const NAME = 'todo'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php index 68fa19d..0fb247d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php @@ -13,9 +13,10 @@ */ final class TypeAliasTag extends Tag { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -30,6 +31,8 @@ public function __construct( */ protected readonly TypeReference $statement, ) { + $this->type = $statement->type; + parent::__construct($name); } diff --git a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php index e068398..2b4f9a7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php @@ -22,7 +22,7 @@ */ final class TypeAliasTagDefinition extends TagDefinition { - public const string NAME = 'type'; + public const NAME = 'type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TypedTag.php b/libs/phpdoc/src/DocBlock/Tag/TypedTag.php index 5a87a04..1e7d54a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypedTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypedTag.php @@ -13,9 +13,10 @@ */ abstract class TypedTag extends Tag implements TypedTagInterface { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -25,6 +26,8 @@ public function __construct( protected readonly TypeReference $statement, ?DescriptionInterface $description = null, ) { + $this->type = $statement->type; + parent::__construct($name, $description); } diff --git a/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php index e50efef..923aa1b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php @@ -8,13 +8,9 @@ /** * A tag that carries a single type. + * + * @property-read TypeNode $type The type declared by the tag. */ interface TypedTagInterface extends TagInterface { - /** - * The type declared by the tag. - */ - public TypeNode $type { - get; - } } diff --git a/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php index f695910..5076f08 100644 --- a/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php @@ -22,7 +22,7 @@ */ final class UnusedParamTagDefinition extends TagDefinition { - public const string NAME = 'unused-param'; + public const NAME = 'unused-param'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php index dd0d44b..2dab863 100644 --- a/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php @@ -23,7 +23,7 @@ */ final class UsedByTagDefinition extends TagDefinition { - public const string NAME = 'used-by'; + public const NAME = 'used-by'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php index 5070543..24159e0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php @@ -23,7 +23,7 @@ */ final class UsesTagDefinition extends TagDefinition { - public const string NAME = 'uses'; + public const NAME = 'uses'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php index a7ba1b5..4a921a2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php @@ -24,7 +24,7 @@ */ final class VarTagDefinition extends TagDefinition { - public const string NAME = 'var'; + public const NAME = 'var'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php index c948a82..ec9de9b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php @@ -6,15 +6,9 @@ /** * A tag that refers to a variable. + * + * @property-read non-empty-string $variable The referenced variable name, without the leading "$". */ interface VariableTagInterface extends TagInterface { - /** - * The referenced variable name, without the leading "$". - * - * @var non-empty-string - */ - public string $variable { - get; - } } diff --git a/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php index dab4fed..379d9d4 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php @@ -21,7 +21,7 @@ */ final class VersionTagDefinition extends TagDefinition { - public const string NAME = 'version'; + public const NAME = 'version'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php index 8d40141..ca1bfee 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php @@ -6,15 +6,9 @@ /** * A tag that carries an optional version. + * + * @property-read ?non-empty-string $version The version the tag refers to, if any. */ interface VersionedTagInterface extends TagInterface { - /** - * The version the tag refers to, or {@see null} when none was given. - * - * @var non-empty-string|null - */ - public ?string $version { - get; - } } diff --git a/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php index 870fc9c..4c7dda2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php @@ -23,7 +23,7 @@ */ final class YieldTagDefinition extends TagDefinition { - public const string NAME = 'yield'; + public const NAME = 'yield'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php b/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php index 9a45413..e57adba 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php @@ -12,7 +12,7 @@ use TypeLang\PhpDoc\Parser\Grammar\Rule\RuleInterface; use TypeLang\PhpDoc\Parser\Grammar\Rule\SequencingRule; -final readonly class Spec +final class Spec { /** * @param non-empty-string $value diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php b/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php index 5d1d654..6eef03c 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php @@ -12,36 +12,13 @@ * * The shape of its body (a {@see RuleInterface}) and how to build a {@see TagInterface} * from the parsed pieces. + * + * @property-read non-empty-string $name Canonical tag name. + * @property-read RuleInterface $spec Tag definition specification: shape of the tag body. + * @property-read TagPlacement $placement Where the tag may appear: only inline, only as a block tag, or anywhere. */ interface TagDefinitionInterface extends \Stringable { - /** - * Canonical tag name - * - * @var non-empty-string - */ - public string $name { - get; - } - - /** - * Tag definition specification: shape of the tag body. - */ - public RuleInterface $spec { - get; - } - - /** - * Where the tag may appear: only inline (inside a description as a - * "{@tag}" sequence), only as a block tag, or anywhere. - * - * A block-only tag (such as "@param") is never lifted out of a description: - * a "{@param}" written in running text stays raw text instead of being - * parsed as an inline tag. - */ - public TagPlacement $placement { - get; - } /** * Builds the tag from the values captured while matching {@see $spec}. diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php b/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php index a03a747..dc7eda2 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php @@ -16,7 +16,7 @@ * $result->getAll('parameter'); // every value captured under the alias * ``` */ -final readonly class TagPayload +final class TagPayload { public function __construct( /** @@ -24,7 +24,7 @@ public function __construct( * * @var array> */ - private array $captures = [], + private readonly array $captures = [], ) {} public function has(string $alias): bool diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php b/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php index 62c8f5f..fbbfa8d 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php @@ -27,5 +27,5 @@ enum TagPlacement /** * Placement assumed when a definition does not specify one. */ - public const self DEFAULT = self::Any; + public const DEFAULT = self::Any; } diff --git a/libs/phpdoc/src/DocBlockParser.php b/libs/phpdoc/src/DocBlockParser.php index 93a0dac..f0747d9 100644 --- a/libs/phpdoc/src/DocBlockParser.php +++ b/libs/phpdoc/src/DocBlockParser.php @@ -16,6 +16,7 @@ use TypeLang\PhpDoc\Parser\Description\DescriptionParserInterface; use TypeLang\PhpDoc\Parser\DocBlockAnalyzer; use TypeLang\PhpDoc\Parser\Grammar\CombinatorInterface; +use TypeLang\PhpDoc\Parser\Grammar\Cursor; use TypeLang\PhpDoc\Parser\Splitter\Segment; use TypeLang\PhpDoc\Parser\Splitter\SplitterInterface; use TypeLang\PhpDoc\Parser\Splitter\StringSplitter; @@ -35,15 +36,15 @@ /** * @phpstan-import-type CombinatorType from CombinatorInterface */ -final readonly class DocBlockParser implements DocBlockParserInterface +final class DocBlockParser implements DocBlockParserInterface { - public TagFactoryInterface $factory; + public readonly TagFactoryInterface $factory; - public TagRegistryInterface $tags; + public readonly TagRegistryInterface $tags; - private DocBlockAnalyzer $docBlockAnalyzer; - private TagParserInterface $tagParser; - private DescriptionParserInterface $descriptionParser; + private readonly DocBlockAnalyzer $docBlockAnalyzer; + private readonly TagParserInterface $tagParser; + private readonly DescriptionParserInterface $descriptionParser; /** * @param iterable $platforms additional tag platforms @@ -72,7 +73,9 @@ public static function createDefault(iterable $additionalPlatforms = []): self new PhanPlatform(), new PhpStormPlatform(), new PhpCodeSnifferPlatform(), - ...\iterator_to_array($additionalPlatforms, false), + ...(\is_array($additionalPlatforms) + ? \array_values($additionalPlatforms) + : \iterator_to_array($additionalPlatforms, false)), ]); } @@ -112,16 +115,16 @@ private function createTagRegistry(array $platforms): TagRegistryInterface $aliases = []; foreach ($platforms as $platform) { - foreach ($platform->tags as $name => $definition) { + foreach ($platform->getTags() as $name => $definition) { $definitions[$name] = $definition; } - foreach ($platform->aliases as $alias => $canonical) { + foreach ($platform->getAliases() as $alias => $canonical) { $aliases[$alias] = $canonical; } } - return new TagRegistryBuilder($definitions, $aliases) + return (new TagRegistryBuilder($definitions, $aliases)) ->build(); } @@ -134,16 +137,24 @@ private function createCombinators(array $platforms): array $combinators = []; foreach ($platforms as $platform) { - foreach ($platform->combinators as $name => $combinator) { + foreach ($platform->getCombinators() as $name => $combinator) { $combinators[$name] = $combinator; } } - // Description is always present and cannot be redefined - $combinators[DescriptionCombinator::NAME] = new \ReflectionClass(DescriptionCombinator::class) - ->newLazyProxy(fn(): DescriptionCombinator => new DescriptionCombinator( + $description = null; + + // Description is always present and cannot be redefined. + // + // The combinator is created on the first call, because the description + // parser is not available at the moment of the combinators creation. + $combinators[DescriptionCombinator::NAME] = function (Cursor $cursor) use (&$description): mixed { + $description ??= new DescriptionCombinator( descriptionParser: $this->descriptionParser, - )); + ); + + return $description($cursor); + }; return $combinators; } diff --git a/libs/phpdoc/src/Exception/ParsingExceptionInterface.php b/libs/phpdoc/src/Exception/ParsingExceptionInterface.php index 98399b2..5057d71 100644 --- a/libs/phpdoc/src/Exception/ParsingExceptionInterface.php +++ b/libs/phpdoc/src/Exception/ParsingExceptionInterface.php @@ -6,22 +6,10 @@ /** * Error occurring while processing phpdoc content. + * + * @property-read string $source Gets the full docblock content in which the error occurred. + * @property-read int<0, max> $offset Gets the offset at which the error occurred. */ interface ParsingExceptionInterface extends PhpDocExceptionInterface { - /** - * Gets the full docblock content in which the error occurred. - */ - public string $source { - get; - } - - /** - * Gets the byte offset at the location where the error occurs. - * - * @var int<0, max> - */ - public int $offset { - get; - } } diff --git a/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php b/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php index d496897..b7b15a5 100644 --- a/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php +++ b/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php @@ -18,19 +18,19 @@ * * An inline tag is any `{@...}` sequence whose curly braces are balanced. */ -final readonly class BalancedBraceAwareParser implements DescriptionParserInterface +final class BalancedBraceAwareParser implements DescriptionParserInterface { - private const string INLINE_TAG_START_SEQUENCE = '{@'; + private const INLINE_TAG_START_SEQUENCE = '{@'; - private const string NESTING_INC_CHAR = '{'; - private const string NESTING_DEC_CHAR = '}'; + private const NESTING_INC_CHAR = '{'; + private const NESTING_DEC_CHAR = '}'; - private const string NESTING_CHARS + private const NESTING_CHARS = self::NESTING_INC_CHAR . self::NESTING_DEC_CHAR; public function __construct( - private TagParserInterface $tagParser, + private readonly TagParserInterface $tagParser, ) {} public function tryParse(string $description): ?DescriptionInterface diff --git a/libs/phpdoc/src/Parser/DocBlockAnalyzer.php b/libs/phpdoc/src/Parser/DocBlockAnalyzer.php index ec2d595..5ef36e0 100644 --- a/libs/phpdoc/src/Parser/DocBlockAnalyzer.php +++ b/libs/phpdoc/src/Parser/DocBlockAnalyzer.php @@ -14,10 +14,10 @@ * A line opening with "@" starts a new tag; any line before the first tag * belongs to the description, and any non-tag line after a tag continues it. */ -final readonly class DocBlockAnalyzer +final class DocBlockAnalyzer { public function __construct( - private SplitterInterface $splitter, + private readonly SplitterInterface $splitter, ) {} public function analyze(string $docblock): RawDocBlock diff --git a/libs/phpdoc/src/Parser/Grammar/Context.php b/libs/phpdoc/src/Parser/Grammar/Context.php index 4d145fc..a355292 100644 --- a/libs/phpdoc/src/Parser/Grammar/Context.php +++ b/libs/phpdoc/src/Parser/Grammar/Context.php @@ -37,7 +37,7 @@ public function capture(string $alias, mixed $value): void */ public function mark(): array { - return [$this->cursor->position, \count($this->captures)]; + return [$this->cursor->getPosition(), \count($this->captures)]; } /** @@ -50,7 +50,7 @@ public function rollback(array $snapshot): void { [$position, $length] = $snapshot; - $this->cursor->position = $position; + $this->cursor->moveTo($position); if (\count($this->captures) > $length) { $this->captures = \array_slice($this->captures, 0, $length); diff --git a/libs/phpdoc/src/Parser/Grammar/Cursor.php b/libs/phpdoc/src/Parser/Grammar/Cursor.php index acd49b1..269c329 100644 --- a/libs/phpdoc/src/Parser/Grammar/Cursor.php +++ b/libs/phpdoc/src/Parser/Grammar/Cursor.php @@ -8,15 +8,15 @@ * A reading position over a tag suffix that grammar rules consume from. * * The reading methods cover the shapes a rule usually needs (a word, an - * identifier, a literal, a run of characters), so a rule rarely has to touch - * {@see $position} by hand that is reserved for rewinding a speculative match. + * identifier, a literal, a run of characters), so a rule rarely has to call + * {@see moveTo()} by hand that is reserved for rewinding a speculative match. */ final class Cursor { /** * The whitespace bytes that separate words. */ - private const string CHARS_WHITESPACE = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"; + private const CHARS_WHITESPACE = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"; /** * @var int<0, max> @@ -24,8 +24,8 @@ final class Cursor private readonly int $length; /** - * The furthest position ever reached, reported by {@see $furthestOffset} - * when a match fails. + * The furthest position ever reached, reported by + * {@see getFurthestOffset()} when a match fails. * * @var int<0, max> */ @@ -34,21 +34,12 @@ final class Cursor /** * The current position within the buffer. * - * Reading advances it; assigning rewinds it to roll a speculative match - * back. Whatever it is set to, the furthest position reached is remembered - * for failure reporting. + * Reading advances it, {@see moveTo()} rewinds it to roll a speculative + * match back. * * @var int<0, max> */ - public int $position = 0 { - set(int $position) { - $this->position = $position; - - if ($position > $this->furthest) { - $this->furthest = $position; - } - } - } + private int $current = 0; /** * List of PHP identifier chars as string @@ -71,26 +62,54 @@ public function __construct( self::$phpIdentifierChars ??= self::createIdentifierChars(); } + /** + * The current position within the buffer. + * + * @return int<0, max> + */ + public function getPosition(): int + { + return $this->current; + } + + /** + * Rewinds the cursor to roll a speculative match back. Whatever it is set + * to, the furthest position reached is remembered for failure reporting. + * + * @param int<0, max> $position + */ + public function moveTo(int $position): void + { + $this->current = $position; + + if ($position > $this->furthest) { + $this->furthest = $position; + } + } + /** * The current byte offset within the source. * - * @var int<0, max> + * @return int<0, max> */ - public int $offset { - get => $this->base + $this->position; + public function getOffset(): int + { + return $this->base + $this->current; } /** * The byte offset to report when a match fails. * - * @var int<0, max> + * @return int<0, max> */ - public int $furthestOffset { - get => $this->base + $this->furthest; + public function getFurthestOffset(): int + { + return $this->base + $this->furthest; } - public bool $isEof { - get => $this->position >= $this->length; + public function isEof(): bool + { + return $this->current >= $this->length; } /** @@ -98,7 +117,7 @@ public function __construct( */ public function peek(int $length = 1): string { - return \substr($this->buffer, $this->position, \max(0, $length)); + return \substr($this->buffer, $this->current, \max(0, $length)); } /** @@ -106,8 +125,9 @@ public function peek(int $length = 1): string */ public function read(int $length): string { - $value = \substr($this->buffer, $this->position, \max(0, $length)); - $this->position += \strlen($value); + $value = \substr($this->buffer, $this->current, \max(0, $length)); + + $this->moveTo($this->current + \strlen($value)); return $value; } @@ -118,7 +138,7 @@ public function read(int $length): string */ public function readWhile(string $characters): string { - return $this->read(\strspn($this->buffer, $characters, $this->position)); + return $this->read(\strspn($this->buffer, $characters, $this->current)); } /** @@ -128,7 +148,7 @@ public function readWhile(string $characters): string */ public function readUntil(string $characters): string { - return $this->read(\strcspn($this->buffer, $characters, $this->position)); + return $this->read(\strcspn($this->buffer, $characters, $this->current)); } /** @@ -170,13 +190,13 @@ public function readLiteral(string $literal): bool return true; } - if ($this->position + $length > $this->length - || \substr_compare($this->buffer, $literal, $this->position, $length) !== 0 + if ($this->current + $length > $this->length + || \substr_compare($this->buffer, $literal, $this->current, $length) !== 0 ) { return false; } - $this->position += $length; + $this->moveTo($this->current + $length); return true; } @@ -186,7 +206,7 @@ public function readLiteral(string $literal): bool */ public function skipWhitespace(): void { - $this->position += \strspn($this->buffer, self::CHARS_WHITESPACE, $this->position); + $this->moveTo($this->current + \strspn($this->buffer, self::CHARS_WHITESPACE, $this->current)); } /** @@ -194,8 +214,9 @@ public function skipWhitespace(): void */ public function readRemainder(): string { - $rest = \substr($this->buffer, $this->position); - $this->position = $this->length; + $rest = \substr($this->buffer, $this->current); + + $this->moveTo($this->length); return $rest; } diff --git a/libs/phpdoc/src/Parser/Grammar/Grammar.php b/libs/phpdoc/src/Parser/Grammar/Grammar.php index a2337c8..61b8650 100644 --- a/libs/phpdoc/src/Parser/Grammar/Grammar.php +++ b/libs/phpdoc/src/Parser/Grammar/Grammar.php @@ -42,7 +42,9 @@ final class Grammar implements \Countable, \IteratorAggregate */ public function __construct(iterable $rules = []) { - $this->combinators = \iterator_to_array($rules); + $this->combinators = \is_array($rules) + ? $rules + : \iterator_to_array($rules); } /** diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php index ebee098..0c12358 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php @@ -10,12 +10,12 @@ /** * Matches the first of the given alternatives that applies. */ -final readonly class AlternationRule implements ProductionInterface +final class AlternationRule implements ProductionInterface { /** * @var non-empty-list */ - private array $rules; + private readonly array $rules; public function __construct(RuleInterface $rule, RuleInterface ...$other) { diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php index d9fa6dd..ddfdabc 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php @@ -10,17 +10,17 @@ /** * Matches an exact literal at the current position. */ -final readonly class LiteralRule implements TerminalInterface +final class LiteralRule implements TerminalInterface { public function __construct( /** * @var non-empty-string */ - private string $value, + private readonly string $value, /** * @var non-empty-string|null */ - public ?string $alias = null, + public readonly ?string $alias = null, ) {} public function match(Context $context): void diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php index 8b1f837..6a07a88 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php @@ -15,7 +15,7 @@ * new MatchRule('URI'); // matches but captures nothing * ``` */ -final readonly class MatchRule implements TerminalInterface +final class MatchRule implements TerminalInterface { public function __construct( /** @@ -23,15 +23,15 @@ public function __construct( * * @var non-empty-string */ - private string $rule, + private readonly string $rule, /** * @var non-empty-string|null */ - public ?string $alias = null, + public readonly ?string $alias = null, /** * @var non-empty-string|null */ - public ?string $renderAs = null, + public readonly ?string $renderAs = null, ) {} public function match(Context $context): void diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php index 82f87ac..15a5350 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php @@ -10,10 +10,10 @@ /** * Matches an inner rule when it applies, and is skipped when it does not. */ -final readonly class OptionalityRule implements ProductionInterface +final class OptionalityRule implements ProductionInterface { public function __construct( - private RuleInterface $rule, + private readonly RuleInterface $rule, ) {} public function match(Context $context): void diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php index 876514d..f671c8d 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php @@ -10,16 +10,16 @@ /** * Matches an inner rule as many times as it applies. */ -final readonly class RepetitionRule implements ProductionInterface +final class RepetitionRule implements ProductionInterface { public function __construct( - private RuleInterface $rule, + private readonly RuleInterface $rule, /** * Least number of repetitions required to match. * * @var int<0, max> */ - private int $min = 0, + private readonly int $min = 0, ) {} public function match(Context $context): void @@ -43,7 +43,7 @@ public function match(Context $context): void ++$count; - if ($context->cursor->position === $snapshot[0]) { + if ($context->cursor->getPosition() === $snapshot[0]) { break; } } diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php index 9240ea3..1b29b52 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php @@ -10,12 +10,12 @@ /** * Matches an ordered sequence of rules, one after another. */ -final readonly class SequencingRule implements ProductionInterface +final class SequencingRule implements ProductionInterface { /** * @var non-empty-list */ - private array $rules; + private readonly array $rules; public function __construct(RuleInterface $rule, RuleInterface ...$other) { diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php b/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php index 604c924..d7052a7 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php @@ -6,16 +6,9 @@ /** * Interface denoting a leaf (that is a terminal) rule. + * + * @property-read ?non-empty-string $alias The name the matched value is captured under, if any. */ interface TerminalInterface extends RuleInterface { - /** - * Capture name under which the presence of the rule is recorded, - * or {@see null} to match without capturing. - * - * @var non-empty-string|null - */ - public ?string $alias { - get; - } } diff --git a/libs/phpdoc/src/Parser/RawDocBlock.php b/libs/phpdoc/src/Parser/RawDocBlock.php index 737c390..e92ec29 100644 --- a/libs/phpdoc/src/Parser/RawDocBlock.php +++ b/libs/phpdoc/src/Parser/RawDocBlock.php @@ -6,13 +6,13 @@ use TypeLang\PhpDoc\Parser\Splitter\Segment; -final readonly class RawDocBlock +final class RawDocBlock { public function __construct( - public ?Segment $description, + public readonly ?Segment $description, /** * @var list */ - public array $tags, + public readonly array $tags, ) {} } diff --git a/libs/phpdoc/src/Parser/Splitter/StringSplitter.php b/libs/phpdoc/src/Parser/Splitter/StringSplitter.php index 77cb194..c986561 100644 --- a/libs/phpdoc/src/Parser/Splitter/StringSplitter.php +++ b/libs/phpdoc/src/Parser/Splitter/StringSplitter.php @@ -4,39 +4,39 @@ namespace TypeLang\PhpDoc\Parser\Splitter; -final readonly class StringSplitter implements SplitterInterface +final class StringSplitter implements SplitterInterface { /** * @var non-empty-string */ - private const string SEQUENCE_OPENING = '/*'; + private const SEQUENCE_OPENING = '/*'; /** * @var non-empty-string */ - private const string SEQUENCE_OPENING_DOCBLOCK = '/**'; + private const SEQUENCE_OPENING_DOCBLOCK = '/**'; /** * @var non-empty-string */ - private const string SEQUENCE_CLOSING = '*/'; + private const SEQUENCE_CLOSING = '*/'; /** * @var non-empty-string */ - private const string SEQUENCE_BODY = '*'; + private const SEQUENCE_BODY = '*'; /** * @var non-empty-string */ - private const string LINE_TERMINATORS = "\r\n"; + private const LINE_TERMINATORS = "\r\n"; /** * Characters trimmed from the head of a line before its content begins. * * @var non-empty-string */ - private const string BLANK = " \t\n\r\0\x0B"; + private const BLANK = " \t\n\r\0\x0B"; /** * Trailing whitespace trimmed from a line: {@see BLANK} without the line @@ -44,7 +44,7 @@ * * @var non-empty-string */ - private const string TRAILING_WHITESPACE = " \t\0\x0B"; + private const TRAILING_WHITESPACE = " \t\0\x0B"; /** * @return list diff --git a/libs/phpdoc/src/Parser/Tag/StringTagParser.php b/libs/phpdoc/src/Parser/Tag/StringTagParser.php index 6da4f19..f65491b 100644 --- a/libs/phpdoc/src/Parser/Tag/StringTagParser.php +++ b/libs/phpdoc/src/Parser/Tag/StringTagParser.php @@ -28,22 +28,22 @@ * A definition that does not start with an "@", or whose "@" is not followed * by a name, is an {@see InvalidTag}. */ -final readonly class StringTagParser implements TagParserInterface +final class StringTagParser implements TagParserInterface { /** * The ASCII characters allowed inside a tag name. * * @var non-empty-string */ - private const string ASCII_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz' + private const ASCII_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz' . 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . '0123456789' . '_-\\:'; - private string $nameTerminators; + private readonly string $nameTerminators; public function __construct( - private TagFactoryInterface $tagFactory, + private readonly TagFactoryInterface $tagFactory, ) { $this->nameTerminators = self::createTerminatorMask(); } diff --git a/libs/phpdoc/src/Parser/TagFactory.php b/libs/phpdoc/src/Parser/TagFactory.php index bc4a45d..f256bca 100644 --- a/libs/phpdoc/src/Parser/TagFactory.php +++ b/libs/phpdoc/src/Parser/TagFactory.php @@ -28,15 +28,15 @@ * * @phpstan-import-type CombinatorType from CombinatorInterface */ -final readonly class TagFactory implements TagFactoryInterface +final class TagFactory implements TagFactoryInterface { - private TagSpecificationParser $parser; + private readonly TagSpecificationParser $parser; /** * @param iterable $combinators */ public function __construct( - private TagRegistryInterface $registry, + private readonly TagRegistryInterface $registry, iterable $combinators = [], ) { $this->parser = new TagSpecificationParser($combinators); diff --git a/libs/phpdoc/src/Parser/TagRegistry.php b/libs/phpdoc/src/Parser/TagRegistry.php index 3251f4e..2b932a5 100644 --- a/libs/phpdoc/src/Parser/TagRegistry.php +++ b/libs/phpdoc/src/Parser/TagRegistry.php @@ -26,14 +26,14 @@ * * @template-implements \IteratorAggregate */ -final readonly class TagRegistry implements TagRegistryInterface, \IteratorAggregate +final class TagRegistry implements TagRegistryInterface, \IteratorAggregate { public function __construct( /** * @var array */ - private array $definitions, - private TagDefinitionInterface $genericTagDefinition, + private readonly array $definitions, + private readonly TagDefinitionInterface $genericTagDefinition, ) {} public function has(string $name): bool diff --git a/libs/phpdoc/src/Parser/TagSpecificationParser.php b/libs/phpdoc/src/Parser/TagSpecificationParser.php index 9a80f76..41a2e55 100644 --- a/libs/phpdoc/src/Parser/TagSpecificationParser.php +++ b/libs/phpdoc/src/Parser/TagSpecificationParser.php @@ -18,9 +18,9 @@ /** * @phpstan-import-type CombinatorType from CombinatorInterface */ -final readonly class TagSpecificationParser +final class TagSpecificationParser { - private Grammar $grammar; + private readonly Grammar $grammar; /** * @param iterable $combinators @@ -49,7 +49,7 @@ public function parse(TagDefinitionInterface $definition, string $name, string $ tag: $name, grammar: (string) $rule, source: $suffix, - offset: $cursor->furthestOffset, + offset: $cursor->getFurthestOffset(), ); } diff --git a/libs/phpdoc/src/Platform/PhanPlatform.php b/libs/phpdoc/src/Platform/PhanPlatform.php index fba14bb..0e668d3 100644 --- a/libs/phpdoc/src/Platform/PhanPlatform.php +++ b/libs/phpdoc/src/Platform/PhanPlatform.php @@ -50,10 +50,11 @@ final class PhanPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ 'phan-assert' => new AssertTagDefinition(), 'phan-assert-if-true' => new AssertIfTrueTagDefinition(), 'phan-assert-if-false' => new AssertIfFalseTagDefinition(), @@ -78,10 +79,11 @@ final class PhanPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'phan-abstract' => AbstractTagDefinition::NAME, 'phanclosurescope' => PhanClosureScopeTagDefinition::NAME, 'phan-extends' => ExtendsTagDefinition::NAME, diff --git a/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php b/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php index 8a7837c..70ee6b7 100644 --- a/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php +++ b/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php @@ -19,10 +19,11 @@ final class PhpCodeSnifferPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ CodingStandardsIgnoreStartTagDefinition::NAME => new CodingStandardsIgnoreStartTagDefinition(), CodingStandardsIgnoreEndTagDefinition::NAME => new CodingStandardsIgnoreEndTagDefinition(), CodingStandardsIgnoreLineTagDefinition::NAME => new CodingStandardsIgnoreLineTagDefinition(), diff --git a/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php b/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php index 17d7704..8cc57b5 100644 --- a/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php +++ b/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php @@ -43,10 +43,11 @@ final class PhpDocumentorPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ AccessTagDefinition::NAME => new AccessTagDefinition(), AuthorTagDefinition::NAME => new AuthorTagDefinition(), CategoryTagDefinition::NAME => new CategoryTagDefinition(), @@ -72,10 +73,11 @@ final class PhpDocumentorPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => [ + public function getCombinators(): iterable + { + return [ VisibilityCombinator::NAME => new VisibilityCombinator(), AuthorNameCombinator::NAME => new AuthorNameCombinator(), EmailCombinator::NAME => new EmailCombinator(), diff --git a/libs/phpdoc/src/Platform/PhpStanPlatform.php b/libs/phpdoc/src/Platform/PhpStanPlatform.php index 875fd6b..aa65f78 100644 --- a/libs/phpdoc/src/Platform/PhpStanPlatform.php +++ b/libs/phpdoc/src/Platform/PhpStanPlatform.php @@ -53,10 +53,11 @@ final class PhpStanPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ 'phpstan-allow-private-mutation' => new AllowPrivateMutationTagDefinition(), 'phpstan-assert' => new AssertTagDefinition(), 'phpstan-assert-if-true' => new AssertIfTrueTagDefinition(), @@ -76,10 +77,11 @@ final class PhpStanPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'phpstan-extends' => ExtendsTagDefinition::NAME, 'phpstan-immutable' => ImmutableTagDefinition::NAME, 'phpstan-implements' => ImplementsTagDefinition::NAME, diff --git a/libs/phpdoc/src/Platform/PhpStormPlatform.php b/libs/phpdoc/src/Platform/PhpStormPlatform.php index 7a7b967..20fe377 100644 --- a/libs/phpdoc/src/Platform/PhpStormPlatform.php +++ b/libs/phpdoc/src/Platform/PhpStormPlatform.php @@ -17,10 +17,11 @@ final class PhpStormPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ ExpectedExceptionTagDefinition::NAME => new ExpectedExceptionTagDefinition(), FormatterOffTagDefinition::NAME => new FormatterOffTagDefinition(), FormatterOnTagDefinition::NAME => new FormatterOnTagDefinition(), diff --git a/libs/phpdoc/src/Platform/Platform.php b/libs/phpdoc/src/Platform/Platform.php index e80820c..29decc2 100644 --- a/libs/phpdoc/src/Platform/Platform.php +++ b/libs/phpdoc/src/Platform/Platform.php @@ -13,23 +13,26 @@ abstract class Platform implements PlatformInterface { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => []; + public function getTags(): iterable + { + return []; } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => []; + public function getAliases(): iterable + { + return []; } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => []; + public function getCombinators(): iterable + { + return []; } } diff --git a/libs/phpdoc/src/Platform/PlatformInterface.php b/libs/phpdoc/src/Platform/PlatformInterface.php index ce30d7a..f00be3f 100644 --- a/libs/phpdoc/src/Platform/PlatformInterface.php +++ b/libs/phpdoc/src/Platform/PlatformInterface.php @@ -21,27 +21,21 @@ interface PlatformInterface /** * Tag definitions keyed by their canonical (lower-case) name. * - * @var iterable + * @return iterable */ - public iterable $tags { - get; - } + public function getTags(): iterable; /** * Alias-to-canonical name pairs, both lower-case. * - * @var iterable + * @return iterable */ - public iterable $aliases { - get; - } + public function getAliases(): iterable; /** * Grammar combinators keyed by their name. * - * @var iterable + * @return iterable */ - public iterable $combinators { - get; - } + public function getCombinators(): iterable; } diff --git a/libs/phpdoc/src/Platform/PsalmPlatform.php b/libs/phpdoc/src/Platform/PsalmPlatform.php index 82cd169..4383532 100644 --- a/libs/phpdoc/src/Platform/PsalmPlatform.php +++ b/libs/phpdoc/src/Platform/PsalmPlatform.php @@ -80,10 +80,11 @@ final class PsalmPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ 'psalm-allow-private-mutation' => new AllowPrivateMutationTagDefinition(), 'psalm-assert' => new AssertTagDefinition(), 'psalm-assert-if-true' => new AssertIfTrueTagDefinition(), @@ -126,10 +127,11 @@ final class PsalmPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'psalm-api' => ApiTagDefinition::NAME, 'psalm-extends' => ExtendsTagDefinition::NAME, 'psalm-immutable' => ImmutableTagDefinition::NAME, @@ -158,10 +160,11 @@ final class PsalmPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => [ + public function getCombinators(): iterable + { + return [ FlowTypeCombinator::NAME => new FlowTypeCombinator(), ]; } diff --git a/libs/phpdoc/src/Platform/StandardPlatform.php b/libs/phpdoc/src/Platform/StandardPlatform.php index 8aadd2b..fbf8d59 100644 --- a/libs/phpdoc/src/Platform/StandardPlatform.php +++ b/libs/phpdoc/src/Platform/StandardPlatform.php @@ -69,10 +69,11 @@ final class StandardPlatform implements PlatformInterface { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ LinkTagDefinition::NAME => new LinkTagDefinition(), SeeTagDefinition::NAME => new SeeTagDefinition(), ReturnTagDefinition::NAME => new ReturnTagDefinition(), @@ -115,10 +116,11 @@ final class StandardPlatform implements PlatformInterface } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'inherits' => ExtendsTagDefinition::NAME, 'template-extends' => ExtendsTagDefinition::NAME, 'template-implements' => ImplementsTagDefinition::NAME, @@ -131,10 +133,11 @@ final class StandardPlatform implements PlatformInterface } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => [ + public function getCombinators(): iterable + { + return [ UriCombinator::NAME => new UriCombinator(), UrlCombinator::NAME => new UrlCombinator(), ReferenceCombinator::NAME => new ReferenceCombinator(), diff --git a/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php b/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php index a9d9d8c..8a9bc14 100644 --- a/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php +++ b/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php @@ -22,7 +22,7 @@ public function constructorStoresValue(): void #[Test] public function valueDefaultsToEmptyString(): void { - self::assertSame('', new Description()->value); + self::assertSame('', (new Description())->value); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php b/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php index f94f955..f7a20b4 100644 --- a/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php +++ b/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php @@ -27,7 +27,7 @@ public function constructorStoresComponentsAsList(): void #[Test] public function componentsDefaultToEmptyList(): void { - self::assertSame([], new TaggedDescription()->components); + self::assertSame([], (new TaggedDescription())->components); } #[Test] @@ -95,7 +95,7 @@ public function offsetGetReturnsComponent(): void #[Test] public function offsetGetReturnsNullForMissingOffset(): void { - self::assertNull(new TaggedDescription()[42]); + self::assertNull((new TaggedDescription())[42]); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/DocBlockTest.php b/libs/phpdoc/tests/DocBlock/DocBlockTest.php index 129c2dc..1d85e06 100644 --- a/libs/phpdoc/tests/DocBlock/DocBlockTest.php +++ b/libs/phpdoc/tests/DocBlock/DocBlockTest.php @@ -15,7 +15,7 @@ final class DocBlockTest extends TestCase #[Test] public function descriptionDefaultsToNull(): void { - self::assertNull(new DocBlock()->description); + self::assertNull((new DocBlock())->description); } #[Test] @@ -23,13 +23,13 @@ public function constructorKeepsExistingDescriptionInstance(): void { $description = new Description('Summary'); - self::assertSame($description, new DocBlock($description)->description); + self::assertSame($description, (new DocBlock($description))->description); } #[Test] public function tagsDefaultToEmptyList(): void { - self::assertSame([], new DocBlock()->tags); + self::assertSame([], (new DocBlock())->tags); } #[Test] @@ -82,7 +82,7 @@ public function offsetGetReturnsTag(): void #[Test] public function offsetGetReturnsNullForMissingOffset(): void { - self::assertNull(new DocBlock()[42]); + self::assertNull((new DocBlock())[42]); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php index befd0e7..8cf7456 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php @@ -21,7 +21,7 @@ final class DescriptionGrammarRuleTest extends GrammarRuleTestCase { protected function rule(): DescriptionCombinator { - return new \ReflectionClass(DescriptionCombinator::class) + return (new \ReflectionClass(DescriptionCombinator::class)) ->newLazyProxy(function (DescriptionCombinator $proxy) { $registry = new TagRegistry( definitions: [], @@ -66,7 +66,7 @@ public function consumesTheEntireRemainder(): void self::assertInstanceOf(Description::class, $description); self::assertSame('a b c', (string) $description); - self::assertTrue($cursor->isEof); + self::assertTrue($cursor->isEof()); } /** diff --git a/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php index af98152..e350f7d 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php @@ -69,7 +69,7 @@ public function stopsAtTheFirstWhitespace(): void self::assertInstanceOf(VariableReference::class, $reference); self::assertSame('var', $reference->name); - self::assertSame(4, $cursor->offset); + self::assertSame(4, $cursor->getOffset()); } #[Test] @@ -79,7 +79,7 @@ public function stopsAfterTheReference(): void $reference = $this->matchCursor($cursor); self::assertInstanceOf(ClassMethodReference::class, $reference); - self::assertSame(21, $cursor->offset); + self::assertSame(21, $cursor->getOffset()); } /** diff --git a/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php index dfd30a1..5d21172 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php @@ -65,7 +65,7 @@ public function stopsAfterTheType(): void self::assertInstanceOf(UnionTypeNode::class, $statement->type); self::assertSame('int|string', $statement->source); - self::assertSame(11, $cursor->offset); + self::assertSame(11, $cursor->getOffset()); } #[Test] @@ -75,7 +75,7 @@ public function respectsTheCursorBase(): void $statement = $this->matchCursor($cursor); self::assertInstanceOf(TypeReference::class, $statement); - self::assertSame(104, $cursor->offset); + self::assertSame(104, $cursor->getOffset()); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php index d3b0a10..dfdc5c6 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php @@ -43,7 +43,7 @@ public function stopsAtTheFirstWhitespace(): void $name = $this->matchCursor($cursor); self::assertSame('var', $name); - self::assertSame(4, $cursor->offset); + self::assertSame(4, $cursor->getOffset()); } /** @@ -56,7 +56,7 @@ public static function invalidDataProvider(): iterable yield 'missing dollar' => ['var']; yield 'dollar only' => ['$']; yield 'illegal character' => ['$foo!bar']; - yield 'namespace separator' => ['$foo\\bar']; + yield 'namespace separator' => ['$foo\$tag->typear']; } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php index 6fbc7bc..c20e7df 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php @@ -93,8 +93,8 @@ public function parsesTrailingTextAsDescription(): void #[Test] public function placementIsRecognized(): void { - self::assertSame(TagPlacement::Any, new InternalTagDefinition()->placement); - self::assertSame(TagPlacement::Block, new AbstractTagDefinition()->placement); + self::assertSame(TagPlacement::Any, (new InternalTagDefinition())->placement); + self::assertSame(TagPlacement::Block, (new AbstractTagDefinition())->placement); } /** diff --git a/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php index e1accd7..ca8e993 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php @@ -30,7 +30,7 @@ public function constructorStoresReason(): void #[Test] public function descriptionDefaultsToNull(): void { - self::assertNull(new InvalidTag(new \RuntimeException(), 'param')->description); + self::assertNull((new InvalidTag(new \RuntimeException(), 'param'))->description); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Tag/TagTest.php b/libs/phpdoc/tests/DocBlock/Tag/TagTest.php index ee07809..b6033ad 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/TagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/TagTest.php @@ -14,13 +14,13 @@ final class TagTest extends TagTestCase #[Test] public function constructorStoresName(): void { - self::assertSame('param', new Tag('param')->name); + self::assertSame('param', (new Tag('param'))->name); } #[Test] public function descriptionDefaultsToNull(): void { - self::assertNull(new Tag('param')->description); + self::assertNull((new Tag('param'))->description); } #[Test] @@ -28,7 +28,7 @@ public function constructorKeepsExistingDescriptionInstance(): void { $description = new Description('int $a'); - self::assertSame($description, new Tag('param', $description)->description); + self::assertSame($description, (new Tag('param', $description))->description); } #[Test] diff --git a/libs/phpdoc/tests/Parser/Grammar/CursorTest.php b/libs/phpdoc/tests/Parser/Grammar/CursorTest.php index 813af33..26ddc46 100644 --- a/libs/phpdoc/tests/Parser/Grammar/CursorTest.php +++ b/libs/phpdoc/tests/Parser/Grammar/CursorTest.php @@ -16,9 +16,9 @@ public function startsAtTheBeginning(): void { $cursor = new Cursor('example'); - self::assertSame(0, $cursor->position); - self::assertSame(0, $cursor->offset); - self::assertFalse($cursor->isEof); + self::assertSame(0, $cursor->getPosition()); + self::assertSame(0, $cursor->getOffset()); + self::assertFalse($cursor->isEof()); } #[Test] @@ -27,8 +27,8 @@ public function rebasesTheOffsetOntoTheSource(): void $cursor = new Cursor('example', base: 100); $cursor->read(3); - self::assertSame(3, $cursor->position); - self::assertSame(103, $cursor->offset); + self::assertSame(3, $cursor->getPosition()); + self::assertSame(103, $cursor->getOffset()); } #[Test] @@ -38,7 +38,7 @@ public function peeksWithoutConsuming(): void self::assertSame('e', $cursor->peek()); self::assertSame('exam', $cursor->peek(4)); - self::assertSame(0, $cursor->position); + self::assertSame(0, $cursor->getPosition()); } #[Test] @@ -49,7 +49,7 @@ public function readsAFixedLength(): void self::assertSame('exa', $cursor->read(3)); self::assertSame('mple', $cursor->read(100)); self::assertSame('', $cursor->read(1)); - self::assertTrue($cursor->isEof); + self::assertTrue($cursor->isEof()); } #[Test] @@ -119,7 +119,7 @@ public function consumesAMatchingLiteral(): void $cursor = new Cursor('$name'); self::assertTrue($cursor->readLiteral('$')); - self::assertSame(1, $cursor->position); + self::assertSame(1, $cursor->getPosition()); } #[Test] @@ -128,7 +128,7 @@ public function leavesTheCursorOnAMismatchingLiteral(): void $cursor = new Cursor('name'); self::assertFalse($cursor->readLiteral('$')); - self::assertSame(0, $cursor->position); + self::assertSame(0, $cursor->getPosition()); } #[Test] @@ -137,7 +137,7 @@ public function doesNotMatchALiteralPastTheEnd(): void $cursor = new Cursor('ab'); self::assertFalse($cursor->readLiteral('abc')); - self::assertSame(0, $cursor->position); + self::assertSame(0, $cursor->getPosition()); } #[Test] @@ -145,9 +145,9 @@ public function remembersTheFurthestOffset(): void { $cursor = new Cursor('example', base: 10); $cursor->read(5); - $cursor->position = 2; + $cursor->moveTo(2); - self::assertSame(12, $cursor->offset); - self::assertSame(15, $cursor->furthestOffset); + self::assertSame(12, $cursor->getOffset()); + self::assertSame(15, $cursor->getFurthestOffset()); } } diff --git a/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php b/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php index 03a05d6..c8888a3 100644 --- a/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php +++ b/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php @@ -27,7 +27,7 @@ final class DefinitionTest extends TestCase #[Test] public function ruleStringifiesToItsGrammar(): void { - self::assertSame(' [ ]', (string) new LinkTagDefinition()->spec); + self::assertSame(' [ ]', (string) (new LinkTagDefinition())->spec); } #[Test] diff --git a/libs/phpdoc/tests/TestCase.php b/libs/phpdoc/tests/TestCase.php index 79c2423..2bf4190 100644 --- a/libs/phpdoc/tests/TestCase.php +++ b/libs/phpdoc/tests/TestCase.php @@ -43,12 +43,12 @@ private static function buildTagFactory(): TagFactoryInterface $tagFactory = null; - $baseRules[DescriptionCombinator::NAME] = new \ReflectionClass(DescriptionCombinator::class) + $baseRules[DescriptionCombinator::NAME] = (new \ReflectionClass(DescriptionCombinator::class)) ->newLazyProxy(function () use (&$tagFactory): DescriptionCombinator { if ($tagFactory === null) { return new DescriptionCombinator(new BalancedBraceAwareParser( new StringTagParser(new TagFactory( - registry: new TagRegistryBuilder() + registry: (new TagRegistryBuilder()) ->build(), combinators: [ UriCombinator::NAME => new UriCombinator(), @@ -65,7 +65,7 @@ private static function buildTagFactory(): TagFactoryInterface ); }); - $tagFactory = new TagFactory(new TagRegistryBuilder()->build(), $baseRules); + $tagFactory = new TagFactory((new TagRegistryBuilder())->build(), $baseRules); return $tagFactory; } From 7a0c4b34088374bb604ff11dd778914c3a192f9c Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:01:54 +0300 Subject: [PATCH 012/119] Simplify matcher visitor --- libs/parser/src/Traverser/MatcherVisitor.php | 27 +------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/libs/parser/src/Traverser/MatcherVisitor.php b/libs/parser/src/Traverser/MatcherVisitor.php index 2771344..b44c8df 100644 --- a/libs/parser/src/Traverser/MatcherVisitor.php +++ b/libs/parser/src/Traverser/MatcherVisitor.php @@ -6,18 +6,8 @@ use TypeLang\Type\Node; -/** - * @property-read bool $isFound An alias of {@see isFound()} method. - */ class MatcherVisitor extends Visitor { - /** - * @var list - */ - private const VIRTUAL_PROPERTIES = [ - 'isFound', - ]; - public ?Node $node = null; private bool $shouldContinue = false; @@ -34,26 +24,11 @@ public function __construct( /** * Returns {@see true} in case of a node matching the criteria was found. */ - public function isFound(): bool + public function hasMatches(): bool { return $this->node !== null; } - public function __get(string $name): mixed - { - return match ($name) { - 'isFound' => $this->isFound(), - default => throw new \OutOfRangeException( - message: \sprintf('Undefined property %s::$%s', static::class, $name), - ), - }; - } - - public function __isset(string $name): bool - { - return \in_array($name, self::VIRTUAL_PROPERTIES, true); - } - public function before(): void { $this->node = null; From 9cd089ba26f59b124a5527abfc0f3fcfb036d548 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:05:48 +0300 Subject: [PATCH 013/119] Improve type resolvers --- libs/parser/src/TypeResolver.php | 4 ++-- .../TypeResolver/PhpUseStatementsTransformer.php | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/libs/parser/src/TypeResolver.php b/libs/parser/src/TypeResolver.php index 1a2490b..f592de8 100644 --- a/libs/parser/src/TypeResolver.php +++ b/libs/parser/src/TypeResolver.php @@ -116,7 +116,7 @@ public function withTypeImportAs(string $name, string $alias): self */ public function withTypeImportsFromClass(\ReflectionClass $class): self { - $statements = new PhpUseStatementsReader() + $statements = (new PhpUseStatementsReader()) ->getClassUseStatements($class); return new self([...$this->imports, ...$statements]); @@ -162,7 +162,7 @@ public function withTypeImportsFromFunction(\ReflectionFunctionAbstract $functio return $this->withTypeImportsFromClass($function->getDeclaringClass()); } - $statements = new PhpUseStatementsReader() + $statements = (new PhpUseStatementsReader()) ->getFunctionUseStatements($function); return new self([...$this->imports, ...$statements]); diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php b/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php index 61d40ac..c8da302 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php @@ -73,7 +73,7 @@ private function format(iterable $replacements): array // normalize key if (\is_int($key) || $key === '') { - $key = $replacement->last->toString(); + $key = $replacement->getLastPartAsString(); } $result[\strtolower($key)] = $replacement; @@ -89,17 +89,17 @@ private static function isNonSubstituted(Name $name): bool return true; } + $first = $name->getFirstPart(); + // Non-FQN names prefixed with "namespace" must not be replaced - if (!$name->isSimple) { - return $name->first->toLowerString() === 'namespace'; + if (!$name->isSimple()) { + return $first->toLowerString() === 'namespace'; } - $first = $name->first; - // builtin types must not be replaced - return $first->isBuiltin + return $first->isBuiltin() // special types also must not be replaced - || $first->isSpecial; + || $first->isSpecial(); } public function __invoke(Name $name): ?Name @@ -108,7 +108,7 @@ public function __invoke(Name $name): ?Name return null; } - $first = \strtolower($name->first->toString()); + $first = \strtolower($name->getFirstPartAsString()); $prefix = $this->replacements[$first] ?? null; return $prefix?->mergeWith($name); From 6fe319f7d63283a1d9879399d06a30fe97eb05b3 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:06:04 +0300 Subject: [PATCH 014/119] Fix and rebuild grammar --- libs/parser/resources/grammar.php | 2 +- libs/parser/resources/grammar/shape-fields.pp2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/parser/resources/grammar.php b/libs/parser/resources/grammar.php index 3dc27b4..98be68a 100644 --- a/libs/parser/resources/grammar.php +++ b/libs/parser/resources/grammar.php @@ -559,7 +559,7 @@ foreach ($children as $field) { if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->index; + $key = $field->getIndex(); if (\in_array($key, $explicit, true)) { throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); diff --git a/libs/parser/resources/grammar/shape-fields.pp2 b/libs/parser/resources/grammar/shape-fields.pp2 index 0c1a2fe..2e79b19 100644 --- a/libs/parser/resources/grammar/shape-fields.pp2 +++ b/libs/parser/resources/grammar/shape-fields.pp2 @@ -43,7 +43,7 @@ ShapeFieldsList -> { foreach ($children as $field) { if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->index; + $key = $field->getIndex(); if (\in_array($key, $explicit, true)) { throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); From 459fced0e1162b36aa6c490e80c15f528264d188 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:06:13 +0300 Subject: [PATCH 015/119] Fix parser tests --- .../tests/Literal/IntLiteralParserTest.php | 10 ++-- .../tests/Literal/StringLiteralParserTest.php | 14 ++--- libs/parser/tests/Syntax/NameTest.php | 2 +- .../tests/TypeResolver/TypeResolverTest.php | 60 +++++++++---------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/libs/parser/tests/Literal/IntLiteralParserTest.php b/libs/parser/tests/Literal/IntLiteralParserTest.php index 5a3cef2..a4f0f14 100644 --- a/libs/parser/tests/Literal/IntLiteralParserTest.php +++ b/libs/parser/tests/Literal/IntLiteralParserTest.php @@ -38,7 +38,7 @@ public function integerIsParsedToItsDecimalValue(string $literal, int $value, st { $node = IntLiteralParser::parse($literal); - self::assertSame($value, $node->value()); + self::assertSame($value, $node->value); self::assertSame($decimal, $node->decimal); } @@ -46,7 +46,7 @@ public function integerIsParsedToItsDecimalValue(string $literal, int $value, st #[DataProvider('provideIntegers')] public function integerParsingKeepsTheOriginalRepresentation(string $literal, int $value, string $decimal): void { - self::assertSame($literal, IntLiteralParser::parse($literal)->raw()); + self::assertSame($literal, IntLiteralParser::parse($literal)->raw); } #[Test] @@ -54,7 +54,7 @@ public function integerParsingSupportsPhpIntMin(): void { $node = IntLiteralParser::parse((string) \PHP_INT_MIN); - self::assertSame(\PHP_INT_MIN, $node->value()); + self::assertSame(\PHP_INT_MIN, $node->value); } #[Test] @@ -62,7 +62,7 @@ public function integerParsingSupportsPhpIntMax(): void { $node = IntLiteralParser::parse((string) \PHP_INT_MAX); - self::assertSame(\PHP_INT_MAX, $node->value()); + self::assertSame(\PHP_INT_MAX, $node->value); } #[Test] @@ -70,6 +70,6 @@ public function negativeZeroIsParsedAsZero(): void { $node = IntLiteralParser::parse('-0'); - self::assertSame(0, $node->value()); + self::assertSame(0, $node->value); } } diff --git a/libs/parser/tests/Literal/StringLiteralParserTest.php b/libs/parser/tests/Literal/StringLiteralParserTest.php index 7c818e6..ff2627d 100644 --- a/libs/parser/tests/Literal/StringLiteralParserTest.php +++ b/libs/parser/tests/Literal/StringLiteralParserTest.php @@ -54,15 +54,15 @@ public function doubleQuotedStringIsDecoded(string $literal, string $value): voi { $node = StringLiteralParser::parse($literal); - self::assertSame($value, $node->value()); - self::assertSame($literal, $node->raw()); + self::assertSame($value, $node->value); + self::assertSame($literal, $node->raw); } #[Test] #[DataProvider('provideDoubleQuotedStrings')] public function doubleQuotedFactoryIsTheSameAsParsing(string $literal, string $value): void { - self::assertSame($value, StringLiteralParser::createFromDoubleQuotedString($literal)->value()); + self::assertSame($value, StringLiteralParser::createFromDoubleQuotedString($literal)->value); } /** @@ -85,15 +85,15 @@ public function singleQuotedStringIsDecoded(string $literal, string $value): voi { $node = StringLiteralParser::parse($literal); - self::assertSame($value, $node->value()); - self::assertSame($literal, $node->raw()); + self::assertSame($value, $node->value); + self::assertSame($literal, $node->raw); } #[Test] #[DataProvider('provideSingleQuotedStrings')] public function singleQuotedFactoryIsTheSameAsParsing(string $literal, string $value): void { - self::assertSame($value, StringLiteralParser::createFromSingleQuotedString($literal)->value()); + self::assertSame($value, StringLiteralParser::createFromSingleQuotedString($literal)->value); } #[Test] @@ -101,7 +101,7 @@ public function derivedRawValueCanBeParsedBack(): void { $node = new StringLiteralNode('a"b'); - self::assertSame($node->value(), StringLiteralParser::parse($node->raw())->value()); + self::assertSame($node->value, StringLiteralParser::parse($node->raw)->value); } #[Test] diff --git a/libs/parser/tests/Syntax/NameTest.php b/libs/parser/tests/Syntax/NameTest.php index b68646f..ccd7a7a 100644 --- a/libs/parser/tests/Syntax/NameTest.php +++ b/libs/parser/tests/Syntax/NameTest.php @@ -39,7 +39,7 @@ public function testValidNames(string $type, string $expected): void self::assertInstanceOf(NamedTypeNode::class, $statement); self::assertSame($expected, $statement->name->toString()); - self::assertTrue($statement->name->isSimple); + self::assertTrue($statement->name->isSimple()); } public function testRelativeNamespacedName(): void diff --git a/libs/parser/tests/TypeResolver/TypeResolverTest.php b/libs/parser/tests/TypeResolver/TypeResolverTest.php index 24c84ec..5759b39 100644 --- a/libs/parser/tests/TypeResolver/TypeResolverTest.php +++ b/libs/parser/tests/TypeResolver/TypeResolverTest.php @@ -33,7 +33,7 @@ private function resolveName(TypeResolver $resolver, string $code): string public function testResolvesImportedName(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame( @@ -44,7 +44,7 @@ public function testResolvesImportedName(): void public function testResolvesImportedNamePrefix(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame( @@ -55,7 +55,7 @@ public function testResolvesImportedNamePrefix(): void public function testResolvesAliasedName(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); self::assertSame( @@ -66,7 +66,7 @@ public function testResolvesAliasedName(): void public function testResolvesAliasItself(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); self::assertSame( @@ -93,7 +93,7 @@ public static function caseInsensitiveDataProvider(): iterable #[DataProvider('caseInsensitiveDataProvider')] public function testMatchesFirstSegmentCaseInsensitively(string $code, string $expected): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame($expected, $this->resolveName($resolver, $code)); @@ -101,7 +101,7 @@ public function testMatchesFirstSegmentCaseInsensitively(string $code, string $e public function testLeavesUnknownNameUnchanged(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame('Unknown', $this->resolveName($resolver, 'Unknown')); @@ -117,7 +117,7 @@ public function testEmptyResolverLeavesNameUnchanged(): void public function testBuiltinTypeIsLeftUnchanged(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame('int', $this->resolveName($resolver, 'int')); @@ -125,7 +125,7 @@ public function testBuiltinTypeIsLeftUnchanged(): void public function testLastImportWinsForDuplicateLastSegment(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('First\Node') ->withTypeImport('Second\Node'); @@ -134,7 +134,7 @@ public function testLastImportWinsForDuplicateLastSegment(): void public function testResolvesNamesInsideUnion(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node') ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); @@ -155,7 +155,7 @@ public function testResolvesNamesInsideUnion(): void public function testResolvesNamesInsideShape(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node') ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); @@ -182,7 +182,7 @@ public function testResolvesNamesInsideShape(): void public function testResolvesNamesInsideGenericArguments(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('Vendor\Collection') ->withTypeImport('Vendor\Node'); @@ -202,7 +202,7 @@ public function testResolvesNamesInsideGenericArguments(): void public function testResolveMutatesInPlaceAndReturnsSameInstance(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); $source = $this->parse('Node'); @@ -262,7 +262,7 @@ public static function aliasCaseDataProvider(): iterable #[DataProvider('aliasCaseDataProvider')] public function testMatchesAliasCaseInsensitively(string $code, string $expected): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); self::assertSame($expected, $this->resolveName($resolver, $code)); @@ -270,7 +270,7 @@ public function testMatchesAliasCaseInsensitively(string $code, string $expected public function testResolvesNamesInsideCallableType(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); @@ -294,7 +294,7 @@ public function testResolvesNamesInsideCallableType(): void public function testResolvesClassInClassConstant(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); self::assertSame(<<<'AST' @@ -308,7 +308,7 @@ public function testResolvesClassInClassConstant(): void public function testResolvesClassInClassConstantMask(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); self::assertSame(<<<'AST' @@ -321,7 +321,7 @@ public function testResolvesClassInClassConstantMask(): void public function testResolvesNamesInsideIntersection(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); @@ -341,7 +341,7 @@ public function testResolvesNamesInsideIntersection(): void public function testResolvesNamesInsideConditional(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); @@ -368,7 +368,7 @@ public function testResolvesNamesInsideConditional(): void */ public function testDoesNotResolveNamespaceRelativePrefix(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); self::assertSame('namespace\Node', $this->resolveName($resolver, 'namespace\Node')); @@ -393,7 +393,7 @@ public static function nonMatchingNameDataProvider(): iterable #[DataProvider('nonMatchingNameDataProvider')] public function testDoesNotResolveNonMatchingName(string $code): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); @@ -418,7 +418,7 @@ public static function literalTypeDataProvider(): iterable #[DataProvider('literalTypeDataProvider')] public function testLeavesLiteralTypesUntouched(string $code): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); $source = $this->parse($code); @@ -436,7 +436,7 @@ public function testLeavesLiteralTypesUntouched(string $code): void */ public function testLeavesUnrelatedCompoundTypeUntouched(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); $source = $this->parse('array|callable(bool): void'); @@ -453,7 +453,7 @@ public function testEmptyResolverIsANoOp(): void $source = $this->parse('array{Node, Error\Sub}'); $before = $this->print($source); - self::assertSame($before, $this->print(new TypeResolver()->resolve($source))); + self::assertSame($before, $this->print((new TypeResolver())->resolve($source))); } /** @@ -463,7 +463,7 @@ public function testEmptyResolverIsANoOp(): void */ public function testMalformedImportIsRejected(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('\\'); $this->expectException(\Throwable::class); @@ -492,7 +492,7 @@ public static function fullyQualifiedNameDataProvider(): iterable #[DataProvider('fullyQualifiedNameDataProvider')] public function testFullyQualifiedNamesIgnoreImports(string $code, string $expected): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node') ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); @@ -525,7 +525,7 @@ public static function reservedBuiltinTypeDataProvider(): iterable #[DataProvider('reservedBuiltinTypeDataProvider')] public function testReservedBuiltinTypeIsNeverRewrittenByImport(string $reserved): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport("Vendor\\{$reserved}"); self::assertSame($reserved, $this->resolveName($resolver, $reserved)); @@ -547,7 +547,7 @@ public static function reservedSpecialTypeDataProvider(): iterable #[DataProvider('reservedSpecialTypeDataProvider')] public function testReservedSpecialTypeIsNeverRewrittenByImport(string $reserved): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport("Vendor\\{$reserved}"); self::assertSame($reserved, $this->resolveName($resolver, $reserved)); @@ -571,7 +571,7 @@ public static function nonMatchingSegmentDataProvider(): iterable #[DataProvider('nonMatchingSegmentDataProvider')] public function testImportMatchesWholeSegmentOnly(string $code): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('A\Node'); self::assertSame($code, $this->resolveName($resolver, $code)); @@ -584,7 +584,7 @@ public function testImportMatchesWholeSegmentOnly(string $code): void */ public function testResolutionIsIdempotent(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); $node = $this->parse('Node\Foo'); @@ -602,7 +602,7 @@ public function testResolutionIsIdempotent(): void */ public function testAliasSubstitutionKeepsAllTrailingSegments(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('A\B\Exception', 'Error'); self::assertSame( From ddf9ddb09b566350eddb0cf22e19919e61cf5c51 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:06:29 +0300 Subject: [PATCH 016/119] Improve Name's node API --- libs/types/src/Name.php | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index c9a444e..517ccf1 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -72,9 +72,17 @@ public function getFirstPart(): Identifier */ public function getFirstPartAsString(): string { - $identifier = $this->getFirstPart(); + return \reset($this->parts)->toString(); + } - return $identifier->toString(); + /** + * Gets the first segment of a name as a lowercase string + * + * @return non-empty-lowercase-string + */ + public function getFirstPartAsLowerString(): string + { + return \reset($this->parts)->toLowerString(); } /** @@ -92,9 +100,17 @@ public function getLastPart(): Identifier */ public function getLastPartAsString(): string { - $identifier = $this->getLastPart(); + return \end($this->parts)->toString(); + } - return $identifier->toString(); + /** + * Gets the last segment of a name as a lowercase string + * + * @return non-empty-lowercase-string + */ + public function getLastPartAsLowerString(): string + { + return \end($this->parts)->toLowerString(); } /** From a1eb95f939d3d0e1eca22e314425e3d81229b4ff Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:09:09 +0300 Subject: [PATCH 017/119] Fix composer dev-dependencies --- .github/workflows/tests.yml | 2 +- composer.json | 22 +++++++++++----------- libs/parser/composer.json | 6 +++--- libs/phpdoc/composer.json | 4 ++-- libs/printer/composer.json | 4 ++-- libs/reader/composer.json | 4 ++-- libs/types/composer.json | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/composer.json b/composer.json index cc49d4e..4b94866 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ {"type": "path", "url": "libs/*"} ], "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/parser": "^2.0", "type-lang/phpdoc": "^2.0", "type-lang/printer": "^2.0", @@ -15,17 +15,17 @@ "type-lang/types": "^2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.95.13", + "friendsofphp/php-cs-fixer": "^3.95", "jetbrains/phpstorm-attributes": "^1.2", - "phpbench/phpbench": "^1.7.0", - "phpdocumentor/reflection-docblock": "^6.0.3", - "phplrt/compiler": "^3.7.5", - "phpstan/phpdoc-parser": "^2.3.3", - "phpstan/phpstan": "^2.2.5", - "phpstan/phpstan-deprecation-rules": "^2.0.4", - "phpstan/phpstan-strict-rules": "^2.0.11", - "phpunit/phpunit": "^13.2.4", - "symfony/var-dumper": "^7.4|^8.0" + "phpbench/phpbench": "^1.2", + "phpdocumentor/reflection-docblock": "^6.0", + "phplrt/compiler": "^3.7", + "phpstan/phpdoc-parser": "^2.3", + "phpstan/phpstan": "^2.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "autoload-dev": { "psr-4": { diff --git a/libs/parser/composer.json b/libs/parser/composer.json index 15480d0..769ba7f 100644 --- a/libs/parser/composer.json +++ b/libs/parser/composer.json @@ -22,9 +22,9 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", - "phplrt/compiler": "^3.7.5", - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" + "phplrt/compiler": "^3.7", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "autoload-dev": { "psr-4": { diff --git a/libs/phpdoc/composer.json b/libs/phpdoc/composer.json index 83daccd..35f4349 100644 --- a/libs/phpdoc/composer.json +++ b/libs/phpdoc/composer.json @@ -19,8 +19,8 @@ }, "require-dev": { "phpstan/phpdoc-parser": "^2.3", - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "autoload-dev": { "psr-4": { diff --git a/libs/printer/composer.json b/libs/printer/composer.json index 6133ca2..2da22f3 100644 --- a/libs/printer/composer.json +++ b/libs/printer/composer.json @@ -19,8 +19,8 @@ }, "require-dev": { "type-lang/parser": "^2.0", - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "autoload-dev": { "psr-4": { diff --git a/libs/reader/composer.json b/libs/reader/composer.json index f3ecd69..7874f25 100644 --- a/libs/reader/composer.json +++ b/libs/reader/composer.json @@ -18,8 +18,8 @@ } }, "require-dev": { - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0", "type-lang/printer": "^2.0" }, "autoload-dev": { diff --git a/libs/types/composer.json b/libs/types/composer.json index 4de2110..798a0e6 100644 --- a/libs/types/composer.json +++ b/libs/types/composer.json @@ -17,8 +17,8 @@ } }, "require-dev": { - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.4 || ^13.2.4" + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "autoload-dev": { "psr-4": { From 57f0e646a337ea08fd16c6361a8b7135ccadd2eb Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:21:03 +0300 Subject: [PATCH 018/119] Remove unnecessary __toString() methods from AST --- .../src/Callable/CallableParameterNode.php | 25 +------------------ libs/types/src/ConstMaskNode.php | 7 +----- libs/types/src/Name.php | 18 ++++++++----- libs/types/src/Shape/FieldNode.php | 7 +----- libs/types/src/Shape/FieldsListNode.php | 7 +----- 5 files changed, 16 insertions(+), 48 deletions(-) diff --git a/libs/types/src/Callable/CallableParameterNode.php b/libs/types/src/Callable/CallableParameterNode.php index f5c69c2..8088634 100644 --- a/libs/types/src/Callable/CallableParameterNode.php +++ b/libs/types/src/Callable/CallableParameterNode.php @@ -9,7 +9,7 @@ use TypeLang\Type\Node; use TypeLang\Type\TypeNode; -final class CallableParameterNode extends Node implements \Stringable +final class CallableParameterNode extends Node { public function __construct( public ?TypeNode $type = null, @@ -38,27 +38,4 @@ public function is(string $class): bool { return $this instanceof $class; } - - public function __toString(): string - { - $result = []; - - if ($this->isOutput) { - $result[] = 'output'; - } - - if ($this->isVariadic) { - $result[] = 'variadic'; - } - - if ($this->isOptional) { - $result[] = 'optional'; - } - - if ($result === []) { - return 'simple'; - } - - return \implode(', ', $result); - } } diff --git a/libs/types/src/ConstMaskNode.php b/libs/types/src/ConstMaskNode.php index 3df1292..9e059e9 100644 --- a/libs/types/src/ConstMaskNode.php +++ b/libs/types/src/ConstMaskNode.php @@ -4,14 +4,9 @@ namespace TypeLang\Type; -final class ConstMaskNode extends TypeNode implements \Stringable +final class ConstMaskNode extends TypeNode { public function __construct( public Name $name, ) {} - - public function __toString(): string - { - return $this->name->toString() . '*'; - } } diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index 517ccf1..aa96952 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -23,6 +23,9 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String */ public readonly array $parts; + public readonly Identifier $first; + public readonly Identifier $last; + /** * @param iterable $parts */ @@ -39,6 +42,9 @@ public function __construct( \assert($parts !== [], new \InvalidArgumentException('Name parts count can not be empty')); $this->parts = $parts; + + $this->first = \reset($parts); + $this->last = \end($parts); } public static function createFromString(string|\Stringable $name): self @@ -62,7 +68,7 @@ public static function createFromString(string|\Stringable $name): self */ public function getFirstPart(): Identifier { - return $this->parts[0]; + return $this->first; } /** @@ -72,7 +78,7 @@ public function getFirstPart(): Identifier */ public function getFirstPartAsString(): string { - return \reset($this->parts)->toString(); + return $this->first->toString(); } /** @@ -82,7 +88,7 @@ public function getFirstPartAsString(): string */ public function getFirstPartAsLowerString(): string { - return \reset($this->parts)->toLowerString(); + return $this->first->toLowerString(); } /** @@ -90,7 +96,7 @@ public function getFirstPartAsLowerString(): string */ public function getLastPart(): Identifier { - return $this->parts[\count($this->parts) - 1]; + return $this->last; } /** @@ -100,7 +106,7 @@ public function getLastPart(): Identifier */ public function getLastPartAsString(): string { - return \end($this->parts)->toString(); + return $this->last->toString(); } /** @@ -110,7 +116,7 @@ public function getLastPartAsString(): string */ public function getLastPartAsLowerString(): string { - return \end($this->parts)->toLowerString(); + return $this->last->toLowerString(); } /** diff --git a/libs/types/src/Shape/FieldNode.php b/libs/types/src/Shape/FieldNode.php index ce177da..c4d6f92 100644 --- a/libs/types/src/Shape/FieldNode.php +++ b/libs/types/src/Shape/FieldNode.php @@ -8,7 +8,7 @@ use TypeLang\Type\Node; use TypeLang\Type\TypeNode; -abstract class FieldNode extends Node implements \Stringable +abstract class FieldNode extends Node { public function __construct( public TypeNode $type, @@ -26,9 +26,4 @@ public function is(string $class): bool { return $this instanceof $class; } - - public function __toString(): string - { - return $this->isOptional ? 'optional' : 'required'; - } } diff --git a/libs/types/src/Shape/FieldsListNode.php b/libs/types/src/Shape/FieldsListNode.php index 71ada23..d821780 100644 --- a/libs/types/src/Shape/FieldsListNode.php +++ b/libs/types/src/Shape/FieldsListNode.php @@ -9,7 +9,7 @@ /** * @template-extends NodeList */ -final class FieldsListNode extends NodeList implements \Stringable +final class FieldsListNode extends NodeList { /** * @param list $list @@ -20,9 +20,4 @@ public function __construct( ) { parent::__construct($list); } - - public function __toString(): string - { - return $this->sealed ? 'sealed' : 'unsealed'; - } } From 7a9f3832d3ae4f4e72e8df044016b98f4abec41c Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:50:07 +0300 Subject: [PATCH 019/119] Fix shape rendering tests --- libs/types/src/Shape/ConstMaskFieldNode.php | 2 +- libs/types/src/Shape/FieldsListNode.php | 2 +- .../Callable/CallableParameterNodeTest.php | 53 ------------------- libs/types/tests/ConstMaskNodeTest.php | 16 ------ .../tests/Shape/ExplicitFieldNodeTest.php | 5 +- libs/types/tests/Shape/FieldsListNodeTest.php | 20 +------ .../tests/Shape/ImplicitFieldNodeTest.php | 19 ------- libs/types/tests/Shape/NamedFieldNodeTest.php | 23 -------- 8 files changed, 5 insertions(+), 135 deletions(-) diff --git a/libs/types/src/Shape/ConstMaskFieldNode.php b/libs/types/src/Shape/ConstMaskFieldNode.php index 6136a2b..1975fbd 100644 --- a/libs/types/src/Shape/ConstMaskFieldNode.php +++ b/libs/types/src/Shape/ConstMaskFieldNode.php @@ -29,6 +29,6 @@ public function __construct( public function getIndex(): string { - return (string) $this->key; + return $this->key->name . '*'; } } diff --git a/libs/types/src/Shape/FieldsListNode.php b/libs/types/src/Shape/FieldsListNode.php index d821780..d675c0a 100644 --- a/libs/types/src/Shape/FieldsListNode.php +++ b/libs/types/src/Shape/FieldsListNode.php @@ -16,7 +16,7 @@ final class FieldsListNode extends NodeList */ public function __construct( array $list = [], - public bool $sealed = true, + public bool $isSealed = true, ) { parent::__construct($list); } diff --git a/libs/types/tests/Callable/CallableParameterNodeTest.php b/libs/types/tests/Callable/CallableParameterNodeTest.php index cab9272..da520b8 100644 --- a/libs/types/tests/Callable/CallableParameterNodeTest.php +++ b/libs/types/tests/Callable/CallableParameterNodeTest.php @@ -81,59 +81,6 @@ public function optionalFlagIsStored(): void self::assertTrue($node->isOptional); } - #[Test] - public function toStringReturnsSimpleWhenNoFlags(): void - { - $node = new CallableParameterNode(type: new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('simple', (string) $node); - } - - #[Test] - public function toStringReturnsOutputWhenOutputIsSet(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isOutput: true, - ); - - self::assertSame('output', (string) $node); - } - - #[Test] - public function toStringReturnsVariadicWhenVariadicIsSet(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isVariadic: true, - ); - - self::assertSame('variadic', (string) $node); - } - - #[Test] - public function toStringReturnsOptionalWhenOptionalIsSet(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isOptional: true, - ); - - self::assertSame('optional', (string) $node); - } - - #[Test] - public function toStringCombinesMultipleFlags(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isOutput: true, - isOptional: true, - ); - - self::assertSame('output, optional', (string) $node); - } - #[Test] public function throwsWhenBothTypeAndNameAreNull(): void { diff --git a/libs/types/tests/ConstMaskNodeTest.php b/libs/types/tests/ConstMaskNodeTest.php index 6c1d56a..f64ae92 100644 --- a/libs/types/tests/ConstMaskNodeTest.php +++ b/libs/types/tests/ConstMaskNodeTest.php @@ -19,22 +19,6 @@ public function constructorStoresName(): void self::assertSame($name, $node->name); } - #[Test] - public function toStringAppendsAsterisk(): void - { - $node = new ConstMaskNode(Name::createFromString('SOME_CONST')); - - self::assertSame('SOME_CONST*', (string) $node); - } - - #[Test] - public function toStringWithQualifiedNameAppendsAsterisk(): void - { - $node = new ConstMaskNode(Name::createFromString('Vendor\Package\STATUS')); - - self::assertSame('Vendor\Package\STATUS*', (string) $node); - } - #[Test] public function defaultOffsetIsZero(): void { diff --git a/libs/types/tests/Shape/ExplicitFieldNodeTest.php b/libs/types/tests/Shape/ExplicitFieldNodeTest.php index 347d9f0..97f12ae 100644 --- a/libs/types/tests/Shape/ExplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ExplicitFieldNodeTest.php @@ -101,7 +101,6 @@ public function indexIsRecalculatedAfterKeyMutation(): void public function explicitFieldIsRequiredByDefault(ExplicitFieldNode $field, string $index): void { self::assertFalse($field->isOptional); - self::assertSame('required', (string) $field); } #[Test] @@ -120,12 +119,11 @@ public function explicitFieldIsAFieldNode(ExplicitFieldNode $field, string $inde } #[Test] - public function optionalFieldIsStringifiedAsOptional(): void + public function optionalFieldStoresTheFlag(): void { $field = new NamedFieldNode(new Identifier('key'), self::type(), true); self::assertTrue($field->isOptional); - self::assertSame('optional', (string) $field); } #[Test] @@ -144,7 +142,6 @@ public function implicitFieldHasNoKey(): void self::assertInstanceOf(FieldNode::class, $field); self::assertNotInstanceOf(ExplicitFieldNode::class, $field); - self::assertSame('required', (string) $field); } #[Test] diff --git a/libs/types/tests/Shape/FieldsListNodeTest.php b/libs/types/tests/Shape/FieldsListNodeTest.php index 2d3e834..111c50e 100644 --- a/libs/types/tests/Shape/FieldsListNodeTest.php +++ b/libs/types/tests/Shape/FieldsListNodeTest.php @@ -33,7 +33,7 @@ public function sealedByDefault(): void { $list = new FieldsListNode(); - self::assertTrue($list->sealed); + self::assertTrue($list->isSealed); } #[Test] @@ -41,7 +41,7 @@ public function constructorAcceptsUnsealedFlag(): void { $list = new FieldsListNode([], false); - self::assertFalse($list->sealed); + self::assertFalse($list->isSealed); } #[Test] @@ -56,22 +56,6 @@ public function constructorAcceptsFields(): void self::assertSame($b, $list->last()); } - #[Test] - public function toStringReturnsSealedWhenSealed(): void - { - $list = new FieldsListNode([], true); - - self::assertSame('sealed', (string) $list); - } - - #[Test] - public function toStringReturnsUnsealedWhenNotSealed(): void - { - $list = new FieldsListNode([], false); - - self::assertSame('unsealed', (string) $list); - } - #[Test] public function findIndexReturnsCorrectPosition(): void { diff --git a/libs/types/tests/Shape/ImplicitFieldNodeTest.php b/libs/types/tests/Shape/ImplicitFieldNodeTest.php index 51bfd85..f6d7193 100644 --- a/libs/types/tests/Shape/ImplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ImplicitFieldNodeTest.php @@ -40,25 +40,6 @@ public function constructorStoresOptionalFlag(): void self::assertTrue($node->isOptional); } - #[Test] - public function toStringReturnsRequiredWhenNotOptional(): void - { - $node = new ImplicitFieldNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('required', (string) $node); - } - - #[Test] - public function toStringReturnsOptionalWhenOptional(): void - { - $node = new ImplicitFieldNode( - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertSame('optional', (string) $node); - } - #[Test] public function attributesDefaultToNull(): void { diff --git a/libs/types/tests/Shape/NamedFieldNodeTest.php b/libs/types/tests/Shape/NamedFieldNodeTest.php index 592c0ee..fb6d6b8 100644 --- a/libs/types/tests/Shape/NamedFieldNodeTest.php +++ b/libs/types/tests/Shape/NamedFieldNodeTest.php @@ -56,29 +56,6 @@ public function constructorStoresOptionalFlag(): void self::assertTrue($node->isOptional); } - #[Test] - public function toStringReturnsRequiredWhenNotOptional(): void - { - $node = new NamedFieldNode( - new Identifier('field'), - new NamedTypeNode(Name::createFromString('string')), - ); - - self::assertSame('required', (string) $node); - } - - #[Test] - public function toStringReturnsOptionalWhenOptional(): void - { - $node = new NamedFieldNode( - new Identifier('field'), - new NamedTypeNode(Name::createFromString('string')), - true, - ); - - self::assertSame('optional', (string) $node); - } - #[Test] public function defaultOffsetIsZero(): void { From ab3813e07e2dc5964eaf2c7b3bd63988dd5988ce Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 20:54:00 +0300 Subject: [PATCH 020/119] Add more tests for AST --- .../Callable/CallableParameterNodeTest.php | 18 ++++ libs/types/tests/NameTest.php | 94 +++++++++++++++++++ libs/types/tests/NodeListTest.php | 34 +++++++ libs/types/tests/NodeTest.php | 19 ++++ .../tests/Shape/ExplicitFieldNodeTest.php | 26 +++++ 5 files changed, 191 insertions(+) diff --git a/libs/types/tests/Callable/CallableParameterNodeTest.php b/libs/types/tests/Callable/CallableParameterNodeTest.php index da520b8..9c083ab 100644 --- a/libs/types/tests/Callable/CallableParameterNodeTest.php +++ b/libs/types/tests/Callable/CallableParameterNodeTest.php @@ -9,6 +9,7 @@ use TypeLang\Type\Literal\VariableLiteralNode; use TypeLang\Type\Name; use TypeLang\Type\NamedTypeNode; +use TypeLang\Type\Node; use TypeLang\Type\Tests\TestCase; final class CallableParameterNodeTest extends TestCase @@ -112,4 +113,21 @@ public function defaultOffsetIsZero(): void self::assertSame(0, $node->offset); } + + #[Test] + public function isReturnsTrueForOwnClass(): void + { + $parameter = new CallableParameterNode(new NamedTypeNode(Name::createFromString('int'))); + + self::assertTrue($parameter->is(CallableParameterNode::class)); + self::assertTrue($parameter->is(Node::class)); + } + + #[Test] + public function isReturnsFalseForAnotherClass(): void + { + $parameter = new CallableParameterNode(new NamedTypeNode(Name::createFromString('int'))); + + self::assertFalse($parameter->is(NamedTypeNode::class)); + } } diff --git a/libs/types/tests/NameTest.php b/libs/types/tests/NameTest.php index f51f5ea..1c924b4 100644 --- a/libs/types/tests/NameTest.php +++ b/libs/types/tests/NameTest.php @@ -530,4 +530,98 @@ public function stringCastIsTheSameAsToStringMethod(): void self::assertSame($name->toString(), (string) $name); } + + #[Test] + public function getFirstPartReturnsTheFirstSegment(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + self::assertSame($name->first, $name->getFirstPart()); + self::assertSame('Foo', $name->getFirstPart()->value); + } + + #[Test] + public function getFirstPartAsStringReturnsTheFirstSegmentValue(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame('Foo', $name->getFirstPartAsString()); + } + + #[Test] + public function getFirstPartAsLowerStringLowercasesTheFirstSegment(): void + { + $name = Name::createFromString('FooBar\Baz'); + + self::assertSame('foobar', $name->getFirstPartAsLowerString()); + } + + #[Test] + public function getLastPartReturnsTheLastSegment(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + self::assertSame($name->last, $name->getLastPart()); + self::assertSame('Baz', $name->getLastPart()->value); + } + + #[Test] + public function getLastPartAsStringReturnsTheLastSegmentValue(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame('Bar', $name->getLastPartAsString()); + } + + #[Test] + public function getLastPartAsLowerStringLowercasesTheLastSegment(): void + { + $name = Name::createFromString('Foo\BarBaz'); + + self::assertSame('barbaz', $name->getLastPartAsLowerString()); + } + + #[Test] + public function partsOfASimpleNameAreTheSameSegment(): void + { + $name = Name::createFromString('Foo'); + + self::assertSame($name->first, $name->last); + } + + #[Test] + public function toArrayReturnsTheSegments(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame($name->parts, $name->toArray()); + self::assertContainsOnlyInstancesOf(Identifier::class, $name->toArray()); + } + + #[Test] + public function isFullQualifiedIsAnAliasOfTheProperty(): void + { + $qualified = Name::createFromString('\Foo\Bar'); + $unqualified = Name::createFromString('Foo\Bar'); + + self::assertTrue($qualified->isFullQualified()); + self::assertFalse($unqualified->isFullQualified()); + } + + #[Test] + public function getPartsIsAnAliasOfTheProperty(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame($name->parts, $name->getParts()); + } + + #[Test] + public function getPartsAsStringIsAnAliasOfToArrayStrings(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame(['Foo', 'Bar'], $name->getPartsAsString()); + self::assertSame($name->toArrayStrings(), $name->getPartsAsString()); + } } diff --git a/libs/types/tests/NodeListTest.php b/libs/types/tests/NodeListTest.php index 007aaae..57e7b34 100644 --- a/libs/types/tests/NodeListTest.php +++ b/libs/types/tests/NodeListTest.php @@ -245,4 +245,38 @@ public function defaultOffsetIsZero(): void { self::assertSame(0, $this->list()->offset); } + + #[Test] + public function offsetGetIsAnAliasOfTheArrayAccess(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + self::assertSame($a, $list->offsetGet(0)); + self::assertNull($list->offsetGet(1)); + } + + #[Test] + public function offsetSetIsAnAliasOfTheArrayAccess(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list->offsetSet(0, $b); + + self::assertSame([$b], $list->items); + } + + #[Test] + public function offsetUnsetIsAnAliasOfTheArrayAccess(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a, $b]); + + $list->offsetUnset(0); + + self::assertSame([$b], $list->items); + } } diff --git a/libs/types/tests/NodeTest.php b/libs/types/tests/NodeTest.php index e70c4b0..7eb6411 100644 --- a/libs/types/tests/NodeTest.php +++ b/libs/types/tests/NodeTest.php @@ -245,4 +245,23 @@ public function wrappingTypeNodeTypeIsMutable(): void self::assertSame('B', $node->type->name->toString()); } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeOffsetIsReadableByMethod(Node $node): void + { + self::assertSame(0, $node->getOffset()); + + $node->offset = 42; + + self::assertSame(42, $node->getOffset()); + } + + #[Test] + public function abstractNodeOffsetIsReadableByMethod(): void + { + $node = new class extends Node {}; + + self::assertSame(0, $node->getOffset()); + } } diff --git a/libs/types/tests/Shape/ExplicitFieldNodeTest.php b/libs/types/tests/Shape/ExplicitFieldNodeTest.php index 97f12ae..83bb168 100644 --- a/libs/types/tests/Shape/ExplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ExplicitFieldNodeTest.php @@ -187,4 +187,30 @@ public function fieldTypeIsMutable(): void self::assertSame('B', $field->type->name->toString()); } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function isReturnsTrueForOwnClass(ExplicitFieldNode $field, string $index): void + { + self::assertTrue($field->is($field::class)); + self::assertTrue($field->is(ExplicitFieldNode::class)); + self::assertTrue($field->is(FieldNode::class)); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function isReturnsFalseForAnotherClass(ExplicitFieldNode $field, string $index): void + { + self::assertFalse($field->is(ImplicitFieldNode::class)); + } + + #[Test] + public function implicitFieldIsNotAnExplicitOne(): void + { + $field = new ImplicitFieldNode(self::type()); + + self::assertTrue($field->is(ImplicitFieldNode::class)); + self::assertTrue($field->is(FieldNode::class)); + self::assertFalse($field->is(ExplicitFieldNode::class)); + } } From 83ee79b8a83255bd81a313135d722dc9f8d77667 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 21:02:26 +0300 Subject: [PATCH 021/119] Improve AST nodes behaviour --- libs/types/src/Identifier.php | 9 +++- libs/types/src/Name.php | 25 +++++++-- libs/types/tests/IdentifierTest.php | 16 ++++++ libs/types/tests/NameTest.php | 84 ++++++++++++++++++++++++++--- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/libs/types/src/Identifier.php b/libs/types/src/Identifier.php index 3a01c58..d3c2e4f 100644 --- a/libs/types/src/Identifier.php +++ b/libs/types/src/Identifier.php @@ -38,12 +38,19 @@ final class Identifier extends Node implements \Stringable 'false', ]; + /** + * @throws \InvalidArgumentException in case of an empty value + */ public function __construct( /** * @var non-empty-string */ public readonly string $value, - ) {} + ) { + if ($value === '') { + throw new \InvalidArgumentException('Name identifier cannot be empty'); + } + } public static function createFromString(string|\Stringable $value): self { diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index aa96952..0c20d90 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -28,6 +28,8 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String /** * @param iterable $parts + * @throws \InvalidArgumentException in case of parts are empty or contain + * something else than an {@see Identifier} */ public function __construct( iterable $parts, @@ -39,12 +41,24 @@ public function __construct( default => \array_values($parts), }; - \assert($parts !== [], new \InvalidArgumentException('Name parts count can not be empty')); + if ($parts === []) { + throw new \InvalidArgumentException('Name parts count can not be empty'); + } + + $first = \reset($parts); + $last = \end($parts); + + if (!$first instanceof Identifier || !$last instanceof Identifier) { + throw new \InvalidArgumentException(\sprintf( + 'Name parts must contain only %s instances', + Identifier::class, + )); + } $this->parts = $parts; - $this->first = \reset($parts); - $this->last = \end($parts); + $this->first = $first; + $this->last = $last; } public static function createFromString(string|\Stringable $name): self @@ -413,10 +427,13 @@ public function __serialize(): array */ public function __unserialize(array $data): void { - $this->parts = $data[0] ?? throw new \UnexpectedValueException( + $this->parts = $parts = $data[0] ?? throw new \UnexpectedValueException( message: 'Unable to unserialize Name segments', ); + $this->first = \reset($parts); + $this->last = \end($parts); + $this->offset = $data[1] ?? 0; $this->isFullyQualified = $data[2] ?? self::IS_FULLY_QUALIFIED_DEFAULT_VALUE; } diff --git a/libs/types/tests/IdentifierTest.php b/libs/types/tests/IdentifierTest.php index dad1a1a..0941629 100644 --- a/libs/types/tests/IdentifierTest.php +++ b/libs/types/tests/IdentifierTest.php @@ -342,4 +342,20 @@ public function toLowerStringDoesNotModifyTheValue(): void self::assertSame('someclass', $id->toLowerString()); self::assertSame('SomeClass', $id->value); } + + #[Test] + public function constructorThrowsOnEmptyValue(): void + { + $this->expectException(\InvalidArgumentException::class); + + new Identifier(''); + } + + #[Test] + public function constructorKeepsSurroundingWhitespace(): void + { + $id = new Identifier(' A '); + + self::assertSame(' A ', $id->value); + } } diff --git a/libs/types/tests/NameTest.php b/libs/types/tests/NameTest.php index 1c924b4..81383fe 100644 --- a/libs/types/tests/NameTest.php +++ b/libs/types/tests/NameTest.php @@ -285,8 +285,6 @@ public function serializeAndUnserializeRoundtrip(): void #[Test] public function constructorThrowsOnEmptySegmentsArray(): void { - self::skipWhenAssertsAreDisabled(); - $this->expectException(\InvalidArgumentException::class); new Name([]); @@ -367,8 +365,6 @@ public function __toString(): string #[Test] public function createFromStringThrowsOnEmptyString(): void { - self::skipWhenAssertsAreDisabled(); - $this->expectException(\InvalidArgumentException::class); Name::createFromString(''); @@ -377,8 +373,6 @@ public function createFromStringThrowsOnEmptyString(): void #[Test] public function createFromStringThrowsOnDelimiterOnlyString(): void { - self::skipWhenAssertsAreDisabled(); - $this->expectException(\InvalidArgumentException::class); Name::createFromString('\\'); @@ -624,4 +618,82 @@ public function getPartsAsStringIsAnAliasOfToArrayStrings(): void self::assertSame(['Foo', 'Bar'], $name->getPartsAsString()); self::assertSame($name->toArrayStrings(), $name->getPartsAsString()); } + + #[Test] + public function serializationRoundtripRestoresBoundaryParts(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + /** @var Name $restored */ + $restored = \unserialize(\serialize($name)); + + self::assertSame('Foo', $restored->first->value); + self::assertSame('Baz', $restored->last->value); + self::assertSame('Foo', $restored->getFirstPart()->value); + self::assertSame('Baz', $restored->getLastPart()->value); + } + + #[Test] + public function unserializedNameIsFullyUsable(): void + { + $name = Name::createFromString('self'); + + /** @var Name $restored */ + $restored = \unserialize(\serialize($name)); + + self::assertTrue($restored->isSimple()); + self::assertTrue($restored->isSpecial()); + self::assertFalse($restored->isBuiltin()); + self::assertSame('self', $restored->getFirstPartAsString()); + } + + #[Test] + public function unserializeThrowsOnEmptyParts(): void + { + $name = Name::createFromString('Foo'); + + $this->expectException(\UnexpectedValueException::class); + + $name->__unserialize([[]]); + } + + #[Test] + public function unserializeThrowsOnNonIdentifierParts(): void + { + $name = Name::createFromString('Foo'); + + $this->expectException(\UnexpectedValueException::class); + + /** @phpstan-ignore-next-line */ + $name->__unserialize([['Foo']]); + } + + #[Test] + public function constructorThrowsOnNonIdentifierParts(): void + { + $this->expectException(\InvalidArgumentException::class); + + /** @phpstan-ignore-next-line */ + new Name(['Foo']); + } + + #[Test] + public function sliceThrowsWhenTheResultIsEmpty(): void + { + $name = Name::createFromString('Foo\Bar'); + + $this->expectException(\InvalidArgumentException::class); + + $name->slice(5); + } + + #[Test] + public function sliceOfZeroLengthThrows(): void + { + $name = Name::createFromString('Foo\Bar'); + + $this->expectException(\InvalidArgumentException::class); + + $name->slice(0, 0); + } } From c44d55a353c743ff526974b1ae13e49e44f1630c Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 21:02:37 +0300 Subject: [PATCH 022/119] Fix printer --- libs/printer/src/PrettyTypePrinter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/printer/src/PrettyTypePrinter.php b/libs/printer/src/PrettyTypePrinter.php index 2948dfa..0980bfa 100644 --- a/libs/printer/src/PrettyTypePrinter.php +++ b/libs/printer/src/PrettyTypePrinter.php @@ -203,7 +203,7 @@ private function getShapeFieldsNodes(NamedTypeNode $node, FieldsListNode $shape, $fields[] = $current . $prefix . $this->printShapeFieldNode($field); } - if (!$shape->sealed || $node->arguments !== null) { + if (!$shape->isSealed || $node->arguments !== null) { $prefix .= '...'; if ($node->arguments !== null) { From 5c369d12ba8ed2b24b5371a5fb18d7f78f682815 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 21:02:55 +0300 Subject: [PATCH 023/119] Fix doc block comments --- libs/phpdoc/src/DocBlock/DocBlock.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/phpdoc/src/DocBlock/DocBlock.php b/libs/phpdoc/src/DocBlock/DocBlock.php index 961dccb..d1136a8 100644 --- a/libs/phpdoc/src/DocBlock/DocBlock.php +++ b/libs/phpdoc/src/DocBlock/DocBlock.php @@ -48,7 +48,7 @@ final class DocBlock implements * value is empty. * * ``` - * foreach ($block->getTags() as $tag) { + * foreach ($block->tags as $tag) { * printf('@%s => %s', $tag->name, $tag->description); * } * ``` @@ -158,7 +158,7 @@ public function __toString(): string $result[] = (string) $this->description; } - foreach ($this->getTags() as $tag) { + foreach ($this->tags as $tag) { $result[] = (string) $tag; } From 3f4c3ab8cdeeecd709150a946971d74925a38185 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 21:26:40 +0300 Subject: [PATCH 024/119] Add more Name AST tests --- libs/types/tests/NameTest.php | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/libs/types/tests/NameTest.php b/libs/types/tests/NameTest.php index 81383fe..216ac4d 100644 --- a/libs/types/tests/NameTest.php +++ b/libs/types/tests/NameTest.php @@ -650,22 +650,43 @@ public function unserializedNameIsFullyUsable(): void #[Test] public function unserializeThrowsOnEmptyParts(): void { - $name = Name::createFromString('Foo'); - - $this->expectException(\UnexpectedValueException::class); + $this->expectException(\Throwable::class); - $name->__unserialize([[]]); + \unserialize(self::payload([], 0, false)); } #[Test] public function unserializeThrowsOnNonIdentifierParts(): void { - $name = Name::createFromString('Foo'); + $this->expectException(\Throwable::class); + \unserialize(self::payload(['Foo'], 0, false)); + } + + #[Test] + public function unserializeThrowsOnMissingParts(): void + { $this->expectException(\UnexpectedValueException::class); - /** @phpstan-ignore-next-line */ - $name->__unserialize([['Foo']]); + \unserialize(self::payload()); + } + + /** + * Builds a serialized {@see Name} payload from the given + * {@see Name::__serialize()} data. + * + * @return non-empty-string + */ + private static function payload(mixed ...$data): string + { + $body = \serialize($data); + + return \vsprintf('O:%d:"%s":%d:%s', [ + \strlen(Name::class), + Name::class, + \count($data), + \substr($body, (int) \strpos($body, '{')), + ]); } #[Test] From 2b2111085f9723aa2ee56bccc4c722d287293dab Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 21:27:10 +0300 Subject: [PATCH 025/119] Actualize parser DumperVisitor --- libs/parser/resources/grammar.php | 2 +- libs/parser/src/Traverser/DumperVisitor.php | 44 ++++++++++++++++++- .../SimplePropertyAccessor.php | 2 +- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/libs/parser/resources/grammar.php b/libs/parser/resources/grammar.php index 98be68a..bb9e0e6 100644 --- a/libs/parser/resources/grammar.php +++ b/libs/parser/resources/grammar.php @@ -600,7 +600,7 @@ : new Type\Shape\FieldsListNode(); if ($children !== []) { - $fields->sealed = false; + $fields->isSealed = false; } return \array_filter([$parameters, $fields]); diff --git a/libs/parser/src/Traverser/DumperVisitor.php b/libs/parser/src/Traverser/DumperVisitor.php index 84780d1..a51c2da 100644 --- a/libs/parser/src/Traverser/DumperVisitor.php +++ b/libs/parser/src/Traverser/DumperVisitor.php @@ -35,7 +35,9 @@ public function enter(Node $node): ?Command $suffix = \str_replace($this->simplifyNodeNamespace, '', $node::class); if ($node instanceof \Stringable) { - $suffix .= \sprintf('(%s)', (string) $node); + $suffix .= $this->printStringableNodeSuffix($node); + } else { + $suffix .= $this->printNodePropertiesSuffix($node); } $this->write($prefix . $suffix . "\n"); @@ -43,6 +45,46 @@ public function enter(Node $node): ?Command return null; } + /** + * @param \Stringable $node + * @return non-empty-string + */ + private function printStringableNodeSuffix(\Stringable $node): string + { + return \sprintf('(%s)', (string) $node); + } + + /** + * Returns a "(prop=value, ...)" suffix built from the writable scalar + * properties of the node, or an empty string in case of there are none. + */ + private function printNodePropertiesSuffix(Node $node): string + { + $result = []; + + foreach ((new \ReflectionObject($node))->getProperties() as $property) { + // Skip readonly + static and builtin "offset" properties + if ($property->isStatic() || $property->isReadOnly() || $property->getName() === 'offset') { + continue; + } + + $value = $property->getValue($node); + + // Skip non-scalar properties + if (!\is_scalar($value)) { + continue; + } + + $result[] = \sprintf('%s=%s', $property->getName(), \var_export($value, true)); + } + + if ($result === []) { + return ''; + } + + return \sprintf('(%s)', \implode(', ', $result)); + } + public function leave(Node $node): void { // @phpstan-ignore-next-line : $depth is always non-negative diff --git a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php index fb214f9..e16081c 100644 --- a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php +++ b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php @@ -11,7 +11,7 @@ final class SimplePropertyAccessor implements PropertyAccessorInterface */ private function shouldSkip(\ReflectionProperty $property): bool { - if ($property->isStatic()) { + if ($property->isStatic() || $property->isReadOnly()) { return true; } From 6c684331615d2666210a6ee5be349a38665cf414 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 21:27:16 +0300 Subject: [PATCH 026/119] Fix tests --- .../tests/Exception/ParseExceptionTest.php | 218 ++++++++++ .../tests/Exception/SemanticExceptionTest.php | 192 +++++++++ libs/parser/tests/Syntax/AttributeTest.php | 30 +- libs/parser/tests/Syntax/CallableTest.php | 57 +-- libs/parser/tests/Syntax/ConditionalTest.php | 7 - libs/parser/tests/Syntax/ConstantTest.php | 16 +- libs/parser/tests/Syntax/GenericTest.php | 29 -- libs/parser/tests/Syntax/ListTest.php | 2 - libs/parser/tests/Syntax/LogicalTest.php | 13 - libs/parser/tests/Syntax/NameTest.php | 6 - libs/parser/tests/Syntax/OffsetAccessTest.php | 27 +- libs/parser/tests/Syntax/ShapeTest.php | 73 +--- .../tests/Traverser/MatcherVisitorTest.php | 195 +++++++++ .../Traverser/StringDumperVisitorTest.php | 236 +++++++++++ libs/parser/tests/Traverser/TraverserTest.php | 401 ++++++++++++++++++ .../tests/Traverser/TypeMapVisitorTest.php | 169 ++++++++ libs/parser/tests/TypeParserFeaturesTest.php | 192 +++++++++ libs/parser/tests/TypeParserTest.php | 152 +++++++ .../TypeResolver/NamespaceFinderTest.php | 154 +++++++ .../PhpUseStatementsReaderTest.php | 48 +++ .../ReflectionSourcePrefixReaderTest.php | 102 +++++ .../TypeResolver/Stub/ClassWithMethodStub.php | 17 + .../tests/TypeResolver/Stub/functions.php | 16 + .../TypeResolver/TypeResolverImportsTest.php | 193 +++++++++ .../tests/TypeResolver/TypeResolverTest.php | 46 +- 25 files changed, 2334 insertions(+), 257 deletions(-) create mode 100644 libs/parser/tests/Exception/ParseExceptionTest.php create mode 100644 libs/parser/tests/Exception/SemanticExceptionTest.php create mode 100644 libs/parser/tests/Traverser/MatcherVisitorTest.php create mode 100644 libs/parser/tests/Traverser/StringDumperVisitorTest.php create mode 100644 libs/parser/tests/Traverser/TraverserTest.php create mode 100644 libs/parser/tests/Traverser/TypeMapVisitorTest.php create mode 100644 libs/parser/tests/TypeParserFeaturesTest.php create mode 100644 libs/parser/tests/TypeParserTest.php create mode 100644 libs/parser/tests/TypeResolver/NamespaceFinderTest.php create mode 100644 libs/parser/tests/TypeResolver/ReflectionSourcePrefixReaderTest.php create mode 100644 libs/parser/tests/TypeResolver/Stub/ClassWithMethodStub.php create mode 100644 libs/parser/tests/TypeResolver/Stub/functions.php create mode 100644 libs/parser/tests/TypeResolver/TypeResolverImportsTest.php diff --git a/libs/parser/tests/Exception/ParseExceptionTest.php b/libs/parser/tests/Exception/ParseExceptionTest.php new file mode 100644 index 0000000..7e91e4f --- /dev/null +++ b/libs/parser/tests/Exception/ParseExceptionTest.php @@ -0,0 +1,218 @@ +getMessage(), + ); + self::assertSame(ParseException::ERROR_CODE_UNEXPECTED_TOKEN, $exception->getCode()); + } + + #[Test] + public function theStatementIsOmittedInCaseOfItEqualsTheToken(): void + { + $exception = UnexpectedTokenException::becauseTokenIsUnexpected('foo', 'foo', 0); + + self::assertSame('Syntax error, unexpected "foo" at column 1', $exception->getMessage()); + } + + #[Test] + public function theEndOfInputIsReportedInsteadOfANullToken(): void + { + $exception = UnexpectedTokenException::becauseTokenIsUnexpected("\0", 'int|', 4); + + self::assertSame( + 'Syntax error, unexpected end of input in "int|" at column 5', + $exception->getMessage(), + ); + } + + #[Test] + public function theEndOfInputIsReportedInsteadOfAnEmptyToken(): void + { + $exception = UnexpectedTokenException::becauseTokenIsUnexpected('', 'int|', 4); + + self::assertStringContainsString('unexpected end of input', $exception->getMessage()); + } + + #[Test] + public function theDoubleQuoteTokenIsReportedByItsName(): void + { + $exception = UnexpectedTokenException::becauseTokenIsUnexpected('"', 'a"', 1); + + self::assertStringContainsString('unexpected double quote (")', $exception->getMessage()); + } + + #[Test] + public function theMultilineStatementIsReportedUsingTheLineAndColumn(): void + { + $exception = UnexpectedTokenException::becauseTokenIsUnexpected('x', "int|\nx", 5); + + self::assertStringEndsWith('on line 2 at column 1', $exception->getMessage()); + } + + #[Test] + public function theLongStatementIsTruncated(): void + { + $statement = \str_repeat('x', 100) . 'foo'; + $exception = UnexpectedTokenException::becauseTokenIsUnexpected('foo', $statement, 100); + + self::assertStringContainsString('…', $exception->getMessage()); + self::assertStringNotContainsString($statement, $exception->getMessage()); + } + + #[Test] + public function theUnrecognizedTokenIsReportedWithItsLocation(): void + { + $exception = UnrecognizedTokenException::becauseTokenIsUnrecognized('%', 'int|%', 4); + + self::assertSame( + 'Syntax error, unrecognized "%" in "int|%" at column 5', + $exception->getMessage(), + ); + self::assertSame(ParseException::ERROR_CODE_UNRECOGNIZED_TOKEN, $exception->getCode()); + } + + #[Test] + public function theUnrecognizedSyntaxIsReportedWithItsLocation(): void + { + $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized('int|', 4); + + self::assertSame( + 'Internal syntax error, in "int|" at column 5', + $exception->getMessage(), + ); + self::assertSame(ParseException::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR, $exception->getCode()); + } + + #[Test] + public function theBlankStatementIsReportedAsEmpty(): void + { + $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized(' ', 0); + + self::assertStringContainsString('', $exception->getMessage()); + } + + #[Test] + public function theInternalErrorKeepsThePreviousException(): void + { + $previous = new \LogicException('oops'); + $exception = InternalParseException::becauseInternalErrorOccurs('int', $previous); + + self::assertSame( + 'An internal error occurred while parsing "int"', + $exception->getMessage(), + ); + self::assertSame($previous, $exception->getPrevious()); + self::assertSame(ParseException::ERROR_CODE_INTERNAL_ERROR, $exception->getCode()); + } + + #[Test] + public function theUnreadableStatementIsReported(): void + { + $exception = InternalParseException::becauseTypeStatementIsUnreadable(); + + self::assertSame('Could not read type statement', $exception->getMessage()); + self::assertSame(ParseException::ERROR_CODE_INTERNAL_ERROR, $exception->getCode()); + self::assertNull($exception->getPrevious()); + } + + #[Test] + public function theUnreadableSourceIsReportedUsingTheOriginalMessage(): void + { + $previous = new class ('source is unreadable') extends \RuntimeException implements + \Phplrt\Contracts\Source\SourceExceptionInterface {}; + + $exception = InternalParseException::becauseSourceIsUnreadable($previous); + + self::assertSame('source is unreadable', $exception->getMessage()); + self::assertSame($previous, $exception->getPrevious()); + self::assertSame(ParseException::ERROR_CODE_INTERNAL_ERROR, $exception->getCode()); + } + + #[Test] + public function theSemanticErrorIsRebasedOntoTheWholeSource(): void + { + $exception = SemanticParseException::becauseSemanticErrorOccurs( + e: ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a', 10), + source: (new SourceFactory())->create('array{a: int, a: int}'), + ); + + self::assertSame( + 'Duplicate key "a" in "array{a: int, a: int}" at column 11', + $exception->getMessage(), + ); + } + + #[Test] + public function theSemanticErrorCodeIsShiftedByTheBaseValue(): void + { + $semantic = ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a', 10); + + $exception = SemanticParseException::becauseSemanticErrorOccurs( + e: $semantic, + source: (new SourceFactory())->create('array{a: int, a: int}'), + ); + + self::assertSame( + ParseException::ERROR_CODE_SEMANTIC_ERROR_BASE + $semantic->getCode(), + $exception->getCode(), + ); + } + + #[Test] + public function theSemanticErrorMessageIsCapitalized(): void + { + $exception = SemanticParseException::becauseSemanticErrorOccurs( + e: ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a'), + source: (new SourceFactory())->create('array{a: int, a: int}'), + ); + + self::assertStringStartsWith('Duplicate', $exception->getMessage()); + } + + #[Test] + public function everyErrorCodeIsUnique(): void + { + $codes = [ + ParseException::ERROR_CODE_UNEXPECTED_TOKEN, + ParseException::ERROR_CODE_UNRECOGNIZED_TOKEN, + ParseException::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR, + ParseException::ERROR_CODE_INTERNAL_ERROR, + ParseException::ERROR_CODE_SEMANTIC_ERROR_BASE, + ]; + + self::assertSame($codes, \array_values(\array_unique($codes))); + } +} diff --git a/libs/parser/tests/Exception/SemanticExceptionTest.php b/libs/parser/tests/Exception/SemanticExceptionTest.php new file mode 100644 index 0000000..448d5cc --- /dev/null +++ b/libs/parser/tests/Exception/SemanticExceptionTest.php @@ -0,0 +1,192 @@ + + */ + public static function factoryDataProvider(): iterable + { + yield 'feature not allowed' => [ + static fn(int $offset): SemanticException + => FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes', $offset), + ]; + + yield 'unexpected sub-node' => [ + static fn(int $offset): SemanticException + => InternalSemanticException::becauseSubNodeIsUnexpected('Example', $offset), + ]; + + yield 'invalid conditional operator' => [ + static fn(int $offset): SemanticException + => InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~', $offset), + ]; + + yield 'shape field duplication' => [ + static fn(int $offset): SemanticException + => ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key', $offset), + ]; + + yield 'shape keys mixing' => [ + static fn(int $offset): SemanticException + => ShapeKeysMixingException::becauseShapeKeysAreMixed($offset), + ]; + + yield 'variadic redefinition' => [ + static fn(int $offset): SemanticException + => VariadicRedefinitionException::becauseVariadicIsRedefined($offset), + ]; + + yield 'variadic with default' => [ + static fn(int $offset): SemanticException + => VariadicWithDefaultException::becauseVariadicHasDefault($offset), + ]; + } + + /** + * @param \Closure(int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionIsAParserException(\Closure $factory): void + { + $exception = $factory(0); + + self::assertInstanceOf(ParserExceptionInterface::class, $exception); + self::assertInstanceOf(\LogicException::class, $exception); + } + + /** + * @param \Closure(int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionKeepsTheOffset(\Closure $factory): void + { + $exception = $factory(42); + + self::assertSame(42, $exception->offset); + self::assertSame(42, $exception->getOffset()); + } + + /** + * @param \Closure(int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionHasANonEmptyMessage(\Closure $factory): void + { + self::assertNotSame('', $factory(0)->getMessage()); + } + + #[Test] + public function theFeatureNameIsCapitalizedInTheMessage(): void + { + $exception = FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields'); + + self::assertSame('Shape fields not allowed', $exception->getMessage()); + } + + #[Test] + public function theFeatureNotAllowedOffsetDefaultsToZero(): void + { + self::assertSame(0, FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes')->offset); + } + + #[Test] + public function theUnexpectedSubNodeIsReported(): void + { + $exception = InternalSemanticException::becauseSubNodeIsUnexpected('Example'); + + self::assertSame( + 'Internal error, unexpected square bracket sub-node Example', + $exception->getMessage(), + ); + } + + #[Test] + public function theInvalidConditionalOperatorIsReported(): void + { + $exception = InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~'); + + self::assertSame('Invalid conditional operator "~"', $exception->getMessage()); + self::assertSame(SemanticException::ERROR_CODE_INVALID_OPERATOR, $exception->getCode()); + } + + #[Test] + public function theDuplicatedShapeFieldIsReported(): void + { + $exception = ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key'); + + self::assertSame('Duplicate key "key"', $exception->getMessage()); + self::assertSame(SemanticException::ERROR_CODE_SHAPE_KEY_DUPLICATION, $exception->getCode()); + } + + #[Test] + public function theMixedShapeKeysAreReported(): void + { + $exception = ShapeKeysMixingException::becauseShapeKeysAreMixed(); + + self::assertSame('Cannot mix explicit and implicit shape keys', $exception->getMessage()); + self::assertSame(SemanticException::ERROR_CODE_SHAPE_KEY_MIX, $exception->getCode()); + } + + #[Test] + public function theRedefinedVariadicIsReported(): void + { + $exception = VariadicRedefinitionException::becauseVariadicIsRedefined(); + + self::assertSame( + 'Either prefix or postfix variadic syntax should be used, but not both', + $exception->getMessage(), + ); + self::assertSame(SemanticException::ERROR_CODE_VARIADIC_ALREADY_VARIADIC, $exception->getCode()); + } + + #[Test] + public function theVariadicWithADefaultIsReported(): void + { + $exception = VariadicWithDefaultException::becauseVariadicHasDefault(); + + self::assertSame('Cannot have variadic param with a default', $exception->getMessage()); + self::assertSame(SemanticException::ERROR_CODE_VARIADIC_WITH_DEFAULT, $exception->getCode()); + } + + #[Test] + public function theInternalErrorsHaveNoDedicatedCode(): void + { + self::assertSame(0, InternalSemanticException::becauseSubNodeIsUnexpected('Example')->getCode()); + self::assertSame(0, FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes')->getCode()); + } + + #[Test] + public function everyErrorCodeIsUnique(): void + { + $codes = [ + SemanticException::ERROR_CODE_SHAPE_KEY_DUPLICATION, + SemanticException::ERROR_CODE_SHAPE_KEY_MIX, + SemanticException::ERROR_CODE_VARIADIC_WITH_DEFAULT, + SemanticException::ERROR_CODE_VARIADIC_ALREADY_VARIADIC, + SemanticException::ERROR_CODE_INVALID_OPERATOR, + ]; + + self::assertSame($codes, \array_values(\array_unique($codes))); + } +} diff --git a/libs/parser/tests/Syntax/AttributeTest.php b/libs/parser/tests/Syntax/AttributeTest.php index acf377a..73e6432 100644 --- a/libs/parser/tests/Syntax/AttributeTest.php +++ b/libs/parser/tests/Syntax/AttributeTest.php @@ -21,17 +21,14 @@ public function testSingleAttributeOnTemplateArgument(): void self::assertSame(<<<'AST' NamedTypeNode Name(HashMap) - Identifier(HashMap) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(T) - Identifier(T) Attribute\AttributeGroupListNode Attribute\AttributeGroupNode Attribute\AttributeNode Name(name) - Identifier(name) AST, $this->parseAndPrint('HashMap<#[name("key")] T>')); } @@ -40,20 +37,16 @@ public function testMultipleAttributesInOneGroup(): void self::assertSame(<<<'AST' NamedTypeNode Name(HashMap) - Identifier(HashMap) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(T) - Identifier(T) Attribute\AttributeGroupListNode Attribute\AttributeGroupNode Attribute\AttributeNode Name(name) - Identifier(name) Attribute\AttributeNode Name(out) - Identifier(out) AST, $this->parseAndPrint('HashMap<#[name("key"), out] T>')); } @@ -62,21 +55,17 @@ public function testMultipleAttributeGroups(): void self::assertSame(<<<'AST' NamedTypeNode Name(HashMap) - Identifier(HashMap) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(T) - Identifier(T) Attribute\AttributeGroupListNode Attribute\AttributeGroupNode Attribute\AttributeNode Name(name) - Identifier(name) Attribute\AttributeGroupNode Attribute\AttributeNode Name(out) - Identifier(out) AST, $this->parseAndPrint('HashMap<#[name("key")] #[out] T>')); } @@ -85,20 +74,15 @@ public function testSingleAttributeOnShapeField(): void self::assertSame(<<<'AST' NamedTypeNode Name(App\Domain\User) - Identifier(App) - Identifier(Domain) - Identifier(User) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) Identifier(userName) NamedTypeNode Name(non-empty-string) - Identifier(non-empty-string) Attribute\AttributeGroupListNode Attribute\AttributeGroupNode Attribute\AttributeNode Name(name) - Identifier(name) AST, $this->parseAndPrint('App\\Domain\\User{#[name("user_name")] userName: non-empty-string}')); } @@ -107,24 +91,18 @@ public function testMultipleAttributeGroupsOnShapeField(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(optional) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=true) Identifier(test) NamedTypeNode Name(App\Domain\User) - Identifier(App) - Identifier(Domain) - Identifier(User) Attribute\AttributeGroupListNode Attribute\AttributeGroupNode Attribute\AttributeNode Name(serialize) - Identifier(serialize) Attribute\AttributeGroupNode Attribute\AttributeNode Name(deserialize) - Identifier(deserialize) AST, $this->parseAndPrint('array{#[serialize("onSerialize")] #[deserialize("onDeserialize")] test?: App\\Domain\\User}')); } diff --git a/libs/parser/tests/Syntax/CallableTest.php b/libs/parser/tests/Syntax/CallableTest.php index decb6d9..a101ebc 100644 --- a/libs/parser/tests/Syntax/CallableTest.php +++ b/libs/parser/tests/Syntax/CallableTest.php @@ -17,7 +17,6 @@ public function testCallableWithoutParametersAndReturnType(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode AST, $this->parseAndPrint('foo()')); } @@ -27,15 +26,12 @@ public function testCallableWithParameterAndReturnType(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) NamedTypeNode Name(void) - Identifier(void) AST, $this->parseAndPrint('foo(T): void')); } @@ -44,35 +40,28 @@ public function testComplexNestedCallable(): void self::assertSame(<<<'AST' CallableTypeNode Name(a) - Identifier(a) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(int) - Identifier(int) Template\TemplateArgumentListNode Template\TemplateArgumentNode Literal\IntLiteralNode(0) Template\TemplateArgumentNode NamedTypeNode Name(max) - Identifier(max) - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) CallableTypeNode Name(c) - Identifier(c) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NullableTypeNode NamedTypeNode Name(C) - Identifier(C) NamedTypeNode Name(mixed) - Identifier(mixed) NamedTypeNode Name(void) - Identifier(void) AST, $this->parseAndPrint('a(int<0, max>, c(?C): mixed): void')); } @@ -81,12 +70,10 @@ public function testNamedParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) Literal\VariableLiteralNode($name) AST, $this->parseAndPrint('foo(T $name)')); } @@ -96,21 +83,17 @@ public function testMixedNamedAndAnonymousParameters(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(A) - Identifier(A) Literal\VariableLiteralNode($a) - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(B) - Identifier(B) - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('foo(A $a, B, C)')); } @@ -119,12 +102,10 @@ public function testOutputParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(output) + Callable\CallableParameterNode(isOutput=true, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('foo(T&)')); } @@ -133,12 +114,10 @@ public function testOutputNamedParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(output) + Callable\CallableParameterNode(isOutput=true, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) Literal\VariableLiteralNode($name) AST, $this->parseAndPrint('foo(T &$name)')); } @@ -148,12 +127,10 @@ public function testOptionalParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(optional) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=true) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('foo(T=)')); } @@ -162,12 +139,10 @@ public function testVariadicParameterPrefixSyntax(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(variadic) + Callable\CallableParameterNode(isOutput=false, isVariadic=true, isOptional=false) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('foo(...T)')); } @@ -176,12 +151,10 @@ public function testVariadicParameterPostfixSyntax(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(variadic) + Callable\CallableParameterNode(isOutput=false, isVariadic=true, isOptional=false) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('foo(T...)')); } @@ -190,12 +163,10 @@ public function testVariadicNamedOutputParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(output, variadic) + Callable\CallableParameterNode(isOutput=true, isVariadic=true, isOptional=false) NamedTypeNode Name(T) - Identifier(T) Literal\VariableLiteralNode($name) AST, $this->parseAndPrint('foo(...T &$name)')); } diff --git a/libs/parser/tests/Syntax/ConditionalTest.php b/libs/parser/tests/Syntax/ConditionalTest.php index aacaccf..f426967 100644 --- a/libs/parser/tests/Syntax/ConditionalTest.php +++ b/libs/parser/tests/Syntax/ConditionalTest.php @@ -34,16 +34,12 @@ public function testConditionalOperators(string $type, string $condition): void Condition\\{$condition} NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) NamedTypeNode Name(D) - Identifier(D) AST, $this->parseAndPrint($type)); } @@ -55,13 +51,10 @@ public function testConditionalWithVariableSubject(): void Literal\VariableLiteralNode($T) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) NamedTypeNode Name(D) - Identifier(D) AST, $this->parseAndPrint('$T is B ? C : D')); } diff --git a/libs/parser/tests/Syntax/ConstantTest.php b/libs/parser/tests/Syntax/ConstantTest.php index a4bf1e0..0d83420 100644 --- a/libs/parser/tests/Syntax/ConstantTest.php +++ b/libs/parser/tests/Syntax/ConstantTest.php @@ -17,7 +17,6 @@ public function testGlobalConstantIsInterpretedAsNamedType(): void self::assertSame(<<<'AST' NamedTypeNode Name(JSON_THROW_ON_ERROR) - Identifier(JSON_THROW_ON_ERROR) AST, $this->parseAndPrint('JSON_THROW_ON_ERROR')); } @@ -26,8 +25,6 @@ public function testNamespacedConstantIsInterpretedAsNamedType(): void self::assertSame(<<<'AST' NamedTypeNode Name(pcov\version) - Identifier(pcov) - Identifier(version) AST, $this->parseAndPrint('pcov\\version')); } @@ -36,7 +33,6 @@ public function testClassConstant(): void self::assertSame(<<<'AST' ClassConstNode Name(ClassName) - Identifier(ClassName) Identifier(CONSTANT_NAME) AST, $this->parseAndPrint('ClassName::CONSTANT_NAME')); } @@ -46,9 +42,6 @@ public function testNamespacedClassConstant(): void self::assertSame(<<<'AST' ClassConstNode Name(Path\To\ClassName) - Identifier(Path) - Identifier(To) - Identifier(ClassName) Identifier(ANOTHER_CONSTANT_NAME) AST, $this->parseAndPrint('Path\\To\\ClassName::ANOTHER_CONSTANT_NAME')); } @@ -56,9 +49,8 @@ public function testNamespacedClassConstant(): void public function testGlobalConstantMask(): void { self::assertSame(<<<'AST' - ConstMaskNode(JSON_*) + ConstMaskNode Name(JSON_) - Identifier(JSON_) AST, $this->parseAndPrint('JSON_*')); } @@ -67,9 +59,6 @@ public function testClassConstantMask(): void self::assertSame(<<<'AST' ClassConstMaskNode Name(Path\To\ClassName) - Identifier(Path) - Identifier(To) - Identifier(ClassName) Identifier(PREFIX_) AST, $this->parseAndPrint('Path\\To\\ClassName::PREFIX_*')); } @@ -79,9 +68,6 @@ public function testClassConstantMaskWithoutPrefix(): void self::assertSame(<<<'AST' ClassConstMaskNode Name(Path\To\ClassName) - Identifier(Path) - Identifier(To) - Identifier(ClassName) AST, $this->parseAndPrint('Path\\To\\ClassName::*')); } diff --git a/libs/parser/tests/Syntax/GenericTest.php b/libs/parser/tests/Syntax/GenericTest.php index caf897f..c6e4efd 100644 --- a/libs/parser/tests/Syntax/GenericTest.php +++ b/libs/parser/tests/Syntax/GenericTest.php @@ -17,18 +17,13 @@ public function testTemplateArguments(): void self::assertSame(<<<'AST' NamedTypeNode Name(Path\To\ExampleClass) - Identifier(Path) - Identifier(To) - Identifier(ExampleClass) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(T) - Identifier(T) Template\TemplateArgumentNode NamedTypeNode Name(U) - Identifier(U) AST, $this->parseAndPrint('Path\\To\\ExampleClass')); } @@ -37,12 +32,10 @@ public function testSingleTemplateArgument(): void self::assertSame(<<<'AST' NamedTypeNode Name(Collection) - Identifier(Collection) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('Collection')); } @@ -51,28 +44,23 @@ public function testNestedTemplateArguments(): void self::assertSame(<<<'AST' NamedTypeNode Name(iterable) - Identifier(iterable) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(int) - Identifier(int) Template\TemplateArgumentListNode Template\TemplateArgumentNode Literal\IntLiteralNode(0) Template\TemplateArgumentNode NamedTypeNode Name(max) - Identifier(max) Template\TemplateArgumentNode NamedTypeNode Name(Collection) - Identifier(Collection) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('iterable, Collection>')); } @@ -81,16 +69,13 @@ public function testTrailingCommaIsAllowed(): void self::assertSame(<<<'AST' NamedTypeNode Name(HashMap) - Identifier(HashMap) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(Request) - Identifier(Request) Template\TemplateArgumentNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('HashMap')); } @@ -99,17 +84,14 @@ public function testCallSiteHint(): void self::assertSame(<<<'AST' NamedTypeNode Name(HashMap) - Identifier(HashMap) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(array-key) - Identifier(array-key) Template\TemplateArgumentNode Identifier(covariant) NamedTypeNode Name(Request) - Identifier(Request) AST, $this->parseAndPrint('HashMap')); } @@ -123,13 +105,10 @@ public function testHintLikeIdentifierWithoutSpaceIsRelativeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(out\Some) - Identifier(out) - Identifier(Some) AST, $this->parseAndPrint('Type')); } @@ -143,13 +122,11 @@ public function testHintFollowedBySpaceAndFullyQualifiedName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode Identifier(out) NamedTypeNode Name(\Some) - Identifier(Some) AST, $this->parseAndPrint('Type')); } @@ -162,14 +139,10 @@ public function testHintLikeIdentifierWithoutSpaceIsNestedRelativeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(out\Some\Deep) - Identifier(out) - Identifier(Some) - Identifier(Deep) AST, $this->parseAndPrint('Type')); } @@ -182,13 +155,11 @@ public function testHintFollowedBySpaceAndRelativeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode Identifier(out) NamedTypeNode Name(Some) - Identifier(Some) AST, $this->parseAndPrint('Type')); } diff --git a/libs/parser/tests/Syntax/ListTest.php b/libs/parser/tests/Syntax/ListTest.php index 500f953..19741e9 100644 --- a/libs/parser/tests/Syntax/ListTest.php +++ b/libs/parser/tests/Syntax/ListTest.php @@ -18,7 +18,6 @@ public function testSimpleList(): void TypesListNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('User[]')); } @@ -29,7 +28,6 @@ public function testNestedList(): void TypesListNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('User[][]')); } } diff --git a/libs/parser/tests/Syntax/LogicalTest.php b/libs/parser/tests/Syntax/LogicalTest.php index bf99348..ace03b6 100644 --- a/libs/parser/tests/Syntax/LogicalTest.php +++ b/libs/parser/tests/Syntax/LogicalTest.php @@ -19,13 +19,10 @@ public function testUnionType(): void UnionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('A | B | C')); } @@ -35,13 +32,10 @@ public function testIntersectionType(): void IntersectionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('A & B & C')); } @@ -51,7 +45,6 @@ public function testNullableType(): void NullableTypeNode NamedTypeNode Name(Example) - Identifier(Example) AST, $this->parseAndPrint('?Example')); } @@ -62,13 +55,10 @@ public function testDisjunctiveNormalForm(): void IntersectionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('(A & B) | C')); } @@ -79,13 +69,10 @@ public function testConjunctiveNormalForm(): void UnionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('(A | B) & C')); } diff --git a/libs/parser/tests/Syntax/NameTest.php b/libs/parser/tests/Syntax/NameTest.php index ccd7a7a..bd34007 100644 --- a/libs/parser/tests/Syntax/NameTest.php +++ b/libs/parser/tests/Syntax/NameTest.php @@ -47,8 +47,6 @@ public function testRelativeNamespacedName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Example\Name) - Identifier(Example) - Identifier(Name) AST, $this->parseAndPrint('Example\\Name')); } @@ -57,9 +55,6 @@ public function testAbsoluteNamespacedName(): void self::assertSame(<<<'AST' NamedTypeNode Name(\Absolute\Type\Name) - Identifier(Absolute) - Identifier(Type) - Identifier(Name) AST, $this->parseAndPrint('\\Absolute\\Type\\Name')); } @@ -82,7 +77,6 @@ public function testKeywordAsExplicitTypeReference(string $keyword): void self::assertSame(<<parseAndPrint('\\' . $keyword)); } diff --git a/libs/parser/tests/Syntax/OffsetAccessTest.php b/libs/parser/tests/Syntax/OffsetAccessTest.php index b642321..3f653d0 100644 --- a/libs/parser/tests/Syntax/OffsetAccessTest.php +++ b/libs/parser/tests/Syntax/OffsetAccessTest.php @@ -16,10 +16,8 @@ public function testStringOffset(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode - Literal\StringLiteralNode('offset') NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint("T['offset']")); } @@ -27,12 +25,8 @@ public function testDependentKeyOffset(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode - NamedTypeNode - Name(U) - Identifier(U) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('T[U]')); } @@ -40,19 +34,15 @@ public function testShapeWithNumericOffset(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode - Literal\IntLiteralNode(0) NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\ImplicitFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(int) - Identifier(int) - Shape\ImplicitFieldNode(required) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(string) - Identifier(string) AST, $this->parseAndPrint('array{int, string}[0]')); } @@ -60,23 +50,12 @@ public function testComplexOffsetWithGenericsAndShapes(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode - NamedTypeNode - Name(object) - Identifier(object) - Shape\FieldsListNode(unsealed) - Shape\NamedFieldNode(required) - Identifier(key) - NamedTypeNode - Name(int) - Identifier(int) NamedTypeNode Name(T) - Identifier(T) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(U) - Identifier(U) AST, $this->parseAndPrint('T[object{key: int, ...}]')); } diff --git a/libs/parser/tests/Syntax/ShapeTest.php b/libs/parser/tests/Syntax/ShapeTest.php index 05d4169..b40e8a8 100644 --- a/libs/parser/tests/Syntax/ShapeTest.php +++ b/libs/parser/tests/Syntax/ShapeTest.php @@ -17,18 +17,15 @@ public function testNamedExplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) Identifier(a) NamedTypeNode Name(first) - Identifier(first) - Shape\NamedFieldNode(required) + Shape\NamedFieldNode(isOptional=false) Identifier(b) NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{a: first, b: second}')); } @@ -37,18 +34,15 @@ public function testNumericExplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NumericFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NumericFieldNode(isOptional=false) Literal\IntLiteralNode(1) NamedTypeNode Name(first) - Identifier(first) - Shape\NumericFieldNode(required) + Shape\NumericFieldNode(isOptional=false) Literal\IntLiteralNode(42) NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{1: first, 42: second}')); } @@ -57,18 +51,15 @@ public function testStringExplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\StringNamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\StringNamedFieldNode(isOptional=false) Literal\StringLiteralNode("name-some") NamedTypeNode Name(first) - Identifier(first) - Shape\StringNamedFieldNode(required) + Shape\StringNamedFieldNode(isOptional=false) Literal\StringLiteralNode("escape\nchars") NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{"name-some": first, "escape\\nchars": second}')); } @@ -77,16 +68,13 @@ public function testImplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\ImplicitFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(first) - Identifier(first) - Shape\ImplicitFieldNode(required) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{first, second}')); } @@ -95,8 +83,7 @@ public function testEmptyShape(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) + Shape\FieldsListNode(isSealed=true) AST, $this->parseAndPrint('array{}')); } @@ -105,13 +92,11 @@ public function testTrailingCommaIsAllowed(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) Identifier(a) NamedTypeNode Name(int) - Identifier(int) AST, $this->parseAndPrint('array{a: int,}')); } @@ -120,13 +105,11 @@ public function testOptionalKey(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(optional) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=true) Identifier(key) NamedTypeNode Name(Type) - Identifier(Type) AST, $this->parseAndPrint('array{key?: Type}')); } @@ -135,13 +118,11 @@ public function testUnsealedShape(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(unsealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=false) + Shape\NamedFieldNode(isOptional=false) Identifier(key) NamedTypeNode Name(type) - Identifier(type) AST, $this->parseAndPrint('array{key: type, ...}')); } @@ -150,22 +131,18 @@ public function testTypedUnsealedShape(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(string) - Identifier(string) Template\TemplateArgumentNode NamedTypeNode Name(object) - Identifier(object) - Shape\FieldsListNode(unsealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=false) + Shape\NamedFieldNode(isOptional=false) Identifier(user) NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('array{user: User, ...}')); } @@ -174,15 +151,11 @@ public function testShapeOnArbitraryTypeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(App\Domain\User) - Identifier(App) - Identifier(Domain) - Identifier(User) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) Identifier(userName) NamedTypeNode Name(non-empty-string) - Identifier(non-empty-string) AST, $this->parseAndPrint('App\\Domain\\User{userName: non-empty-string}')); } diff --git a/libs/parser/tests/Traverser/MatcherVisitorTest.php b/libs/parser/tests/Traverser/MatcherVisitorTest.php new file mode 100644 index 0000000..672425c --- /dev/null +++ b/libs/parser/tests/Traverser/MatcherVisitorTest.php @@ -0,0 +1,195 @@ + true); + + self::assertFalse($visitor->hasMatches()); + self::assertNull($visitor->node); + } + + #[Test] + public function theMatchedNodeIsAvailableAfterTheTraversal(): void + { + $expected = $this->type(); + + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof NamedTypeNode), + [$expected], + ); + + self::assertTrue($visitor->hasMatches()); + self::assertSame($expected, $visitor->node); + } + + #[Test] + public function nothingIsFoundInCaseOfNoNodeMatches(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof UnionTypeNode), + [$this->type()], + ); + + self::assertFalse($visitor->hasMatches()); + self::assertNull($visitor->node); + } + + #[Test] + public function theFirstMatchingNodeIsReturned(): void + { + $first = $this->type('First'); + $second = $this->type('Second'); + + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof NamedTypeNode), + [$first, $second], + ); + + self::assertSame($first, $visitor->node); + } + + #[Test] + public function nestedNodesAreMatched(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof Name), + [$this->type('Foo\\Bar')], + ); + + self::assertInstanceOf(Name::class, $visitor->node); + self::assertSame('Foo\\Bar', $visitor->node->toString()); + } + + #[Test] + public function theBreakConditionStopsTheSearch(): void + { + $visitor = Traverser::through( + new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof Name, + break: static fn(Node $node): bool => $node instanceof NamedTypeNode, + ), + [$this->type('Foo\\Bar')], + ); + + self::assertFalse($visitor->hasMatches()); + } + + #[Test] + public function theMatcherHasAPriorityOverTheBreakCondition(): void + { + $expected = $this->type(); + + $visitor = Traverser::through( + new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof NamedTypeNode, + break: static fn(Node $node): bool => $node instanceof NamedTypeNode, + ), + [$expected], + ); + + self::assertSame($expected, $visitor->node); + } + + #[Test] + public function theBreakConditionStopsTheWholeTraversal(): void + { + $visitor = Traverser::through( + new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof Identifier, + break: static fn(Node $node): bool => $node instanceof UnionTypeNode, + ), + [ + new UnionTypeNode($this->type('A'), $this->type('B')), + new Identifier('Example'), + ], + ); + + self::assertFalse($visitor->hasMatches()); + } + + #[Test] + public function enterSkipsTheChildrenOfTheMatchedNode(): void + { + $visitor = new MatcherVisitor(static fn(Node $node): bool => true); + + self::assertSame(Command::SkipChildren, $visitor->enter($this->type())); + } + + #[Test] + public function enterDescendsIntoTheNonMatchingNode(): void + { + $visitor = new MatcherVisitor(static fn(Node $node): bool => false); + + self::assertNull($visitor->enter($this->type())); + } + + #[Test] + public function beforeResetsThePreviouslyMatchedNode(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => true), + [$this->type()], + ); + + $visitor->before(); + + self::assertFalse($visitor->hasMatches()); + self::assertNull($visitor->node); + } + + #[Test] + public function theMatcherReceivesEveryVisitedNode(): void + { + $visited = []; + + Traverser::through( + new MatcherVisitor(static function (Node $node) use (&$visited): bool { + $visited[] = $node::class; + + return false; + }), + [$this->type('Foo\\Bar')], + ); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visited); + } + + #[Test] + public function theMatcherCanBeAnArbitraryCondition(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool + => $node instanceof NamedTypeNode && $node->name->toString() === 'Second'), + [$this->type('First'), $this->type('Second')], + ); + + self::assertInstanceOf(NamedTypeNode::class, $visitor->node); + self::assertSame('Second', $visitor->node->name->toString()); + } +} diff --git a/libs/parser/tests/Traverser/StringDumperVisitorTest.php b/libs/parser/tests/Traverser/StringDumperVisitorTest.php new file mode 100644 index 0000000..2802302 --- /dev/null +++ b/libs/parser/tests/Traverser/StringDumperVisitorTest.php @@ -0,0 +1,236 @@ + $nodes + */ + private function dump(iterable $nodes, ?string $namespace = null): string + { + $visitor = $namespace === null + ? new StringDumperVisitor() + : new StringDumperVisitor($namespace); + + Traverser::new([$visitor])->traverse($nodes); + + return $visitor->output; + } + + #[Test] + public function theOutputIsEmptyBeforeTheTraversal(): void + { + self::assertSame('', (new StringDumperVisitor())->output); + } + + #[Test] + public function theOutputIsEmptyInCaseOfNoNodes(): void + { + self::assertSame('', $this->dump([])); + } + + #[Test] + public function theNodeNameIsPrintedWithoutTheSimplifiedNamespace(): void + { + self::assertSame( + "NamedTypeNode\n Name(Foo\\Bar)\n", + $this->dump([new NamedTypeNode(Name::createFromString('Foo\\Bar'))]), + ); + } + + #[Test] + public function theSimplifiedNamespaceCanBeChanged(): void + { + self::assertSame( + "Type\\Identifier(Example)\n", + $this->dump([new Identifier('Example')], 'TypeLang\\'), + ); + } + + #[Test] + public function theNodeNameIsPrintedAsIsInCaseOfANonMatchingNamespace(): void + { + self::assertSame( + "TypeLang\\Type\\Identifier(Example)\n", + $this->dump([new Identifier('Example')], 'Example\\'), + ); + } + + #[Test] + public function theDefaultSimplifiedNamespaceIsUsed(): void + { + self::assertSame( + $this->dump([new Identifier('Example')]), + $this->dump([new Identifier('Example')], DumperVisitor::DEFAULT_SIMPLIFIED_NODE_NAMESPACE), + ); + } + + #[Test] + public function everyNestingLevelIsIndentedByTwoSpaces(): void + { + self::assertSame(<<<'OUTPUT' + UnionTypeNode + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + + OUTPUT, $this->dump([ + new UnionTypeNode( + new NamedTypeNode(Name::createFromString('int')), + new NamedTypeNode(Name::createFromString('string')), + ), + ])); + } + + #[Test] + public function theStringableNodeIsPrintedUsingItsStringValue(): void + { + self::assertSame( + "Name(\\Foo\\Bar)\n", + $this->dump([Name::createFromString('\\Foo\\Bar')]), + ); + } + + #[Test] + public function theNodeWithoutScalarPropertiesHasNoSuffix(): void + { + self::assertStringStartsWith( + "NamedTypeNode\n", + $this->dump([new NamedTypeNode(Name::createFromString('Example'))]), + ); + } + + #[Test] + public function theOffsetPropertyIsNotPrinted(): void + { + $node = new NamedTypeNode(Name::createFromString('Example')); + $node->offset = 42; + + self::assertStringStartsWith("NamedTypeNode\n", $this->dump([$node])); + } + + #[Test] + public function theWritableScalarPropertiesArePrinted(): void + { + $node = new class extends Node { + public bool $enabled = true; + + public int $count = 42; + + public string $title = 'example'; + }; + + self::assertStringEndsWith("(enabled=true, count=42, title='example')\n", $this->dump([$node])); + } + + #[Test] + public function theReadonlyPropertiesAreNotPrinted(): void + { + $node = new class extends Node { + public readonly bool $hidden; + + public bool $visible = true; + + public function __construct() + { + $this->hidden = true; + } + }; + + self::assertStringEndsWith("(visible=true)\n", $this->dump([$node])); + } + + #[Test] + public function theStaticPropertiesAreNotPrinted(): void + { + $node = new class extends Node { + public static bool $hidden = true; + + public bool $visible = true; + }; + + self::assertStringEndsWith("(visible=true)\n", $this->dump([$node])); + } + + #[Test] + public function theNonScalarPropertiesAreNotPrinted(): void + { + $node = new class extends Node { + /** + * @var list + */ + public array $items = []; + + public ?string $nothing = null; + + public bool $visible = true; + }; + + self::assertStringEndsWith("(visible=true)\n", $this->dump([$node])); + } + + #[Test] + public function theShapeFieldsArePrintedUsingTheirScalarProperties(): void + { + self::assertSame(<<<'OUTPUT' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=true) + Identifier(foo) + NamedTypeNode + Name(int) + + OUTPUT, $this->dump([$this->parse('array{foo?: int}')])); + } + + #[Test] + public function resetClearsTheOutput(): void + { + $visitor = new StringDumperVisitor(); + Traverser::new([$visitor])->traverse([new Identifier('Example')]); + + $visitor->reset(); + + self::assertSame('', $visitor->output); + } + + #[Test] + public function theOutputIsClearedOnEachTraversal(): void + { + $visitor = new StringDumperVisitor(); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([new Identifier('First')]); + $traverser->traverse([new Identifier('Second')]); + + self::assertSame("Identifier(Second)\n", $visitor->output); + } + + #[Test] + public function theIndentationIsClearedOnEachTraversal(): void + { + $visitor = new StringDumperVisitor(); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([new NamedTypeNode(Name::createFromString('First'))]); + $traverser->traverse([new Identifier('Second')]); + + self::assertSame("Identifier(Second)\n", $visitor->output); + } +} diff --git a/libs/parser/tests/Traverser/TraverserTest.php b/libs/parser/tests/Traverser/TraverserTest.php new file mode 100644 index 0000000..7b6a181 --- /dev/null +++ b/libs/parser/tests/Traverser/TraverserTest.php @@ -0,0 +1,401 @@ +, non-empty-string> + */ + private \ArrayObject $log; + + protected function setUp(): void + { + parent::setUp(); + + $this->log = new \ArrayObject(); + } + + private function type(string $name = 'Example'): NamedTypeNode + { + return new NamedTypeNode(Name::createFromString($name)); + } + + /** + * @param non-empty-string $label + * @return VisitorInterface&object{ + * entered: list>, + * left: list>, + * before: int<0, max>, + * after: int<0, max> + * } + */ + private function recorder(string $label = 'a', ?Command $command = null): VisitorInterface + { + return new class ($this->log, $label, $command) extends Visitor { + /** + * @var list> + */ + public array $entered = []; + + /** + * @var list> + */ + public array $left = []; + + /** + * @var int<0, max> + */ + public int $before = 0; + + /** + * @var int<0, max> + */ + public int $after = 0; + + /** + * @param \ArrayObject, non-empty-string> $log + * @param non-empty-string $label + */ + public function __construct( + private readonly \ArrayObject $log, + private readonly string $label, + private readonly ?Command $command = null, + ) {} + + public function before(): void + { + ++$this->before; + $this->log[] = $this->label . ':before'; + } + + public function enter(Node $node): ?Command + { + $this->entered[] = $node::class; + $this->log[] = $this->label . ':enter'; + + return $this->command; + } + + public function leave(Node $node): void + { + $this->left[] = $node::class; + $this->log[] = $this->label . ':leave'; + } + + public function after(): void + { + ++$this->after; + $this->log[] = $this->label . ':after'; + } + }; + } + + #[Test] + public function traverseVisitsEveryNodeInDepth(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseLeavesEveryEnteredNode(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([ + Name::class, + NamedTypeNode::class, + ], $visitor->left); + } + + #[Test] + public function traverseCallsTheLifecycleHooksOnce(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type()]); + + self::assertSame(1, $visitor->before); + self::assertSame(1, $visitor->after); + } + + #[Test] + public function beforeIsCalledPriorToAnyNodeAndAfterIsCalledLast(): void + { + Traverser::new([$this->recorder()])->traverse([$this->type()]); + + self::assertSame('a:before', $this->log[0]); + self::assertSame('a:after', $this->log[\count($this->log) - 1]); + } + + #[Test] + public function skipChildrenCommandStopsTheDescent(): void + { + $visitor = $this->recorder(command: Command::SkipChildren); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([NamedTypeNode::class], $visitor->entered); + } + + #[Test] + public function skipChildrenCommandDoesNotSkipTheLeaveCall(): void + { + $visitor = $this->recorder(command: Command::SkipChildren); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([NamedTypeNode::class], $visitor->left); + } + + #[Test] + public function commandContainsTheSkipChildrenCase(): void + { + self::assertSame([Command::SkipChildren], Command::cases()); + } + + #[Test] + public function throughReturnsThePassedVisitor(): void + { + $visitor = $this->recorder(); + + $result = Traverser::through($visitor, [$this->type()]); + + self::assertSame($visitor, $result); + } + + #[Test] + public function throughTraversesTheNodes(): void + { + $visitor = Traverser::through($this->recorder(), [$this->type('Foo\\Bar')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function newCreatesAnEmptyTraverser(): void + { + Traverser::new()->traverse([$this->type()]); + + self::assertSame([], $this->log->getArrayCopy()); + } + + #[Test] + public function withReturnsANewInstance(): void + { + $traverser = Traverser::new(); + + self::assertNotSame($traverser, $traverser->with($this->recorder())); + } + + #[Test] + public function withDoesNotModifyTheOriginalTraverser(): void + { + $traverser = Traverser::new(); + $traverser->with($this->recorder()); + + $traverser->traverse([$this->type()]); + + self::assertSame([], $this->log->getArrayCopy()); + } + + #[Test] + public function withAppendsTheVisitorToTheEnd(): void + { + Traverser::new([$this->recorder('a')]) + ->with($this->recorder('b')) + ->traverse([]); + + self::assertSame([ + 'a:before', + 'b:before', + 'a:after', + 'b:after', + ], $this->log->getArrayCopy()); + } + + #[Test] + public function withPrependsTheVisitorToTheBeginning(): void + { + Traverser::new([$this->recorder('a')]) + ->with($this->recorder('b'), true) + ->traverse([]); + + self::assertSame([ + 'b:before', + 'a:before', + 'b:after', + 'a:after', + ], $this->log->getArrayCopy()); + } + + #[Test] + public function withPropertyAccessorReturnsANewInstance(): void + { + $traverser = Traverser::new(); + + self::assertNotSame( + $traverser, + $traverser->withPropertyAccessor(new SimplePropertyAccessor()), + ); + } + + #[Test] + public function withPropertyAccessorChangesTheTraversedChildren(): void + { + $accessor = new class implements PropertyAccessorInterface { + public function unwrap(object $object): iterable + { + return []; + } + }; + + $visitor = $this->recorder(); + + Traverser::new([$visitor]) + ->withPropertyAccessor($accessor) + ->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([NamedTypeNode::class], $visitor->entered); + } + + #[Test] + public function withPropertyAccessorDoesNotModifyTheOriginalTraverser(): void + { + $accessor = new class implements PropertyAccessorInterface { + public function unwrap(object $object): iterable + { + return []; + } + }; + + $visitor = $this->recorder(); + $traverser = Traverser::new([$visitor]); + $traverser->withPropertyAccessor($accessor); + + $traverser->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseVisitsEveryNodeOfTheList(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type('A'), $this->type('B')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseCanBeCalledSeveralTimes(): void + { + $visitor = $this->recorder(); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([$this->type()]); + $traverser->traverse([$this->type()]); + + self::assertSame(2, $visitor->before); + self::assertSame(2, $visitor->after); + } + + #[Test] + public function traverseVisitsTheNestedLogicalStatements(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([ + new UnionTypeNode($this->type('A'), $this->type('B')), + ]); + + self::assertSame([ + UnionTypeNode::class, + NamedTypeNode::class, + Name::class, + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseOfAnEmptyListCallsOnlyTheLifecycleHooks(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([]); + + self::assertSame([], $visitor->entered); + self::assertSame(1, $visitor->before); + self::assertSame(1, $visitor->after); + } + + #[Test] + public function traverseAcceptsAnyTraversableSetOfNodes(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse(new \ArrayIterator([ + new Identifier('Example'), + ])); + + self::assertSame([Identifier::class], $visitor->entered); + } + + #[Test] + public function traverserCanBeCreatedUsingTheConstructor(): void + { + $visitor = $this->recorder(); + + (new Traverser([$visitor]))->traverse([$this->type()]); + + self::assertSame(1, $visitor->before); + } + + #[Test] + public function traverserConstructorAcceptsATraversableSetOfVisitors(): void + { + $visitor = $this->recorder(); + + (new Traverser(new \ArrayIterator([$visitor])))->traverse([$this->type()]); + + self::assertSame(1, $visitor->before); + } +} diff --git a/libs/parser/tests/Traverser/TypeMapVisitorTest.php b/libs/parser/tests/Traverser/TypeMapVisitorTest.php new file mode 100644 index 0000000..2b0ba7d --- /dev/null +++ b/libs/parser/tests/Traverser/TypeMapVisitorTest.php @@ -0,0 +1,169 @@ + Name::createFromString($to)); + } + + #[Test] + public function theNameOfANamedTypeIsTransformed(): void + { + $node = new NamedTypeNode(Name::createFromString('Example')); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->name->toString()); + } + + #[Test] + public function theNameOfACallableTypeIsTransformed(): void + { + $node = new CallableTypeNode(Name::createFromString('callable')); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->name->toString()); + } + + #[Test] + public function theNameOfAConstMaskIsTransformed(): void + { + $node = new ConstMaskNode(Name::createFromString('SOME_*')); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->name->toString()); + } + + #[Test] + public function theClassOfAClassConstIsTransformed(): void + { + $node = new ClassConstNode( + class: Name::createFromString('Example'), + constant: new Identifier('CONSTANT'), + ); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->class->toString()); + } + + #[Test] + public function theConstantOfAClassConstIsNotTransformed(): void + { + $node = new ClassConstNode( + class: Name::createFromString('Example'), + constant: new Identifier('CONSTANT'), + ); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('CONSTANT', $node->constant->toString()); + } + + #[Test] + public function theClassOfAClassConstMaskIsTransformed(): void + { + $node = new ClassConstMaskNode(Name::createFromString('Example')); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->class->toString()); + } + + #[Test] + public function theNullResultKeepsTheOriginalName(): void + { + $expected = Name::createFromString('Example'); + $node = new NamedTypeNode($expected); + + Traverser::new([new TypeMapVisitor(static fn(Name $name): ?Name => null)]) + ->traverse([$node]); + + self::assertSame($expected, $node->name); + } + + #[Test] + public function theTransformationIsAppliedToEveryNestedType(): void + { + $node = new UnionTypeNode( + new NamedTypeNode(Name::createFromString('A')), + new NamedTypeNode(Name::createFromString('B')), + ); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame( + ['Aliased', 'Aliased'], + \array_map( + static fn(NamedTypeNode $type): string => $type->name->toString(), + $node->statements, + ), + ); + } + + #[Test] + public function theTransformationReceivesTheOriginalName(): void + { + $received = []; + + $visitor = new TypeMapVisitor(static function (Name $name) use (&$received): Name { + $received[] = $name->toString(); + + return $name; + }); + + Traverser::new([$visitor])->traverse([ + new NamedTypeNode(Name::createFromString('Foo\\Bar')), + ]); + + self::assertSame(['Foo\\Bar'], $received); + } + + #[Test] + public function theTransformationIsNotAppliedToAnArbitraryName(): void + { + $received = []; + + $visitor = new TypeMapVisitor(static function (Name $name) use (&$received): Name { + $received[] = $name->toString(); + + return $name; + }); + + Traverser::new([$visitor])->traverse([Name::createFromString('Example')]); + + self::assertSame([], $received); + } + + #[Test] + public function theTransformedNameIsVisitedInsteadOfTheOriginalOne(): void + { + $node = new NamedTypeNode(Name::createFromString('Example')); + $expected = Name::createFromString('Aliased'); + + Traverser::new([new TypeMapVisitor(static fn(Name $name): Name => $expected)]) + ->traverse([$node]); + + self::assertSame($expected, $node->name); + } +} diff --git a/libs/parser/tests/TypeParserFeaturesTest.php b/libs/parser/tests/TypeParserFeaturesTest.php new file mode 100644 index 0000000..71dbeb7 --- /dev/null +++ b/libs/parser/tests/TypeParserFeaturesTest.php @@ -0,0 +1,192 @@ + + */ + public static function featureDataProvider(): iterable + { + yield 'conditions' => [ + 'conditions', + static fn(TypeParserFeatures $features): bool => $features->conditions, + TypeParserFeatures::CONDITIONAL_FEATURES_DEFAULT_VALUE, + ]; + + yield 'shapes' => [ + 'shapes', + static fn(TypeParserFeatures $features): bool => $features->shapes, + TypeParserFeatures::SHAPES_FEATURES_DEFAULT_VALUE, + ]; + + yield 'callables' => [ + 'callables', + static fn(TypeParserFeatures $features): bool => $features->callables, + TypeParserFeatures::CALLABLES_FEATURES_DEFAULT_VALUE, + ]; + + yield 'literals' => [ + 'literals', + static fn(TypeParserFeatures $features): bool => $features->literals, + TypeParserFeatures::LITERALS_FEATURES_DEFAULT_VALUE, + ]; + + yield 'generics' => [ + 'generics', + static fn(TypeParserFeatures $features): bool => $features->generics, + TypeParserFeatures::GENERICS_FEATURES_DEFAULT_VALUE, + ]; + + yield 'unions' => [ + 'unions', + static fn(TypeParserFeatures $features): bool => $features->unions, + TypeParserFeatures::UNION_FEATURES_DEFAULT_VALUE, + ]; + + yield 'intersections' => [ + 'intersections', + static fn(TypeParserFeatures $features): bool => $features->intersections, + TypeParserFeatures::INTERSECTION_FEATURES_DEFAULT_VALUE, + ]; + + yield 'lists' => [ + 'lists', + static fn(TypeParserFeatures $features): bool => $features->lists, + TypeParserFeatures::LIST_FEATURES_DEFAULT_VALUE, + ]; + + yield 'offsets' => [ + 'offsets', + static fn(TypeParserFeatures $features): bool => $features->offsets, + TypeParserFeatures::OFFSETS_FEATURES_DEFAULT_VALUE, + ]; + + yield 'hints' => [ + 'hints', + static fn(TypeParserFeatures $features): bool => $features->hints, + TypeParserFeatures::HINTS_FEATURES_DEFAULT_VALUE, + ]; + + yield 'attributes' => [ + 'attributes', + static fn(TypeParserFeatures $features): bool => $features->attributes, + TypeParserFeatures::ATTRIBUTES_FEATURES_DEFAULT_VALUE, + ]; + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureIsEnabledByDefault(string $feature, \Closure $read, bool $default): void + { + self::assertTrue($default); + self::assertSame($default, $read(new TypeParserFeatures())); + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureCanBeDisabledUsingTheConstructor(string $feature, \Closure $read, bool $default): void + { + self::assertFalse($read(new TypeParserFeatures(...[$feature => false]))); + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureCanBeDisabledUsingTheWithMethod(string $feature, \Closure $read, bool $default): void + { + self::assertFalse($read((new TypeParserFeatures())->with(...[$feature => false]))); + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureCanBeEnabledUsingTheWithMethod(string $feature, \Closure $read, bool $default): void + { + $features = new TypeParserFeatures(...[$feature => false]); + + self::assertTrue($read($features->with(...[$feature => true]))); + } + + #[Test] + public function withReturnsANewInstance(): void + { + $features = new TypeParserFeatures(); + + self::assertNotSame($features, $features->with(shapes: false)); + } + + #[Test] + public function withDoesNotModifyTheOriginalInstance(): void + { + $features = new TypeParserFeatures(); + $features->with(shapes: false); + + self::assertTrue($features->shapes); + } + + #[Test] + public function withKeepsTheNonSpecifiedFeatures(): void + { + $features = (new TypeParserFeatures(literals: false)) + ->with(shapes: false); + + self::assertFalse($features->literals); + self::assertFalse($features->shapes); + self::assertTrue($features->generics); + } + + #[Test] + public function withCanOverrideSeveralFeaturesAtOnce(): void + { + $features = (new TypeParserFeatures())->with( + unions: false, + intersections: false, + ); + + self::assertFalse($features->unions); + self::assertFalse($features->intersections); + } + + #[Test] + public function withoutArgumentsKeepsEveryFeature(): void + { + $features = new TypeParserFeatures(callables: false, offsets: false); + + self::assertEquals($features, $features->with()); + } + + #[Test] + public function withRejectsAnUnknownFeature(): void + { + $this->expectException(\Error::class); + + /** @phpstan-ignore-next-line : An unknown feature name is expected here */ + (new TypeParserFeatures())->with(unknownFeature: false); + } +} diff --git a/libs/parser/tests/TypeParserTest.php b/libs/parser/tests/TypeParserTest.php new file mode 100644 index 0000000..fabbd25 --- /dev/null +++ b/libs/parser/tests/TypeParserTest.php @@ -0,0 +1,152 @@ +features); + } + + #[Test] + public function theFeaturesArePassedThroughTheConstructor(): void + { + $features = new TypeParserFeatures(shapes: false); + + self::assertSame($features, (new TypeParser($features))->features); + } + + #[Test] + public function withFeaturesReturnsANewInstance(): void + { + $parser = new TypeParser(); + + self::assertNotSame($parser, $parser->withFeatures(shapes: false)); + } + + #[Test] + public function withFeaturesDoesNotModifyTheOriginalParser(): void + { + $parser = new TypeParser(); + $parser->withFeatures(shapes: false); + + self::assertTrue($parser->features->shapes); + } + + #[Test] + public function withFeaturesOverridesTheGivenFeature(): void + { + $parser = (new TypeParser())->withFeatures(shapes: false); + + self::assertFalse($parser->features->shapes); + } + + #[Test] + public function withFeaturesKeepsTheNonSpecifiedFeatures(): void + { + $parser = (new TypeParser(new TypeParserFeatures(literals: false))) + ->withFeatures(shapes: false); + + self::assertFalse($parser->features->literals); + self::assertTrue($parser->features->generics); + } + + #[Test] + public function withFeaturesAffectsTheParsingBehaviour(): void + { + $parser = (new TypeParser())->withFeatures(shapes: false); + + $this->expectException(ParseException::class); + + $parser->parse('array{a: int}'); + } + + #[Test] + public function theOriginalParserKeepsItsParsingBehaviour(): void + { + $parser = new TypeParser(); + $parser->withFeatures(shapes: false); + + self::assertInstanceOf(NamedTypeNode::class, $parser->parse('array{a: int}')); + } + + #[Test] + public function theSameParserCanBeUsedSeveralTimes(): void + { + $parser = new TypeParser(); + + self::assertEquals($parser->parse('int'), $parser->parse('int')); + } + + #[Test] + public function theSourceFactoryCanBeOverridden(): void + { + $parser = new TypeParser(sources: new SourceFactory()); + + self::assertInstanceOf(NamedTypeNode::class, $parser->parse('int')); + } + + #[Test] + public function theSourceFactoryIsInheritedByTheNewInstance(): void + { + $parser = (new TypeParser(sources: new SourceFactory())) + ->withFeatures(shapes: false); + + self::assertInstanceOf(NamedTypeNode::class, $parser->parse('int')); + } + + #[Test] + public function parseTolerantReturnsAResult(): void + { + self::assertInstanceOf(ParsedResult::class, (new TypeParser())->parseTolerant('int')); + } + + #[Test] + public function theOffsetOfACompletelyParsedSourceIsItsLength(): void + { + $result = (new TypeParser())->parseTolerant('int|string'); + + self::assertSame(10, $result->offset); + } + + #[Test] + public function theOffsetPointsToTheUnparsedTail(): void + { + $source = 'array{ field: result } This is an example'; + $result = (new TypeParser())->parseTolerant($source); + + self::assertSame('This is an example', \substr($source, $result->offset)); + } + + #[Test] + public function theResultContainsTheParsedType(): void + { + $result = (new TypeParser())->parseTolerant('int'); + + self::assertInstanceOf(NamedTypeNode::class, $result->type); + self::assertSame('int', $result->type->name->toString()); + } + + #[Test] + public function parsedResultCanBeCreatedManually(): void + { + $type = new NamedTypeNode(Name::createFromString('int')); + $result = new ParsedResult($type, 42); + + self::assertSame($type, $result->type); + self::assertSame(42, $result->offset); + } +} diff --git a/libs/parser/tests/TypeResolver/NamespaceFinderTest.php b/libs/parser/tests/TypeResolver/NamespaceFinderTest.php new file mode 100644 index 0000000..202eca9 --- /dev/null +++ b/libs/parser/tests/TypeResolver/NamespaceFinderTest.php @@ -0,0 +1,154 @@ + + */ + private function rewind(string $namespace, string $source): \Iterator + { + $tokens = new \ArrayIterator(\PhpToken::tokenize("rewind($namespace, $tokens); + } + + /** + * Returns the text of the first meaningful token the stream stopped at. + */ + private function stoppedAt(string $namespace, string $source): ?string + { + $tokens = $this->rewind($namespace, $source); + + while ($tokens->valid()) { + $current = $tokens->current(); + + if ($current->id !== \T_WHITESPACE) { + return $current->text; + } + + $tokens->next(); + } + + return null; + } + + /** + * Returns every qualified name available after the stream position. + * + * @return list + */ + private function qualifiedNamesAfter(string $namespace, string $source): array + { + $tokens = $this->rewind($namespace, $source); + $result = []; + + while ($tokens->valid()) { + $current = $tokens->current(); + + if ($current->id === \T_NAME_QUALIFIED) { + $result[] = $current->text; + } + + $tokens->next(); + } + + return $result; + } + + #[Test] + public function theStreamIsMovedAfterTheMatchingNamespace(): void + { + self::assertSame('use', $this->stoppedAt('App', <<<'PHP' + namespace App; + + use Some\Any; + PHP)); + } + + #[Test] + public function theNestedNamespaceNameIsMatched(): void + { + self::assertSame('use', $this->stoppedAt('App\\Domain', <<<'PHP' + namespace App\Domain; + + use Some\Any; + PHP)); + } + + #[Test] + public function theNonMatchingNamespaceExhaustsTheStream(): void + { + self::assertNull($this->stoppedAt('Other', <<<'PHP' + namespace App; + + use Some\Any; + PHP)); + } + + #[Test] + public function theRequestedNamespaceIsFoundAmongSeveralOnes(): void + { + self::assertSame(['Second\\Import'], $this->qualifiedNamesAfter('Second', <<<'PHP' + namespace First; + + use First\Import; + + namespace Second; + + use Second\Import; + PHP)); + } + + #[Test] + public function theBracedNamespaceNameIsTerminatedByTheOpeningBrace(): void + { + self::assertSame(['Some\\Any'], $this->qualifiedNamesAfter('App', <<<'PHP' + namespace App { + use Some\Any; + } + PHP)); + } + + #[Test] + public function theGlobalNamespaceIsMatchedByAnEmptyName(): void + { + self::assertSame(['Some\\Any'], $this->qualifiedNamesAfter('', <<<'PHP' + namespace { + use Some\Any; + } + PHP)); + } + + /** + * A source without any namespace declaration belongs to the global one, so + * its import section starts at the very first "use" statement. + */ + #[Test] + public function theFirstImportIsFoundInASourceWithoutNamespaces(): void + { + self::assertSame('use', $this->stoppedAt('', <<<'PHP' + use Some\Any; + PHP)); + } + + #[Test] + public function theSourceWithoutImportsExhaustsTheStream(): void + { + self::assertNull($this->stoppedAt('Other', <<<'PHP' + final class Example {} + PHP)); + } +} diff --git a/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php b/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php index 1682cd1..f463811 100644 --- a/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php +++ b/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php @@ -5,6 +5,7 @@ namespace TypeLang\Parser\Tests\TypeResolver; use TypeLang\Parser\Tests\TypeResolver\Stub\ClassWithGroupUsesStub; +use TypeLang\Parser\Tests\TypeResolver\Stub\ClassWithMethodStub; use TypeLang\Parser\Tests\TypeResolver\Stub\ClosureUseStub; use TypeLang\Parser\Tests\TypeResolver\Stub\CommentsAroundUsesStub; use TypeLang\Parser\Tests\TypeResolver\Stub\FunctionAndConstUseStub; @@ -32,6 +33,24 @@ private function read(string $class): array return ($this->reader ??= new PhpUseStatementsReader())->getClassUseStatements(new \ReflectionClass($class)); } + /** + * @return array + */ + private function readFunction(\ReflectionFunctionAbstract $function): array + { + return ($this->reader ??= new PhpUseStatementsReader())->getFunctionUseStatements($function); + } + + /** + * @throws \ReflectionException + */ + private function functionStub(): \ReflectionFunction + { + require_once __DIR__ . '/Stub/functions.php'; + + return new \ReflectionFunction(__NAMESPACE__ . '\Stub\exampleFunctionStub'); + } + public function testReadsPlainAndAliasedImports(): void { self::assertSame([ @@ -181,4 +200,33 @@ public function testReadsMultipleImportsInOneStatement(): void 'Some\C', ], $this->read(MultipleImportsPerStatementStub::class)); } + + public function testReadsImportsOfFunction(): void + { + self::assertSame([ + // use Some\Any; + 'Some\Any', + // use Some\Any\Test as Example; + 'Example' => 'Some\Any\Test', + // use function Some\helper; + 'Some\helper', + ], $this->readFunction($this->functionStub())); + } + + /** + * A method is not a namespaced symbol on its own, so its imports have to + * be read from the declaring class instead. + */ + public function testReturnsEmptyForMethod(): void + { + self::assertSame( + [], + $this->readFunction(new \ReflectionMethod(ClassWithMethodStub::class, 'example')), + ); + } + + public function testReturnsEmptyForInternalFunction(): void + { + self::assertSame([], $this->readFunction(new \ReflectionFunction('strlen'))); + } } diff --git a/libs/parser/tests/TypeResolver/ReflectionSourcePrefixReaderTest.php b/libs/parser/tests/TypeResolver/ReflectionSourcePrefixReaderTest.php new file mode 100644 index 0000000..bb0782f --- /dev/null +++ b/libs/parser/tests/TypeResolver/ReflectionSourcePrefixReaderTest.php @@ -0,0 +1,102 @@ +reader = new ReflectionSourcePrefixReader(); + } + + /** + * @throws \ReflectionException + */ + private function functionStub(): \ReflectionFunction + { + require_once __DIR__ . '/Stub/functions.php'; + + return new \ReflectionFunction(__NAMESPACE__ . '\Stub\exampleFunctionStub'); + } + + #[Test] + public function theClassHeaderContainsTheImports(): void + { + $header = $this->reader->readClassHeader(new \ReflectionClass(SimpleClassStub::class)); + + self::assertStringContainsString('use Some\Any;', $header); + self::assertStringContainsString('use Some\Any\Test as Example;', $header); + } + + #[Test] + public function theClassHeaderDoesNotContainTheDeclarationItself(): void + { + $header = $this->reader->readClassHeader(new \ReflectionClass(SimpleClassStub::class)); + + self::assertStringNotContainsString('final class SimpleClassStub', $header); + } + + #[Test] + public function theClassHeaderIsEmptyForAnInternalClass(): void + { + self::assertSame('', $this->reader->readClassHeader(new \ReflectionClass(\stdClass::class))); + } + + #[Test] + public function theFunctionHeaderContainsTheImports(): void + { + $header = $this->reader->readFunctionHeader($this->functionStub()); + + self::assertStringContainsString('use Some\Any;', $header); + self::assertStringContainsString('use function Some\helper;', $header); + } + + #[Test] + public function theFunctionHeaderDoesNotContainTheDeclarationItself(): void + { + $header = $this->reader->readFunctionHeader($this->functionStub()); + + self::assertStringNotContainsString('function exampleFunctionStub', $header); + } + + #[Test] + public function theFunctionHeaderIsEmptyForAnInternalFunction(): void + { + self::assertSame('', $this->reader->readFunctionHeader(new \ReflectionFunction('strlen'))); + } + + #[Test] + public function theMethodHeaderIsReadFromItsOwnFile(): void + { + $header = $this->reader->readFunctionHeader( + new \ReflectionMethod(ClassWithMethodStub::class, 'example'), + ); + + self::assertStringContainsString('use Some\Method\Any;', $header); + } + + #[Test] + public function theHeaderStartsAtTheBeginningOfTheFile(): void + { + $header = $this->reader->readClassHeader(new \ReflectionClass(SimpleClassStub::class)); + + self::assertStringStartsWith('resolve($this->parse($code)); + + self::assertInstanceOf(NamedTypeNode::class, $node); + + return $node->name->toString(); + } + + /** + * @throws \ReflectionException + */ + private function functionStub(): \ReflectionFunction + { + require_once __DIR__ . '/Stub/functions.php'; + + return new \ReflectionFunction(__NAMESPACE__ . '\\Stub\\exampleFunctionStub'); + } + + #[Test] + public function theNonAliasedImportOfAClassIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame('Some\\Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theAliasedImportOfAClassIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame( + 'Some\\Any\\Test\\Nested', + $this->resolveName($resolver, 'Example\\Nested'), + ); + } + + #[Test] + public function withTypeImportsFromClassReturnsANewInstance(): void + { + $resolver = new TypeResolver(); + + self::assertNotSame( + $resolver, + $resolver->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)), + ); + } + + #[Test] + public function withTypeImportsFromClassDoesNotModifyTheOriginalResolver(): void + { + $resolver = new TypeResolver(); + $resolver->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theImportsOfAClassAreMergedWithTheExistingOnes(): void + { + $resolver = (new TypeResolver()) + ->withTypeImport('App\\Node') + ->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame('App\\Node', $this->resolveName($resolver, 'Node')); + self::assertSame('Some\\Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theClassWithoutImportsChangesNothing(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(NoImportsStub::class)); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theInternalClassChangesNothing(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(\stdClass::class)); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theNonAliasedImportOfAFunctionIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction($this->functionStub()); + + self::assertSame('Some\\Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theAliasedImportOfAFunctionIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction($this->functionStub()); + + self::assertSame( + 'Some\\Any\\Test\\Nested', + $this->resolveName($resolver, 'Example\\Nested'), + ); + } + + #[Test] + public function withTypeImportsFromFunctionReturnsANewInstance(): void + { + $resolver = new TypeResolver(); + + self::assertNotSame( + $resolver, + $resolver->withTypeImportsFromFunction($this->functionStub()), + ); + } + + #[Test] + public function withTypeImportsFromFunctionDoesNotModifyTheOriginalResolver(): void + { + $resolver = new TypeResolver(); + $resolver->withTypeImportsFromFunction($this->functionStub()); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theImportsOfAMethodAreReadFromItsDeclaringClass(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction( + new \ReflectionMethod(ClassWithMethodStub::class, 'example'), + ); + + self::assertSame('Some\\Method\\Any', $this->resolveName($resolver, 'Any')); + self::assertSame( + 'Some\\Method\\Any\\Test', + $this->resolveName($resolver, 'Example'), + ); + } + + #[Test] + public function theInternalFunctionChangesNothing(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction(new \ReflectionFunction('strlen')); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + /** + * A closure is not a namespaced symbol on its own, so no imports can be + * associated with it. + */ + public function theClosureChangesNothing(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction( + new \ReflectionFunction(static fn(): int => 42), + ); + + self::assertSame('TypeResolver', $this->resolveName($resolver, 'TypeResolver')); + } +} diff --git a/libs/parser/tests/TypeResolver/TypeResolverTest.php b/libs/parser/tests/TypeResolver/TypeResolverTest.php index 5759b39..acd1ffa 100644 --- a/libs/parser/tests/TypeResolver/TypeResolverTest.php +++ b/libs/parser/tests/TypeResolver/TypeResolverTest.php @@ -142,14 +142,8 @@ public function testResolvesNamesInsideUnion(): void UnionTypeNode NamedTypeNode Name(TypeLang\Parser\Node) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Node) NamedTypeNode Name(TypeLang\Parser\Exception) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Exception) AST, $this->print($resolver->resolve($this->parse('Node|Error')))); } @@ -162,21 +156,13 @@ public function testResolvesNamesInsideShape(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\ImplicitFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(TypeLang\Parser\Node) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Node) - Shape\ImplicitFieldNode(required) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(TypeLang\Parser\Exception\SemanticException) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Exception) - Identifier(SemanticException) AST, $this->print($resolver->resolve($this->parse('array{Node, Error\SemanticException}')))); } @@ -189,14 +175,10 @@ public function testResolvesNamesInsideGenericArguments(): void self::assertSame(<<<'AST' NamedTypeNode Name(Vendor\Collection) - Identifier(Vendor) - Identifier(Collection) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(Vendor\Node) - Identifier(Vendor) - Identifier(Node) AST, $this->print($resolver->resolve($this->parse('Collection')))); } @@ -277,18 +259,12 @@ public function testResolvesNamesInsideCallableType(): void self::assertSame(<<<'AST' CallableTypeNode Name(callable) - Identifier(callable) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(App\Node) - Identifier(App) - Identifier(Node) NamedTypeNode Name(App\Err\Exception) - Identifier(App) - Identifier(Err) - Identifier(Exception) AST, $this->print($resolver->resolve($this->parse('callable(Node): Error')))); } @@ -300,8 +276,6 @@ public function testResolvesClassInClassConstant(): void self::assertSame(<<<'AST' ClassConstNode Name(App\Node) - Identifier(App) - Identifier(Node) Identifier(FOO) AST, $this->print($resolver->resolve($this->parse('Node::FOO')))); } @@ -314,8 +288,6 @@ public function testResolvesClassInClassConstantMask(): void self::assertSame(<<<'AST' ClassConstMaskNode Name(App\Node) - Identifier(App) - Identifier(Node) AST, $this->print($resolver->resolve($this->parse('Node::*')))); } @@ -329,13 +301,8 @@ public function testResolvesNamesInsideIntersection(): void IntersectionTypeNode NamedTypeNode Name(App\Node) - Identifier(App) - Identifier(Node) NamedTypeNode Name(App\Err\Exception) - Identifier(App) - Identifier(Err) - Identifier(Exception) AST, $this->print($resolver->resolve($this->parse('Node&Error')))); } @@ -350,13 +317,8 @@ public function testResolvesNamesInsideConditional(): void Condition\EqualConditionNode NamedTypeNode Name(App\Node) - Identifier(App) - Identifier(Node) NamedTypeNode Name(App\Err\Exception) - Identifier(App) - Identifier(Err) - Identifier(Exception) Literal\BoolLiteralNode(true) Literal\BoolLiteralNode(false) AST, $this->print($resolver->resolve($this->parse('Node is Error ? true : false')))); From c0fb78ed632bdabbc90798547ee26837301a178e Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 6 Sep 2026 21:39:04 +0300 Subject: [PATCH 027/119] Fix MatcherVisitor --- libs/parser/src/Traverser/MatcherVisitor.php | 1 + .../tests/Traverser/MatcherVisitorTest.php | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/libs/parser/src/Traverser/MatcherVisitor.php b/libs/parser/src/Traverser/MatcherVisitor.php index b44c8df..37e7e5d 100644 --- a/libs/parser/src/Traverser/MatcherVisitor.php +++ b/libs/parser/src/Traverser/MatcherVisitor.php @@ -32,6 +32,7 @@ public function hasMatches(): bool public function before(): void { $this->node = null; + $this->shouldContinue = false; } public function enter(Node $node): ?Command diff --git a/libs/parser/tests/Traverser/MatcherVisitorTest.php b/libs/parser/tests/Traverser/MatcherVisitorTest.php index 672425c..6f7a7ed 100644 --- a/libs/parser/tests/Traverser/MatcherVisitorTest.php +++ b/libs/parser/tests/Traverser/MatcherVisitorTest.php @@ -160,6 +160,36 @@ public function beforeResetsThePreviouslyMatchedNode(): void self::assertNull($visitor->node); } + #[Test] + public function theSameVisitorCanBeUsedSeveralTimes(): void + { + $visitor = new MatcherVisitor(static fn(Node $node): bool => $node instanceof NamedTypeNode); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([$this->type('First')]); + $expected = $this->type('Second'); + $traverser->traverse([$expected]); + + self::assertTrue($visitor->hasMatches()); + self::assertSame($expected, $visitor->node); + } + + #[Test] + public function theBreakConditionDoesNotAffectTheNextTraversal(): void + { + $visitor = new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof Name, + break: static fn(Node $node): bool => $node instanceof UnionTypeNode, + ); + + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([new UnionTypeNode($this->type('A'), $this->type('B'))]); + $traverser->traverse([$this->type('Foo\\Bar')]); + + self::assertTrue($visitor->hasMatches()); + } + #[Test] public function theMatcherReceivesEveryVisitedNode(): void { From a7705711de2cf166c07a654b5395870db21c503d Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 00:47:11 +0300 Subject: [PATCH 028/119] Add node offsets and remove attribute nodes --- libs/types/README.md | 4 +- .../Attribute/AttributeArgumentListNode.php | 12 -- .../src/Attribute/AttributeArgumentNode.php | 16 -- .../src/Attribute/AttributeGroupListNode.php | 12 -- .../src/Attribute/AttributeGroupNode.php | 12 -- libs/types/src/Attribute/AttributeNode.php | 16 -- .../src/Callable/CallableParameterNode.php | 8 +- libs/types/src/CallableTypeNode.php | 8 +- libs/types/src/ClassConstMaskNode.php | 8 +- libs/types/src/ClassConstNode.php | 8 +- libs/types/src/Condition/Condition.php | 8 +- libs/types/src/ConstMaskNode.php | 8 +- libs/types/src/Identifier.php | 11 +- libs/types/src/Literal/BoolLiteralNode.php | 6 +- libs/types/src/Literal/FloatLiteralNode.php | 6 +- libs/types/src/Literal/IntLiteralNode.php | 4 +- libs/types/src/Literal/LiteralNode.php | 8 +- libs/types/src/Literal/NullLiteralNode.php | 7 +- libs/types/src/Literal/StringLiteralNode.php | 8 +- .../types/src/Literal/VariableLiteralNode.php | 5 +- libs/types/src/LogicalTypeNode.php | 67 ++++--- libs/types/src/Name.php | 11 +- libs/types/src/NamedTypeNode.php | 8 +- libs/types/src/Node.php | 14 +- libs/types/src/NodeList.php | 15 +- libs/types/src/Shape/ClassConstFieldNode.php | 8 +- .../src/Shape/ClassConstMaskFieldNode.php | 8 +- libs/types/src/Shape/ConstMaskFieldNode.php | 8 +- libs/types/src/Shape/ExplicitFieldNode.php | 8 +- libs/types/src/Shape/FieldNode.php | 10 +- libs/types/src/Shape/FieldsListNode.php | 4 +- libs/types/src/Shape/NamedFieldNode.php | 8 +- libs/types/src/Shape/NumericFieldNode.php | 8 +- libs/types/src/Shape/StringNamedFieldNode.php | 8 +- .../src/Template/TemplateArgumentNode.php | 8 +- libs/types/src/TernaryExpressionNode.php | 8 +- libs/types/src/TypeOffsetAccessNode.php | 4 +- libs/types/src/WrappingTypeNode.php | 6 +- .../Attribute/AttributeArgumentNodeTest.php | 50 ----- .../AttributeArgumentsListNodeTest.php | 131 ------------- .../Attribute/AttributeGroupNodeTest.php | 78 -------- .../Attribute/AttributeGroupsListNodeTest.php | 78 -------- .../tests/Attribute/AttributeNodeTest.php | 48 ----- .../Callable/CallableParameterNodeTest.php | 1 - libs/types/tests/IntersectionTypeNodeTest.php | 12 +- libs/types/tests/Literal/LiteralNodeTest.php | 2 +- .../tests/Literal/StringLiteralNodeTest.php | 8 +- libs/types/tests/LogicalTypeNodeTest.php | 174 +++++++++++++++--- libs/types/tests/NodeTest.php | 159 ++++++++++++++-- .../tests/Shape/ExplicitFieldNodeTest.php | 14 -- .../tests/Shape/ImplicitFieldNodeTest.php | 7 - .../Template/TemplateArgumentNodeTest.php | 7 - libs/types/tests/UnionTypeNodeTest.php | 18 +- 53 files changed, 547 insertions(+), 626 deletions(-) delete mode 100644 libs/types/src/Attribute/AttributeArgumentListNode.php delete mode 100644 libs/types/src/Attribute/AttributeArgumentNode.php delete mode 100644 libs/types/src/Attribute/AttributeGroupListNode.php delete mode 100644 libs/types/src/Attribute/AttributeGroupNode.php delete mode 100644 libs/types/src/Attribute/AttributeNode.php delete mode 100644 libs/types/tests/Attribute/AttributeArgumentNodeTest.php delete mode 100644 libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php delete mode 100644 libs/types/tests/Attribute/AttributeGroupNodeTest.php delete mode 100644 libs/types/tests/Attribute/AttributeGroupsListNodeTest.php delete mode 100644 libs/types/tests/Attribute/AttributeNodeTest.php diff --git a/libs/types/README.md b/libs/types/README.md index 20db434..fe74967 100644 --- a/libs/types/README.md +++ b/libs/types/README.md @@ -108,6 +108,6 @@ foreach ($list as $item) { /* ... */ } ``` The package covers the full type grammar — unions and intersections, callables, -conditional (ternary) expressions, class-constant masks, literals, shape fields -and attributes. See the [documentation](https://typelang.dev) for the complete +conditional (ternary) expressions, class-constant masks, literals and shape +fields. See the [documentation](https://typelang.dev) for the complete node reference. diff --git a/libs/types/src/Attribute/AttributeArgumentListNode.php b/libs/types/src/Attribute/AttributeArgumentListNode.php deleted file mode 100644 index 2d91602..0000000 --- a/libs/types/src/Attribute/AttributeArgumentListNode.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ -final class AttributeArgumentListNode extends NodeList {} diff --git a/libs/types/src/Attribute/AttributeArgumentNode.php b/libs/types/src/Attribute/AttributeArgumentNode.php deleted file mode 100644 index 5c23012..0000000 --- a/libs/types/src/Attribute/AttributeArgumentNode.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ -final class AttributeGroupListNode extends NodeList {} diff --git a/libs/types/src/Attribute/AttributeGroupNode.php b/libs/types/src/Attribute/AttributeGroupNode.php deleted file mode 100644 index 04a38c4..0000000 --- a/libs/types/src/Attribute/AttributeGroupNode.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ -final class AttributeGroupNode extends NodeList {} diff --git a/libs/types/src/Attribute/AttributeNode.php b/libs/types/src/Attribute/AttributeNode.php deleted file mode 100644 index 99f1110..0000000 --- a/libs/types/src/Attribute/AttributeNode.php +++ /dev/null @@ -1,16 +0,0 @@ - $offset + */ public function __construct( public ?TypeNode $type = null, public ?VariableLiteralNode $name = null, public bool $isOutput = false, public bool $isVariadic = false, public bool $isOptional = false, - public ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { + parent::__construct($offset); + \assert($type !== null || $name !== null, new \TypeError( 'Required indication of the type or name of the parameter (one of)', )); diff --git a/libs/types/src/CallableTypeNode.php b/libs/types/src/CallableTypeNode.php index 4d3b817..753ff6e 100644 --- a/libs/types/src/CallableTypeNode.php +++ b/libs/types/src/CallableTypeNode.php @@ -8,9 +8,15 @@ final class CallableTypeNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $name, public CallableParameterListNode $parameters = new CallableParameterListNode(), public ?TypeNode $type = null, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/ClassConstMaskNode.php b/libs/types/src/ClassConstMaskNode.php index a9332fd..9548c6a 100644 --- a/libs/types/src/ClassConstMaskNode.php +++ b/libs/types/src/ClassConstMaskNode.php @@ -6,8 +6,14 @@ final class ClassConstMaskNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $class, public ?Identifier $constant = null, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/ClassConstNode.php b/libs/types/src/ClassConstNode.php index f86f5f1..6b4e5d7 100644 --- a/libs/types/src/ClassConstNode.php +++ b/libs/types/src/ClassConstNode.php @@ -6,8 +6,14 @@ final class ClassConstNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $class, public Identifier $constant, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/Condition/Condition.php b/libs/types/src/Condition/Condition.php index 1150483..55f726e 100644 --- a/libs/types/src/Condition/Condition.php +++ b/libs/types/src/Condition/Condition.php @@ -9,8 +9,14 @@ abstract class Condition extends Node { + /** + * @param int<0, max> $offset + */ public function __construct( public TypeNode $subject, public TypeNode $target, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/ConstMaskNode.php b/libs/types/src/ConstMaskNode.php index 9e059e9..b26f964 100644 --- a/libs/types/src/ConstMaskNode.php +++ b/libs/types/src/ConstMaskNode.php @@ -6,7 +6,13 @@ final class ConstMaskNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $name, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/Identifier.php b/libs/types/src/Identifier.php index d3c2e4f..2545d43 100644 --- a/libs/types/src/Identifier.php +++ b/libs/types/src/Identifier.php @@ -39,6 +39,7 @@ final class Identifier extends Node implements \Stringable ]; /** + * @param int<0, max> $offset * @throws \InvalidArgumentException in case of an empty value */ public function __construct( @@ -46,13 +47,19 @@ public function __construct( * @var non-empty-string */ public readonly string $value, + int $offset = 0, ) { if ($value === '') { throw new \InvalidArgumentException('Name identifier cannot be empty'); } + + parent::__construct($offset); } - public static function createFromString(string|\Stringable $value): self + /** + * @param int<0, max> $offset + */ + public static function createFromString(string|\Stringable $value, int $offset = 0): self { if ($value instanceof self) { return $value; @@ -64,7 +71,7 @@ public static function createFromString(string|\Stringable $value): self throw new \InvalidArgumentException('Name identifier cannot be empty'); } - return new self($normalized); + return new self($normalized, $offset); } /** diff --git a/libs/types/src/Literal/BoolLiteralNode.php b/libs/types/src/Literal/BoolLiteralNode.php index c798d41..09f1ded 100644 --- a/libs/types/src/Literal/BoolLiteralNode.php +++ b/libs/types/src/Literal/BoolLiteralNode.php @@ -11,10 +11,14 @@ */ final class BoolLiteralNode extends LiteralNode { + /** + * @param int<0, max> $offset + */ public function __construct( bool $value, ?string $raw = null, + int $offset = 0, ) { - parent::__construct($value, $raw ?? ($value ? 'true' : 'false')); + parent::__construct($value, $raw ?? ($value ? 'true' : 'false'), $offset); } } diff --git a/libs/types/src/Literal/FloatLiteralNode.php b/libs/types/src/Literal/FloatLiteralNode.php index ed9fc9e..7b8580a 100644 --- a/libs/types/src/Literal/FloatLiteralNode.php +++ b/libs/types/src/Literal/FloatLiteralNode.php @@ -11,10 +11,14 @@ */ final class FloatLiteralNode extends LiteralNode { + /** + * @param int<0, max> $offset + */ public function __construct( float $value, ?string $raw = null, + int $offset = 0, ) { - parent::__construct($value, $raw ?? (string) $value); + parent::__construct($value, $raw ?? (string) $value, $offset); } } diff --git a/libs/types/src/Literal/IntLiteralNode.php b/libs/types/src/Literal/IntLiteralNode.php index 2b18b06..54f1c0e 100644 --- a/libs/types/src/Literal/IntLiteralNode.php +++ b/libs/types/src/Literal/IntLiteralNode.php @@ -18,14 +18,16 @@ final class IntLiteralNode extends LiteralNode /** * @param numeric-string|null $decimal + * @param int<0, max> $offset */ public function __construct( int $value, ?string $raw = null, ?string $decimal = null, + int $offset = 0, ) { $this->decimal = $decimal ?? (string) $value; - parent::__construct($value, $raw ?? (string) $value); + parent::__construct($value, $raw ?? (string) $value, $offset); } } diff --git a/libs/types/src/Literal/LiteralNode.php b/libs/types/src/Literal/LiteralNode.php index 60f8ecd..1e43593 100644 --- a/libs/types/src/Literal/LiteralNode.php +++ b/libs/types/src/Literal/LiteralNode.php @@ -16,13 +16,19 @@ */ abstract class LiteralNode extends TypeNode implements LiteralNodeInterface { + /** + * @param int<0, max> $offset + */ public function __construct( /** * @var TValue */ public readonly mixed $value, public readonly string $raw, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } public function __toString(): string { diff --git a/libs/types/src/Literal/NullLiteralNode.php b/libs/types/src/Literal/NullLiteralNode.php index 98c695e..fb5d7e1 100644 --- a/libs/types/src/Literal/NullLiteralNode.php +++ b/libs/types/src/Literal/NullLiteralNode.php @@ -9,8 +9,11 @@ */ final class NullLiteralNode extends LiteralNode { - public function __construct(?string $raw = null) + /** + * @param int<0, max> $offset + */ + public function __construct(?string $raw = null, int $offset = 0) { - parent::__construct(null, $raw ?? 'null'); + parent::__construct(null, $raw ?? 'null', $offset); } } diff --git a/libs/types/src/Literal/StringLiteralNode.php b/libs/types/src/Literal/StringLiteralNode.php index 409ff77..eb211e3 100644 --- a/libs/types/src/Literal/StringLiteralNode.php +++ b/libs/types/src/Literal/StringLiteralNode.php @@ -11,12 +11,16 @@ */ final class StringLiteralNode extends LiteralNode { + /** + * @param int<0, max> $offset + */ final public function __construct( string $value, ?string $raw = null, + int $offset = 0, ) { - $raw ??= \sprintf('"%s"', \addcslashes($value, '"')); + $raw ??= \sprintf("'%s'", \addcslashes($value, "'")); - parent::__construct($value, $raw); + parent::__construct($value, $raw, $offset); } } diff --git a/libs/types/src/Literal/VariableLiteralNode.php b/libs/types/src/Literal/VariableLiteralNode.php index 222ea91..4219f6c 100644 --- a/libs/types/src/Literal/VariableLiteralNode.php +++ b/libs/types/src/Literal/VariableLiteralNode.php @@ -13,9 +13,10 @@ final class VariableLiteralNode extends LiteralNode { /** * @param non-empty-string $value + * @param int<0, max> $offset */ - public function __construct(string $value) + public function __construct(string $value, int $offset = 0) { - parent::__construct($value, '$' . $value); + parent::__construct($value, '$' . $value, $offset); } } diff --git a/libs/types/src/LogicalTypeNode.php b/libs/types/src/LogicalTypeNode.php index 55ee9b6..525db56 100644 --- a/libs/types/src/LogicalTypeNode.php +++ b/libs/types/src/LogicalTypeNode.php @@ -9,35 +9,63 @@ * * @template-implements \IteratorAggregate */ -abstract class LogicalTypeNode extends TypeNode implements \IteratorAggregate, \Countable +abstract class LogicalTypeNode extends TypeNode implements + \IteratorAggregate, + \Countable { /** * @var non-empty-list */ public array $statements; + /** + * A logical statement accepts an arbitrary number of types, so there is no + * place left for an offset argument: use the {@see $offset} property. + * + * @param iterable $statements + * @param int<0, max> $offset + * @throws \LogicException in case of less than two statements are passed + */ public function __construct( - TypeNode $a, - TypeNode $b, - TypeNode ...$other, + iterable $statements, + int $offset = 0, ) { - // @phpstan-ignore-next-line : List of types cannot be empty - $this->statements = [...$this->unwrap([$a, $b, ...$other])]; + $statements = self::unwrap($statements); + + if (\count($statements) < 2) { + throw new \LogicException('A logical statement must contain at least 2 elements'); + } + + // @phpstan-ignore-next-line : List of types contains at least 2 elements + $this->statements = $statements; + + parent::__construct($offset); } /** - * @param non-empty-list $statements - * @return iterable + * Flattens the statements of the same kind into a single list, so that + * a logical statement never contains a statement of its own kind. + * + * @param iterable $statements + * @return list */ - private function unwrap(array $statements): iterable + private static function unwrap(iterable $statements): array { + $result = []; + foreach ($statements as $statement) { if ($statement instanceof static) { - yield from $this->unwrap($statement->statements); - } else { - yield $statement; + foreach (self::unwrap($statement->statements) as $child) { + $result[] = $child; + } + + continue; } + + $result[] = $statement; } + + return $result; } public function getIterator(): \Traversable @@ -55,27 +83,24 @@ public function count(): int } /** - * @return array{int<0, max>, non-empty-list} + * @return array{non-empty-list, int<0, max>} */ public function __serialize(): array { - return [$this->offset, $this->statements]; + return [$this->statements, $this->offset]; } /** - * @param array{0?: int<0, max>, 1?: non-empty-list} $data + * @param array{0?: non-empty-list, 1?: int<0, max>} $data * @throws \UnexpectedValueException */ public function __unserialize(array $data): void { - $this->offset = $data[0] ?? throw new \UnexpectedValueException(\sprintf( - 'Unable to unserialize %s offset', - static::class, - )); - - $this->statements = $data[1] ?? throw new \UnexpectedValueException(\sprintf( + $this->statements = $data[0] ?? throw new \UnexpectedValueException(\sprintf( 'Unable to unserialize %s statements', static::class, )); + + $this->offset = $data[1] ?? 0; } } diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index 0c20d90..eacafad 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -28,12 +28,14 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String /** * @param iterable $parts + * @param int<0, max> $offset * @throws \InvalidArgumentException in case of parts are empty or contain * something else than an {@see Identifier} */ public function __construct( iterable $parts, public readonly bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, + int $offset = 0, ) { $parts = match (true) { $parts instanceof \Traversable => \iterator_to_array($parts, false), @@ -59,9 +61,14 @@ public function __construct( $this->first = $first; $this->last = $last; + + parent::__construct($offset); } - public static function createFromString(string|\Stringable $name): self + /** + * @param int<0, max> $offset + */ + public static function createFromString(string|\Stringable $name, int $offset = 0): self { $name = (string) $name; $parts = []; @@ -74,7 +81,7 @@ public static function createFromString(string|\Stringable $name): self $parts[] = Identifier::createFromString($segment); } - return new self($parts, \str_starts_with($name, self::NAMESPACE_DELIMITER)); + return new self($parts, \str_starts_with($name, self::NAMESPACE_DELIMITER), $offset); } /** diff --git a/libs/types/src/NamedTypeNode.php b/libs/types/src/NamedTypeNode.php index 9a7d35a..b445c4a 100644 --- a/libs/types/src/NamedTypeNode.php +++ b/libs/types/src/NamedTypeNode.php @@ -9,9 +9,15 @@ final class NamedTypeNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $name, public ?TemplateArgumentListNode $arguments = null, public ?FieldsListNode $fields = null, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/Node.php b/libs/types/src/Node.php index 34e3e03..51e3fe3 100644 --- a/libs/types/src/Node.php +++ b/libs/types/src/Node.php @@ -6,12 +6,14 @@ abstract class Node implements NodeInterface { - /** - * Token offset defined in the source code. - * - * @var int<0, max> - */ - public int $offset = 0; + public function __construct( + /** + * Token offset defined in the source code. + * + * @var int<0, max> + */ + public int $offset = 0, + ) {} public function getOffset(): int { diff --git a/libs/types/src/NodeList.php b/libs/types/src/NodeList.php index 36bd600..ef867d7 100644 --- a/libs/types/src/NodeList.php +++ b/libs/types/src/NodeList.php @@ -22,12 +22,19 @@ abstract class NodeList extends Node implements /** * @param iterable $items + * @param int<0, max> $offset */ - public function __construct(iterable $items = []) + public function __construct(iterable $items = [], int $offset = 0) { - $this->items = \is_array($items) - ? \array_values($items) - : \iterator_to_array($items, false); + $this->items = match (true) { + // A list is already shaped the way it is stored, so it is taken + // as it is rather than copied + \is_array($items) && \array_is_list($items) => $items, + \is_array($items) => \array_values($items), + default => \iterator_to_array($items, false), + }; + + parent::__construct($offset); } /** diff --git a/libs/types/src/Shape/ClassConstFieldNode.php b/libs/types/src/Shape/ClassConstFieldNode.php index d418c16..01e3442 100644 --- a/libs/types/src/Shape/ClassConstFieldNode.php +++ b/libs/types/src/Shape/ClassConstFieldNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\ClassConstNode; use TypeLang\Type\TypeNode; @@ -13,17 +12,20 @@ */ final class ClassConstFieldNode extends ExplicitFieldNode { + /** + * @param int<0, max> $offset + */ public function __construct( ClassConstNode $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( key: $key, type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } diff --git a/libs/types/src/Shape/ClassConstMaskFieldNode.php b/libs/types/src/Shape/ClassConstMaskFieldNode.php index e1fc75f..87fafb3 100644 --- a/libs/types/src/Shape/ClassConstMaskFieldNode.php +++ b/libs/types/src/Shape/ClassConstMaskFieldNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\ClassConstMaskNode; use TypeLang\Type\TypeNode; @@ -13,17 +12,20 @@ */ final class ClassConstMaskFieldNode extends ExplicitFieldNode { + /** + * @param int<0, max> $offset + */ public function __construct( ClassConstMaskNode $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( key: $key, type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } diff --git a/libs/types/src/Shape/ConstMaskFieldNode.php b/libs/types/src/Shape/ConstMaskFieldNode.php index 1975fbd..b8b95fb 100644 --- a/libs/types/src/Shape/ConstMaskFieldNode.php +++ b/libs/types/src/Shape/ConstMaskFieldNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\ConstMaskNode; use TypeLang\Type\TypeNode; @@ -13,17 +12,20 @@ */ final class ConstMaskFieldNode extends ExplicitFieldNode { + /** + * @param int<0, max> $offset + */ public function __construct( ConstMaskNode $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( key: $key, type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } diff --git a/libs/types/src/Shape/ExplicitFieldNode.php b/libs/types/src/Shape/ExplicitFieldNode.php index 8bd7eae..2e2cb41 100644 --- a/libs/types/src/Shape/ExplicitFieldNode.php +++ b/libs/types/src/Shape/ExplicitFieldNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\TypeNode; /** @@ -12,6 +11,9 @@ */ abstract class ExplicitFieldNode extends FieldNode { + /** + * @param int<0, max> $offset + */ public function __construct( /** * @var TKey @@ -19,12 +21,12 @@ public function __construct( public mixed $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } diff --git a/libs/types/src/Shape/FieldNode.php b/libs/types/src/Shape/FieldNode.php index c4d6f92..0791007 100644 --- a/libs/types/src/Shape/FieldNode.php +++ b/libs/types/src/Shape/FieldNode.php @@ -4,17 +4,21 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Node; use TypeLang\Type\TypeNode; abstract class FieldNode extends Node { + /** + * @param int<0, max> $offset + */ public function __construct( public TypeNode $type, public bool $isOptional = false, - public ?AttributeGroupListNode $attributes = null, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } /** * Returns {@see true} in case of the field is an instance of the diff --git a/libs/types/src/Shape/FieldsListNode.php b/libs/types/src/Shape/FieldsListNode.php index d675c0a..34814f8 100644 --- a/libs/types/src/Shape/FieldsListNode.php +++ b/libs/types/src/Shape/FieldsListNode.php @@ -13,11 +13,13 @@ final class FieldsListNode extends NodeList { /** * @param list $list + * @param int<0, max> $offset */ public function __construct( array $list = [], public bool $isSealed = true, + int $offset = 0, ) { - parent::__construct($list); + parent::__construct($list, $offset); } } diff --git a/libs/types/src/Shape/NamedFieldNode.php b/libs/types/src/Shape/NamedFieldNode.php index 3fe63ca..bf2e2cb 100644 --- a/libs/types/src/Shape/NamedFieldNode.php +++ b/libs/types/src/Shape/NamedFieldNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Identifier; use TypeLang\Type\TypeNode; @@ -13,17 +12,20 @@ */ final class NamedFieldNode extends ExplicitFieldNode { + /** + * @param int<0, max> $offset + */ public function __construct( Identifier $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( key: $key, type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } diff --git a/libs/types/src/Shape/NumericFieldNode.php b/libs/types/src/Shape/NumericFieldNode.php index 351cc0f..ed5cb8f 100644 --- a/libs/types/src/Shape/NumericFieldNode.php +++ b/libs/types/src/Shape/NumericFieldNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Literal\IntLiteralNode; use TypeLang\Type\TypeNode; @@ -13,17 +12,20 @@ */ final class NumericFieldNode extends ExplicitFieldNode { + /** + * @param int<0, max> $offset + */ public function __construct( IntLiteralNode $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( key: $key, type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } diff --git a/libs/types/src/Shape/StringNamedFieldNode.php b/libs/types/src/Shape/StringNamedFieldNode.php index 6362e2b..fe83802 100644 --- a/libs/types/src/Shape/StringNamedFieldNode.php +++ b/libs/types/src/Shape/StringNamedFieldNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Literal\StringLiteralNode; use TypeLang\Type\TypeNode; @@ -13,17 +12,20 @@ */ final class StringNamedFieldNode extends ExplicitFieldNode { + /** + * @param int<0, max> $offset + */ public function __construct( StringLiteralNode $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( key: $key, type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } diff --git a/libs/types/src/Template/TemplateArgumentNode.php b/libs/types/src/Template/TemplateArgumentNode.php index e7dbb62..4e06b30 100644 --- a/libs/types/src/Template/TemplateArgumentNode.php +++ b/libs/types/src/Template/TemplateArgumentNode.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Template; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Identifier; use TypeLang\Type\Node; use TypeLang\Type\TypeNode; @@ -13,11 +12,16 @@ final class TemplateArgumentNode extends Node { public ?Identifier $hint; + /** + * @param int<0, max> $offset + */ public function __construct( public TypeNode $value, ?Identifier $hint = null, - public ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { $this->hint = $hint; + + parent::__construct($offset); } } diff --git a/libs/types/src/TernaryExpressionNode.php b/libs/types/src/TernaryExpressionNode.php index 8aba2d9..5f5fd64 100644 --- a/libs/types/src/TernaryExpressionNode.php +++ b/libs/types/src/TernaryExpressionNode.php @@ -8,9 +8,15 @@ final class TernaryExpressionNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Condition $condition, public TypeNode $then, public TypeNode $else, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/TypeOffsetAccessNode.php b/libs/types/src/TypeOffsetAccessNode.php index d6a97ac..61b2bbc 100644 --- a/libs/types/src/TypeOffsetAccessNode.php +++ b/libs/types/src/TypeOffsetAccessNode.php @@ -13,11 +13,13 @@ final class TypeOffsetAccessNode extends WrappingTypeNode { /** * @param T $type + * @param int<0, max> $offset */ public function __construct( TypeNode $type, public readonly TypeNode $access, + int $offset = 0, ) { - parent::__construct($type); + parent::__construct($type, $offset); } } diff --git a/libs/types/src/WrappingTypeNode.php b/libs/types/src/WrappingTypeNode.php index 71c4cc0..0e5a44a 100644 --- a/libs/types/src/WrappingTypeNode.php +++ b/libs/types/src/WrappingTypeNode.php @@ -11,8 +11,12 @@ abstract class WrappingTypeNode extends TypeNode { /** * @param T $type + * @param int<0, max> $offset */ public function __construct( public TypeNode $type, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/tests/Attribute/AttributeArgumentNodeTest.php b/libs/types/tests/Attribute/AttributeArgumentNodeTest.php deleted file mode 100644 index 0b0385f..0000000 --- a/libs/types/tests/Attribute/AttributeArgumentNodeTest.php +++ /dev/null @@ -1,50 +0,0 @@ -value); - } - - #[Test] - public function attributesDefaultToNull(): void - { - $node = new AttributeArgumentNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertNull($node->attributes); - } - - #[Test] - public function constructorStoresAttributes(): void - { - $attrs = new AttributeGroupListNode(); - $value = new NamedTypeNode(Name::createFromString('int')); - $node = new AttributeArgumentNode($value, $attrs); - - self::assertSame($attrs, $node->attributes); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new AttributeArgumentNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php b/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php deleted file mode 100644 index 233f5eb..0000000 --- a/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php +++ /dev/null @@ -1,131 +0,0 @@ -first()); - self::assertNull($list->last()); - } - - #[Test] - public function constructorAcceptsItems(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertCount(2, $list); - } - - #[Test] - public function firstAndLastProperties(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertSame($a, $list->first()); - self::assertSame($b, $list->last()); - } - - #[Test] - public function arrayAccessOffsetExists(): void - { - $list = new AttributeArgumentListNode([$this->makeArg('int')]); - - self::assertTrue(isset($list[0])); - self::assertFalse(isset($list[1])); - } - - #[Test] - public function arrayAccessOffsetGet(): void - { - $arg = $this->makeArg('string'); - $list = new AttributeArgumentListNode([$arg]); - - self::assertSame($arg, $list[0]); - self::assertNull($list[1]); - } - - #[Test] - public function arrayAccessOffsetSet(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a]); - $list[0] = $b; - - self::assertSame($b, $list[0]); - } - - #[Test] - public function arrayAccessOffsetUnsetReindexes(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - unset($list[0]); - - self::assertCount(1, $list); - self::assertSame($b, $list[0]); - } - - #[Test] - public function findIndexReturnsCorrectPosition(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertSame(0, $list->findIndex($a)); - self::assertSame(1, $list->findIndex($b)); - } - - #[Test] - public function findIndexReturnsNullForAbsentNode(): void - { - $list = new AttributeArgumentListNode([$this->makeArg('int')]); - $absent = $this->makeArg('string'); - - self::assertNull($list->findIndex($absent)); - } - - #[Test] - public function iteratorYieldsItems(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertSame([$a, $b], \iterator_to_array($list)); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $list = new AttributeArgumentListNode(); - - self::assertSame(0, $list->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeGroupNodeTest.php b/libs/types/tests/Attribute/AttributeGroupNodeTest.php deleted file mode 100644 index 29b28fa..0000000 --- a/libs/types/tests/Attribute/AttributeGroupNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ -first()); - self::assertNull($group->last()); - } - - #[Test] - public function constructorAcceptsAttributes(): void - { - $a = $this->makeAttr('Pure'); - $b = $this->makeAttr('Deprecated'); - $group = new AttributeGroupNode([$a, $b]); - - self::assertCount(2, $group); - self::assertSame($a, $group->first()); - self::assertSame($b, $group->last()); - } - - #[Test] - public function findIndexReturnsPosition(): void - { - $a = $this->makeAttr('Pure'); - $b = $this->makeAttr('Deprecated'); - $group = new AttributeGroupNode([$a, $b]); - - self::assertSame(0, $group->findIndex($a)); - self::assertSame(1, $group->findIndex($b)); - } - - #[Test] - public function findIndexReturnsNullWhenNotFound(): void - { - $group = new AttributeGroupNode([$this->makeAttr('Pure')]); - - self::assertNull($group->findIndex($this->makeAttr('Other'))); - } - - #[Test] - public function iteratorYieldsAttributes(): void - { - $a = $this->makeAttr('A'); - $b = $this->makeAttr('B'); - $group = new AttributeGroupNode([$a, $b]); - - self::assertSame([$a, $b], \iterator_to_array($group)); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $group = new AttributeGroupNode(); - - self::assertSame(0, $group->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php b/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php deleted file mode 100644 index 9ae6b1b..0000000 --- a/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ - new AttributeNode(Name::createFromString($n)), - $names, - ); - - return new AttributeGroupNode($attrs); - } - - #[Test] - public function emptyListByDefault(): void - { - $list = new AttributeGroupListNode(); - - self::assertCount(0, $list); - self::assertNull($list->first()); - self::assertNull($list->last()); - } - - #[Test] - public function constructorAcceptsGroups(): void - { - $g1 = $this->makeGroup('Pure'); - $g2 = $this->makeGroup('Deprecated'); - $list = new AttributeGroupListNode([$g1, $g2]); - - self::assertCount(2, $list); - self::assertSame($g1, $list->first()); - self::assertSame($g2, $list->last()); - } - - #[Test] - public function arrayAccessWorks(): void - { - $g = $this->makeGroup('Pure'); - $list = new AttributeGroupListNode([$g]); - - self::assertTrue(isset($list[0])); - self::assertSame($g, $list[0]); - self::assertNull($list[1]); - } - - #[Test] - public function unsetRemovesAndReindexes(): void - { - $g1 = $this->makeGroup('A'); - $g2 = $this->makeGroup('B'); - $list = new AttributeGroupListNode([$g1, $g2]); - unset($list[0]); - - self::assertCount(1, $list); - self::assertSame($g2, $list[0]); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $list = new AttributeGroupListNode(); - - self::assertSame(0, $list->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeNodeTest.php b/libs/types/tests/Attribute/AttributeNodeTest.php deleted file mode 100644 index 114faa5..0000000 --- a/libs/types/tests/Attribute/AttributeNodeTest.php +++ /dev/null @@ -1,48 +0,0 @@ -name); - } - - #[Test] - public function argumentsDefaultToNull(): void - { - $node = new AttributeNode(Name::createFromString('Pure')); - - self::assertNull($node->arguments); - } - - #[Test] - public function constructorStoresArguments(): void - { - $args = new AttributeArgumentListNode(); - $node = new AttributeNode(Name::createFromString('Attr'), $args); - - self::assertSame($args, $node->arguments); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new AttributeNode(Name::createFromString('Attr')); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Callable/CallableParameterNodeTest.php b/libs/types/tests/Callable/CallableParameterNodeTest.php index 9c083ab..6ad79a0 100644 --- a/libs/types/tests/Callable/CallableParameterNodeTest.php +++ b/libs/types/tests/Callable/CallableParameterNodeTest.php @@ -25,7 +25,6 @@ public function constructorWithTypeOnly(): void self::assertFalse($node->isOutput); self::assertFalse($node->isVariadic); self::assertFalse($node->isOptional); - self::assertNull($node->attributes); } #[Test] diff --git a/libs/types/tests/IntersectionTypeNodeTest.php b/libs/types/tests/IntersectionTypeNodeTest.php index 6c6e2af..4332b02 100644 --- a/libs/types/tests/IntersectionTypeNodeTest.php +++ b/libs/types/tests/IntersectionTypeNodeTest.php @@ -21,7 +21,7 @@ public function constructorWithTwoTypes(): void { $a = $this->named('Countable'); $b = $this->named('Stringable'); - $node = new IntersectionTypeNode($a, $b); + $node = new IntersectionTypeNode([$a, $b]); self::assertCount(2, $node); self::assertSame([$a, $b], $node->statements); @@ -33,7 +33,7 @@ public function constructorWithThreeTypes(): void $a = $this->named('A'); $b = $this->named('B'); $c = $this->named('C'); - $node = new IntersectionTypeNode($a, $b, $c); + $node = new IntersectionTypeNode([$a, $b, $c]); self::assertCount(3, $node); } @@ -45,8 +45,8 @@ public function nestedIntersectionIsFlattened(): void $b = $this->named('B'); $c = $this->named('C'); - $inner = new IntersectionTypeNode($a, $b); - $outer = new IntersectionTypeNode($inner, $c); + $inner = new IntersectionTypeNode([$a, $b]); + $outer = new IntersectionTypeNode([$inner, $c]); self::assertCount(3, $outer); self::assertSame([$a, $b, $c], $outer->statements); @@ -57,7 +57,7 @@ public function iteratorYieldsStatements(): void { $a = $this->named('Countable'); $b = $this->named('Stringable'); - $node = new IntersectionTypeNode($a, $b); + $node = new IntersectionTypeNode([$a, $b]); self::assertSame([$a, $b], \iterator_to_array($node)); } @@ -65,7 +65,7 @@ public function iteratorYieldsStatements(): void #[Test] public function defaultOffsetIsZero(): void { - $node = new IntersectionTypeNode($this->named('A'), $this->named('B')); + $node = new IntersectionTypeNode([$this->named('A'), $this->named('B')]); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Literal/LiteralNodeTest.php b/libs/types/tests/Literal/LiteralNodeTest.php index fda8533..d7dbf11 100644 --- a/libs/types/tests/Literal/LiteralNodeTest.php +++ b/libs/types/tests/Literal/LiteralNodeTest.php @@ -82,7 +82,7 @@ public function nullLiteralDerivesRawFromValue(): void #[Test] public function stringLiteralDerivesRawFromValue(): void { - self::assertSame('"example"', (new StringLiteralNode('example'))->raw); + self::assertSame("'example'", (new StringLiteralNode('example'))->raw); } #[Test] diff --git a/libs/types/tests/Literal/StringLiteralNodeTest.php b/libs/types/tests/Literal/StringLiteralNodeTest.php index a9dc121..d0e2ad6 100644 --- a/libs/types/tests/Literal/StringLiteralNodeTest.php +++ b/libs/types/tests/Literal/StringLiteralNodeTest.php @@ -25,7 +25,7 @@ public function constructorDerivesRawWhenOmitted(): void $node = new StringLiteralNode('hello'); self::assertSame('hello', $node->value); - self::assertSame('"hello"', $node->raw); + self::assertSame("'hello'", $node->raw); } #[Test] @@ -47,9 +47,9 @@ public function defaultOffsetIsZero(): void #[Test] public function constructorEscapesTheDerivedRawValue(): void { - $node = new StringLiteralNode('a"b'); + $node = new StringLiteralNode("a'b"); - self::assertSame('"a\"b"', $node->raw); - self::assertSame('a"b', $node->value); + self::assertSame("'a\'b'", $node->raw); + self::assertSame("a'b", $node->value); } } diff --git a/libs/types/tests/LogicalTypeNodeTest.php b/libs/types/tests/LogicalTypeNodeTest.php index b3ccffd..fee9751 100644 --- a/libs/types/tests/LogicalTypeNodeTest.php +++ b/libs/types/tests/LogicalTypeNodeTest.php @@ -30,9 +30,9 @@ public static function provideLogicalTypes(): iterable #[Test] #[DataProvider('provideLogicalTypes')] - public function constructorRequiresAtLeastTwoStatements(string $class): void + public function twoStatementsAreEnough(string $class): void { - $node = new $class($this->type('A'), $this->type('B')); + $node = new $class([$this->type('A'), $this->type('B')]); self::assertCount(2, $node); self::assertCount(2, $node->statements); @@ -40,9 +40,14 @@ public function constructorRequiresAtLeastTwoStatements(string $class): void #[Test] #[DataProvider('provideLogicalTypes')] - public function variadicStatementsAreStored(string $class): void + public function anArbitraryNumberOfStatementsIsStored(string $class): void { - $node = new $class($this->type('A'), $this->type('B'), $this->type('C'), $this->type('D')); + $node = new $class([ + $this->type('A'), + $this->type('B'), + $this->type('C'), + $this->type('D'), + ]); self::assertCount(4, $node); } @@ -55,11 +60,100 @@ public function statementsOrderIsPreserved(string $class): void $b = $this->type('B'); $c = $this->type('C'); - $node = new $class($a, $b, $c); + $node = new $class([$a, $b, $c]); self::assertSame([$a, $b, $c], $node->statements); } + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsAreAcceptedFromAnyTraversable(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class(new \ArrayIterator([$a, $b])); + + self::assertSame([$a, $b], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsAreAcceptedFromAGenerator(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class((static function () use ($a, $b): \Generator { + yield $a; + yield $b; + })()); + + self::assertSame([$a, $b], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsAreReindexedIntoAList(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class([7 => $a, 42 => $b]); + + self::assertSame([$a, $b], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function aSingleStatementIsRejected(string $class): void + { + $this->expectException(\LogicException::class); + + new $class([$this->type('A')]); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function noStatementsAreRejected(string $class): void + { + $this->expectException(\LogicException::class); + + new $class([]); + } + + /** + * A statement built out of a single statement of the same kind is the very + * same statement: the nested one is flattened into it. + */ + #[Test] + #[DataProvider('provideLogicalTypes')] + public function aSingleNestedStatementIsUnwrapped(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class([new $class([$a, $b])]); + + self::assertSame([$a, $b], $node->statements); + } + + /** + * A nested statement is flattened before the number of statements is + * checked, so a single type wrapped into a statement is still too little. + */ + #[Test] + #[DataProvider('provideLogicalTypes')] + public function aStatementFlattenedIntoASingleTypeIsRejected(string $class): void + { + $node = new $class([$this->type('A'), $this->type('B')]); + $node->statements = [$this->type('C')]; + + $this->expectException(\LogicException::class); + + new $class([$node]); + } + #[Test] #[DataProvider('provideLogicalTypes')] public function nestedStatementsOfTheSameTypeAreFlattened(string $class): void @@ -68,7 +162,7 @@ public function nestedStatementsOfTheSameTypeAreFlattened(string $class): void $b = $this->type('B'); $c = $this->type('C'); - $node = new $class(new $class($a, $b), $c); + $node = new $class([new $class([$a, $b]), $c]); self::assertSame([$a, $b, $c], $node->statements); } @@ -82,7 +176,7 @@ public function deeplyNestedStatementsAreFlattened(string $class): void $c = $this->type('C'); $d = $this->type('D'); - $node = new $class(new $class(new $class($a, $b), $c), $d); + $node = new $class([new $class([new $class([$a, $b]), $c]), $d]); self::assertSame([$a, $b, $c, $d], $node->statements); } @@ -95,7 +189,7 @@ public function nestedStatementsAreFlattenedInAnyPosition(string $class): void $b = $this->type('B'); $c = $this->type('C'); - $node = new $class($a, new $class($b, $c)); + $node = new $class([$a, new $class([$b, $c])]); self::assertSame([$a, $b, $c], $node->statements); } @@ -103,9 +197,9 @@ public function nestedStatementsAreFlattenedInAnyPosition(string $class): void #[Test] public function unionDoesNotFlattenIntersection(): void { - $intersection = new IntersectionTypeNode($this->type('A'), $this->type('B')); + $intersection = new IntersectionTypeNode([$this->type('A'), $this->type('B')]); - $node = new UnionTypeNode($intersection, $this->type('C')); + $node = new UnionTypeNode([$intersection, $this->type('C')]); self::assertCount(2, $node); self::assertSame($intersection, $node->statements[0]); @@ -114,9 +208,9 @@ public function unionDoesNotFlattenIntersection(): void #[Test] public function intersectionDoesNotFlattenUnion(): void { - $union = new UnionTypeNode($this->type('A'), $this->type('B')); + $union = new UnionTypeNode([$this->type('A'), $this->type('B')]); - $node = new IntersectionTypeNode($union, $this->type('C')); + $node = new IntersectionTypeNode([$union, $this->type('C')]); self::assertCount(2, $node); self::assertSame($union, $node->statements[0]); @@ -129,7 +223,7 @@ public function iteratorYieldsStatements(string $class): void $a = $this->type('A'); $b = $this->type('B'); - $node = new $class($a, $b); + $node = new $class([$a, $b]); self::assertSame([$a, $b], \iterator_to_array($node->getIterator())); } @@ -138,8 +232,7 @@ public function iteratorYieldsStatements(string $class): void #[DataProvider('provideLogicalTypes')] public function serializationRoundtripPreservesStatementsAndOffset(string $class): void { - $node = new $class($this->type('A'), $this->type('B')); - $node->offset = 13; + $node = new $class([$this->type('A'), $this->type('B')], 13); /** @var LogicalTypeNode $restored */ $restored = \unserialize(\serialize($node)); @@ -153,19 +246,18 @@ public function serializationRoundtripPreservesStatementsAndOffset(string $class #[Test] #[DataProvider('provideLogicalTypes')] - public function serializePayloadContainsOffsetAndStatements(string $class): void + public function serializePayloadContainsStatementsAndOffset(string $class): void { - $node = new $class($this->type('A'), $this->type('B')); - $node->offset = 7; + $node = new $class([$this->type('A'), $this->type('B')], 7); - self::assertSame([7, $node->statements], $node->__serialize()); + self::assertSame([$node->statements, 7], $node->__serialize()); } #[Test] #[DataProvider('provideLogicalTypes')] - public function unserializeThrowsWhenOffsetIsMissing(string $class): void + public function unserializeThrowsWhenStatementsAreMissing(string $class): void { - $node = new $class($this->type('A'), $this->type('B')); + $node = new $class([$this->type('A'), $this->type('B')]); $this->expectException(\UnexpectedValueException::class); @@ -174,21 +266,51 @@ public function unserializeThrowsWhenOffsetIsMissing(string $class): void #[Test] #[DataProvider('provideLogicalTypes')] - public function unserializeThrowsWhenStatementsAreMissing(string $class): void + public function unserializeDefaultsTheOffsetToZero(string $class): void { - $node = new $class($this->type('A'), $this->type('B')); + $node = new $class([$this->type('A'), $this->type('B')], 42); - $this->expectException(\UnexpectedValueException::class); + $node->__unserialize([$node->statements]); + + self::assertSame(0, $node->offset); + } - $node->__unserialize([0]); + #[Test] + #[DataProvider('provideLogicalTypes')] + public function theOffsetIsPassedThroughTheConstructor(string $class): void + { + $node = new $class([$this->type('A'), $this->type('B')], 42); + + self::assertSame(42, $node->offset); + self::assertSame(42, $node->getOffset()); } #[Test] #[DataProvider('provideLogicalTypes')] public function defaultOffsetIsZero(string $class): void { - $node = new $class($this->type('A'), $this->type('B')); + $node = new $class([$this->type('A'), $this->type('B')]); self::assertSame(0, $node->offset); } + + /** + * Building a statement out of an already built one must not cost more the + * longer it gets: the flattening is linear in the number of statements. + */ + #[Test] + #[DataProvider('provideLogicalTypes')] + public function flatteningIsLinear(string $class): void + { + $statements = []; + + for ($i = 0; $i < 1000; ++$i) { + $statements[] = $this->type('T' . $i); + } + + $node = new $class($statements); + + self::assertCount(1000, $node); + self::assertSame($statements, $node->statements); + } } diff --git a/libs/types/tests/NodeTest.php b/libs/types/tests/NodeTest.php index 7eb6411..6ed8a5d 100644 --- a/libs/types/tests/NodeTest.php +++ b/libs/types/tests/NodeTest.php @@ -6,11 +6,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use TypeLang\Type\Attribute\AttributeArgumentListNode; -use TypeLang\Type\Attribute\AttributeArgumentNode; -use TypeLang\Type\Attribute\AttributeGroupListNode; -use TypeLang\Type\Attribute\AttributeGroupNode; -use TypeLang\Type\Attribute\AttributeNode; use TypeLang\Type\Callable\CallableParameterListNode; use TypeLang\Type\Callable\CallableParameterNode; use TypeLang\Type\CallableTypeNode; @@ -74,7 +69,7 @@ public static function provideNodes(): iterable ]; yield ConstMaskNode::class => [new ConstMaskNode(Name::createFromString('CONST'))]; yield Identifier::class => [new Identifier('Example')]; - yield IntersectionTypeNode::class => [new IntersectionTypeNode(self::type('A'), self::type('B'))]; + yield IntersectionTypeNode::class => [new IntersectionTypeNode([self::type('A'), self::type('B')])]; yield Name::class => [Name::createFromString('Example')]; yield NamedTypeNode::class => [self::type()]; yield NullableTypeNode::class => [new NullableTypeNode(self::type())]; @@ -87,13 +82,7 @@ public static function provideNodes(): iterable ]; yield TypeOffsetAccessNode::class => [new TypeOffsetAccessNode(self::type(), self::type('Offset'))]; yield TypesListNode::class => [new TypesListNode(self::type())]; - yield UnionTypeNode::class => [new UnionTypeNode(self::type('A'), self::type('B'))]; - - yield AttributeArgumentListNode::class => [new AttributeArgumentListNode()]; - yield AttributeArgumentNode::class => [new AttributeArgumentNode(self::type())]; - yield AttributeGroupListNode::class => [new AttributeGroupListNode()]; - yield AttributeGroupNode::class => [new AttributeGroupNode()]; - yield AttributeNode::class => [new AttributeNode(Name::createFromString('Deprecated'))]; + yield UnionTypeNode::class => [new UnionTypeNode([self::type('A'), self::type('B')])]; yield CallableParameterListNode::class => [new CallableParameterListNode()]; yield CallableParameterNode::class => [new CallableParameterNode(self::type())]; @@ -264,4 +253,148 @@ public function abstractNodeOffsetIsReadableByMethod(): void self::assertSame(0, $node->getOffset()); } + + /** + * Every node accepts the source code offset it has been read at as the + * last argument of its constructor. + * + * @return iterable + */ + public static function provideNodesWithOffset(): iterable + { + yield CallableTypeNode::class => [ + new CallableTypeNode(Name::createFromString('callable'), offset: 42), + ]; + yield ClassConstMaskNode::class => [ + new ClassConstMaskNode(Name::createFromString('Example'), offset: 42), + ]; + yield ClassConstNode::class => [ + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST'), 42), + ]; + yield ConstMaskNode::class => [new ConstMaskNode(Name::createFromString('CONST'), 42)]; + yield Identifier::class => [new Identifier('Example', 42)]; + yield Name::class => [Name::createFromString('Example', 42)]; + yield NamedTypeNode::class => [new NamedTypeNode(Name::createFromString('Example'), offset: 42)]; + yield NullableTypeNode::class => [new NullableTypeNode(self::type(), 42)]; + yield TernaryExpressionNode::class => [ + new TernaryExpressionNode( + new EqualConditionNode(self::type('A'), self::type('B')), + self::type('C'), + self::type('D'), + 42, + ), + ]; + yield TypeOffsetAccessNode::class => [ + new TypeOffsetAccessNode(self::type(), self::type('Offset'), 42), + ]; + yield TypesListNode::class => [new TypesListNode(self::type(), 42)]; + + yield CallableParameterListNode::class => [new CallableParameterListNode([], 42)]; + yield CallableParameterNode::class => [new CallableParameterNode(self::type(), offset: 42)]; + + yield EqualConditionNode::class => [new EqualConditionNode(self::type('A'), self::type('B'), 42)]; + yield GreaterThanConditionNode::class => [ + new GreaterThanConditionNode(self::type('A'), self::type('B'), 42), + ]; + yield GreaterThanOrEqualConditionNode::class => [ + new GreaterThanOrEqualConditionNode(self::type('A'), self::type('B'), 42), + ]; + yield LessThanConditionNode::class => [ + new LessThanConditionNode(self::type('A'), self::type('B'), 42), + ]; + yield LessThanOrEqualConditionNode::class => [ + new LessThanOrEqualConditionNode(self::type('A'), self::type('B'), 42), + ]; + yield NotEqualConditionNode::class => [ + new NotEqualConditionNode(self::type('A'), self::type('B'), 42), + ]; + + yield BoolLiteralNode::class => [new BoolLiteralNode(true, offset: 42)]; + yield FloatLiteralNode::class => [new FloatLiteralNode(0.1, offset: 42)]; + yield IntLiteralNode::class => [new IntLiteralNode(42, offset: 42)]; + yield NullLiteralNode::class => [new NullLiteralNode(offset: 42)]; + yield StringLiteralNode::class => [new StringLiteralNode('example', offset: 42)]; + yield VariableLiteralNode::class => [new VariableLiteralNode('example', 42)]; + + yield ClassConstFieldNode::class => [ + new ClassConstFieldNode( + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), + self::type(), + offset: 42, + ), + ]; + yield ClassConstMaskFieldNode::class => [ + new ClassConstMaskFieldNode( + new ClassConstMaskNode(Name::createFromString('Example')), + self::type(), + offset: 42, + ), + ]; + yield ConstMaskFieldNode::class => [ + new ConstMaskFieldNode( + new ConstMaskNode(Name::createFromString('CONST')), + self::type(), + offset: 42, + ), + ]; + yield FieldsListNode::class => [new FieldsListNode(offset: 42)]; + yield ImplicitFieldNode::class => [new ImplicitFieldNode(self::type(), offset: 42)]; + yield NamedFieldNode::class => [ + new NamedFieldNode(new Identifier('key'), self::type(), offset: 42), + ]; + yield NumericFieldNode::class => [ + new NumericFieldNode(new IntLiteralNode(0), self::type(), offset: 42), + ]; + yield StringNamedFieldNode::class => [ + new StringNamedFieldNode(new StringLiteralNode('key'), self::type(), offset: 42), + ]; + + yield TemplateArgumentListNode::class => [new TemplateArgumentListNode([], 42)]; + yield TemplateArgumentNode::class => [new TemplateArgumentNode(self::type(), offset: 42)]; + + yield UnionTypeNode::class => [new UnionTypeNode([self::type('A'), self::type('B')], 42)]; + yield IntersectionTypeNode::class => [ + new IntersectionTypeNode([self::type('A'), self::type('B')], 42), + ]; + } + + #[Test] + #[DataProvider('provideNodesWithOffset')] + public function everyNodeConstructorPassesTheOffsetThrough(Node $node): void + { + self::assertSame(42, $node->offset); + self::assertSame(42, $node->getOffset()); + } + + /** + * Guards the provider above from getting out of sync with the package: + * every node takes the offset as the last argument of its constructor and + * must be listed there. + */ + #[Test] + public function everyNodeIsCoveredByTheOffsetProvider(): void + { + $expected = $actual = []; + + foreach (self::provideNodes() as [$node]) { + $parameters = (new \ReflectionObject($node)) + ->getConstructor()?->getParameters() ?? []; + + $last = \end($parameters); + + self::assertNotFalse($last, $node::class . ' must take an offset'); + self::assertSame('offset', $last->getName(), $node::class . ' must take an offset last'); + + $expected[] = $node::class; + } + + foreach (self::provideNodesWithOffset() as [$node]) { + $actual[] = $node::class; + } + + \sort($expected); + \sort($actual); + + self::assertSame($expected, $actual); + } } diff --git a/libs/types/tests/Shape/ExplicitFieldNodeTest.php b/libs/types/tests/Shape/ExplicitFieldNodeTest.php index 83bb168..140bd4d 100644 --- a/libs/types/tests/Shape/ExplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ExplicitFieldNodeTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\ClassConstMaskNode; use TypeLang\Type\ClassConstNode; use TypeLang\Type\ConstMaskNode; @@ -105,11 +104,6 @@ public function explicitFieldIsRequiredByDefault(ExplicitFieldNode $field, strin #[Test] #[DataProvider('provideExplicitFields')] - public function explicitFieldHasNoAttributesByDefault(ExplicitFieldNode $field, string $index): void - { - self::assertNull($field->attributes); - } - #[Test] #[DataProvider('provideExplicitFields')] public function explicitFieldIsAFieldNode(ExplicitFieldNode $field, string $index): void @@ -127,14 +121,6 @@ public function optionalFieldStoresTheFlag(): void } #[Test] - public function attributesAreStored(): void - { - $attributes = new AttributeGroupListNode(); - $field = new NamedFieldNode(new Identifier('key'), self::type(), false, $attributes); - - self::assertSame($attributes, $field->attributes); - } - #[Test] public function implicitFieldHasNoKey(): void { diff --git a/libs/types/tests/Shape/ImplicitFieldNodeTest.php b/libs/types/tests/Shape/ImplicitFieldNodeTest.php index f6d7193..0ce2842 100644 --- a/libs/types/tests/Shape/ImplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ImplicitFieldNodeTest.php @@ -41,13 +41,6 @@ public function constructorStoresOptionalFlag(): void } #[Test] - public function attributesDefaultToNull(): void - { - $node = new ImplicitFieldNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertNull($node->attributes); - } - #[Test] public function defaultOffsetIsZero(): void { diff --git a/libs/types/tests/Template/TemplateArgumentNodeTest.php b/libs/types/tests/Template/TemplateArgumentNodeTest.php index 8b6265e..8b2ed29 100644 --- a/libs/types/tests/Template/TemplateArgumentNodeTest.php +++ b/libs/types/tests/Template/TemplateArgumentNodeTest.php @@ -31,13 +31,6 @@ public function hintDefaultsToNull(): void } #[Test] - public function attributesDefaultToNull(): void - { - $node = new TemplateArgumentNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertNull($node->attributes); - } - #[Test] public function constructorAcceptsIdentifierHint(): void { diff --git a/libs/types/tests/UnionTypeNodeTest.php b/libs/types/tests/UnionTypeNodeTest.php index 0c4d7ed..7f0f88a 100644 --- a/libs/types/tests/UnionTypeNodeTest.php +++ b/libs/types/tests/UnionTypeNodeTest.php @@ -22,7 +22,7 @@ public function constructorWithTwoTypes(): void { $a = $this->named('int'); $b = $this->named('string'); - $node = new UnionTypeNode($a, $b); + $node = new UnionTypeNode([$a, $b]); self::assertCount(2, $node); self::assertSame([$a, $b], $node->statements); @@ -34,7 +34,7 @@ public function constructorWithThreeTypes(): void $a = $this->named('int'); $b = $this->named('string'); $c = $this->named('null'); - $node = new UnionTypeNode($a, $b, $c); + $node = new UnionTypeNode([$a, $b, $c]); self::assertCount(3, $node); } @@ -46,8 +46,8 @@ public function nestedUnionIsFlattened(): void $b = $this->named('string'); $c = $this->named('null'); - $inner = new UnionTypeNode($a, $b); - $outer = new UnionTypeNode($inner, $c); + $inner = new UnionTypeNode([$a, $b]); + $outer = new UnionTypeNode([$inner, $c]); self::assertCount(3, $outer); self::assertSame([$a, $b, $c], $outer->statements); @@ -58,7 +58,7 @@ public function iteratorYieldsStatements(): void { $a = $this->named('int'); $b = $this->named('string'); - $node = new UnionTypeNode($a, $b); + $node = new UnionTypeNode([$a, $b]); self::assertSame([$a, $b], \iterator_to_array($node)); } @@ -66,7 +66,7 @@ public function iteratorYieldsStatements(): void #[Test] public function countReturnsNumberOfStatements(): void { - $node = new UnionTypeNode($this->named('int'), $this->named('string'), $this->named('bool')); + $node = new UnionTypeNode([$this->named('int'), $this->named('string'), $this->named('bool')]); self::assertSame(3, $node->count()); } @@ -78,8 +78,8 @@ public function intersectionTypeNodeIsNotFlattenedInsideUnion(): void $b = $this->named('B'); $c = $this->named('C'); - $inner = new IntersectionTypeNode($a, $b); - $outer = new UnionTypeNode($inner, $c); + $inner = new IntersectionTypeNode([$a, $b]); + $outer = new UnionTypeNode([$inner, $c]); self::assertCount(2, $outer); self::assertSame($inner, $outer->statements[0]); @@ -90,7 +90,7 @@ public function serializeAndUnserializeRoundtrip(): void { $a = $this->named('int'); $b = $this->named('string'); - $node = new UnionTypeNode($a, $b); + $node = new UnionTypeNode([$a, $b]); $node->offset = 5; /** @var UnionTypeNode $restored */ From 5409bb69a711c241af712e31ef89713d19756611 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 00:47:53 +0300 Subject: [PATCH 029/119] Fix UnionTypeNode and IntersectionTypeNode in reader --- libs/reader/src/ReflectionReader.php | 4 +-- libs/reader/tests/ConstantReaderTest.php | 24 ++++++++-------- libs/reader/tests/FunctionReaderTest.php | 24 ++++++++-------- libs/reader/tests/MethodReaderTest.php | 24 ++++++++-------- libs/reader/tests/ParameterReaderTest.php | 24 ++++++++-------- libs/reader/tests/PropertyReaderTest.php | 30 ++++++++++---------- libs/reader/tests/StaticMethodReaderTest.php | 24 ++++++++-------- 7 files changed, 77 insertions(+), 77 deletions(-) diff --git a/libs/reader/src/ReflectionReader.php b/libs/reader/src/ReflectionReader.php index e0fe256..695a510 100644 --- a/libs/reader/src/ReflectionReader.php +++ b/libs/reader/src/ReflectionReader.php @@ -179,7 +179,7 @@ private function convertUnionType(\ReflectionUnionType $type): UnionTypeNode $children[] = $this->getType($child); } - return new UnionTypeNode(...$children); + return new UnionTypeNode($children); } /** @@ -194,6 +194,6 @@ private function convertIntersectionType(\ReflectionIntersectionType $type): Int $children[] = $this->getType($child); } - return new IntersectionTypeNode(...$children); + return new IntersectionTypeNode($children); } } diff --git a/libs/reader/tests/ConstantReaderTest.php b/libs/reader/tests/ConstantReaderTest.php index abe8843..f61e287 100644 --- a/libs/reader/tests/ConstantReaderTest.php +++ b/libs/reader/tests/ConstantReaderTest.php @@ -33,10 +33,10 @@ public function testUnionType(ConstantReaderInterface $reader): void constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'UNION'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -46,10 +46,10 @@ public function testIntersectionType(ConstantReaderInterface $reader): void constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'INTERSECTION'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(__ConstantReaderEnum::class), - self::classType(\BackedEnum::class), - ), $type); + self::classType(\BackedEnum::class) + ]), $type); } #[DataProvider('readersDataProvider')] @@ -59,13 +59,13 @@ public function testCompositeType(ConstantReaderInterface $reader): void constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'COMPOSITE'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(__ConstantReaderEnum::class), - self::classType(\BackedEnum::class), - ), - self::builtin('array'), - ), $type); + self::classType(\BackedEnum::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] diff --git a/libs/reader/tests/FunctionReaderTest.php b/libs/reader/tests/FunctionReaderTest.php index 0790311..5803705 100644 --- a/libs/reader/tests/FunctionReaderTest.php +++ b/libs/reader/tests/FunctionReaderTest.php @@ -36,10 +36,10 @@ public function testUnionType(FunctionReaderInterface $reader): void function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_union_type'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -49,10 +49,10 @@ public function testIntersectionType(FunctionReaderInterface $reader): void function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_intersection_type'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] @@ -62,13 +62,13 @@ public function testCompositeType(FunctionReaderInterface $reader): void function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_composite_type'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] diff --git a/libs/reader/tests/MethodReaderTest.php b/libs/reader/tests/MethodReaderTest.php index d975729..e225a4c 100644 --- a/libs/reader/tests/MethodReaderTest.php +++ b/libs/reader/tests/MethodReaderTest.php @@ -31,10 +31,10 @@ public function testUnionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(MethodReaderStub::class, 'getUnionType'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -44,10 +44,10 @@ public function testIntersectionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(MethodReaderStub::class, 'getIntersectionType'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] @@ -57,12 +57,12 @@ public function testCompositeType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(MethodReaderStub::class, 'getCompositeType'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } } diff --git a/libs/reader/tests/ParameterReaderTest.php b/libs/reader/tests/ParameterReaderTest.php index 49cbc2e..2a14eca 100644 --- a/libs/reader/tests/ParameterReaderTest.php +++ b/libs/reader/tests/ParameterReaderTest.php @@ -32,10 +32,10 @@ public function testUnionType(ParameterReaderInterface $reader): void parameter: new \ReflectionParameter(ParameterReaderStub::withUnionType(...), 0), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -45,10 +45,10 @@ public function testIntersectionType(ParameterReaderInterface $reader): void parameter: new \ReflectionParameter(ParameterReaderStub::withIntersectionType(...), 0), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] @@ -58,13 +58,13 @@ public function testCompositeType(ParameterReaderInterface $reader): void parameter: new \ReflectionParameter(ParameterReaderStub::withCompositeType(...), 0), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] diff --git a/libs/reader/tests/PropertyReaderTest.php b/libs/reader/tests/PropertyReaderTest.php index 2fc0ae7..aab7d7e 100644 --- a/libs/reader/tests/PropertyReaderTest.php +++ b/libs/reader/tests/PropertyReaderTest.php @@ -33,10 +33,10 @@ public function testUnionType(PropertyReaderInterface $reader): void property: new \ReflectionProperty(PropertyReaderStub::class, 'unionType'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -46,10 +46,10 @@ public function testIntersectionType(PropertyReaderInterface $reader): void property: new \ReflectionProperty(PropertyReaderStub::class, 'intersectionType'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] @@ -59,13 +59,13 @@ public function testCompositeType(PropertyReaderInterface $reader): void property: new \ReflectionProperty(PropertyReaderStub::class, 'compositeType'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -108,10 +108,10 @@ public function testHookedPropertyWriteType(PropertyReaderInterface $reader): vo ); // The "set" hook widens the accepted type to "string|\Stringable". - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::classType(\Stringable::class), - self::builtin('string'), - ), $type); + self::builtin('string') + ]), $type); } #[DataProvider('readersDataProvider')] diff --git a/libs/reader/tests/StaticMethodReaderTest.php b/libs/reader/tests/StaticMethodReaderTest.php index 036841d..dab0f90 100644 --- a/libs/reader/tests/StaticMethodReaderTest.php +++ b/libs/reader/tests/StaticMethodReaderTest.php @@ -31,10 +31,10 @@ public function testUnionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getUnionType'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -44,10 +44,10 @@ public function testIntersectionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getIntersectionType'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] @@ -57,12 +57,12 @@ public function testCompositeType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getCompositeType'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } } From 1ca61100d2834332eb5ccf329e5e4229e4965568 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 00:48:13 +0300 Subject: [PATCH 030/119] Fix LogicalTypeNode and remove attributes from printer component --- libs/printer/src/NativeTypePrinter.php | 2 +- libs/printer/src/PrettyTypePrinter.php | 52 +------------------- libs/printer/tests/PrettyTypePrinterTest.php | 32 ------------ 3 files changed, 3 insertions(+), 83 deletions(-) diff --git a/libs/printer/src/NativeTypePrinter.php b/libs/printer/src/NativeTypePrinter.php index 5196520..724a641 100644 --- a/libs/printer/src/NativeTypePrinter.php +++ b/libs/printer/src/NativeTypePrinter.php @@ -106,7 +106,7 @@ protected function printTypeListNode(TypesListNode $node): string protected function printTernaryType(TernaryExpressionNode $node): string { /** @var non-empty-string */ - return $this->make(new UnionTypeNode($node->then, $node->else)); + return $this->make(new UnionTypeNode([$node->then, $node->else])); } #[\Override] diff --git a/libs/printer/src/PrettyTypePrinter.php b/libs/printer/src/PrettyTypePrinter.php index 0980bfa..1f5288c 100644 --- a/libs/printer/src/PrettyTypePrinter.php +++ b/libs/printer/src/PrettyTypePrinter.php @@ -5,8 +5,6 @@ namespace TypeLang\Printer; use TypeLang\Printer\Exception\NonPrintableNodeException; -use TypeLang\Type\Attribute\AttributeGroupListNode; -use TypeLang\Type\Attribute\AttributeGroupNode; use TypeLang\Type\Callable\CallableParameterNode; use TypeLang\Type\CallableTypeNode; use TypeLang\Type\ClassConstMaskNode; @@ -194,13 +192,7 @@ private function getShapeFieldsNodes(NamedTypeNode $node, FieldsListNode $shape, $fields = []; foreach ($shape->items as $field) { - $current = ''; - - if ($field->attributes !== null) { - $current .= $this->printAttributeGroups($field->attributes, $multiline); - } - - $fields[] = $current . $prefix . $this->printShapeFieldNode($field); + $fields[] = $prefix . $this->printShapeFieldNode($field); } if (!$shape->isSealed || $node->arguments !== null) { @@ -217,35 +209,6 @@ private function getShapeFieldsNodes(NamedTypeNode $node, FieldsListNode $shape, return $fields; } - protected function printAttributeGroups(AttributeGroupListNode $groups, bool $multiline): string - { - $prefix = $this->prefix(); - $result = ''; - - foreach ($groups as $group) { - $result .= $prefix . $this->printAttributeGroup($group); - $result .= $multiline ? $this->newLine : ' '; - } - - return $result; - } - - protected function printAttributeGroup(AttributeGroupNode $group): string - { - $result = '#['; - - $last = $group->last(); - foreach ($group as $attribute) { - $result .= $attribute->name->toString(); - - if ($attribute !== $last) { - $result .= ', '; - } - } - - return $result . ']'; - } - /** * @return non-empty-string * @throws NonPrintableNodeException @@ -333,13 +296,7 @@ protected function printTemplateArgumentsNode(TemplateArgumentListNode $argument $result = []; foreach ($arguments as $argument) { - $current = ''; - - if ($argument->attributes !== null) { - $current .= $this->printAttributeGroups($argument->attributes, false); - } - - $result[] = $current . $this->printTemplateArgumentNode($argument); + $result[] = $this->printTemplateArgumentNode($argument); } return \sprintf('<%s>', \implode(', ', $result)); @@ -436,11 +393,6 @@ protected function printCallableArgumentNode(CallableParameterNode $node): strin $result = $this->make($node->type); } - if ($node->attributes !== null) { - $result = $this->printAttributeGroups($node->attributes, false) - . $result; - } - if ($node->name !== null) { $result .= ' '; } diff --git a/libs/printer/tests/PrettyTypePrinterTest.php b/libs/printer/tests/PrettyTypePrinterTest.php index 54412d4..4861e28 100644 --- a/libs/printer/tests/PrettyTypePrinterTest.php +++ b/libs/printer/tests/PrettyTypePrinterTest.php @@ -113,12 +113,6 @@ public function testPrintTemplateArgumentHint(): void ->print(self::parse('array'))); } - public function testPrintTemplateArgumentAttribute(): void - { - self::assertSame('list<#[Foo] int>', $this->printer() - ->print(self::parse('list<#[Foo] int>'))); - } - public function testPrintInlineShape(): void { self::assertSame('array{foo: int}', $this->printer() @@ -206,20 +200,6 @@ public function testPrintConstantMaskShapeKey(): void ->print(self::parse('array{FOO_*: int}'))); } - public function testPrintShapeFieldAttribute(): void - { - self::assertSame('array{#[Foo] foo: int}', $this->printer() - ->print(self::parse('array{#[Foo] foo: int}'))); - } - - public function testPrintShapeFieldAttributeMultiline(): void - { - self::assertSame( - "array{\n #[Foo]\n foo: int,\n bar: string\n}", - $this->printer()->print(self::parse('array{#[Foo] foo: int, bar: string}')), - ); - } - public function testPrintCallableType(): void { self::assertSame('callable(int, string): void', $this->printer() @@ -256,18 +236,6 @@ public function testPrintOptionalCallableParameter(): void ->print(self::parse('callable(int=): void'))); } - public function testPrintCallableParameterAttribute(): void - { - self::assertSame('callable(#[Foo] int): void', $this->printer() - ->print(self::parse('callable(#[Foo] int): void'))); - } - - public function testPrintCallableParameterMultipleAttributes(): void - { - self::assertSame('callable(#[Foo, Bar] int): void', $this->printer() - ->print(self::parse('callable(#[Foo, Bar] int): void'))); - } - public function testPrintClassConstant(): void { self::assertSame('Foo::BAR', $this->printer()->print(self::parse('Foo::BAR'))); From b850ec02694865a0d91153801ff91a5c2321e24d Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 00:48:59 +0300 Subject: [PATCH 031/119] Apply phpcs --- libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php | 4 +--- libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php | 4 +--- libs/phpdoc/src/DocBlock/Tag/TagInterface.php | 4 +--- libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php | 4 +--- libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php | 4 +--- libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php | 4 +--- .../src/DocBlock/TagDefinition/TagDefinitionInterface.php | 1 - libs/phpdoc/src/DocBlockParser.php | 2 +- libs/phpdoc/src/Exception/ParsingExceptionInterface.php | 4 +--- libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php | 4 +--- libs/types/src/Name.php | 7 +++---- 11 files changed, 12 insertions(+), 30 deletions(-) diff --git a/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php b/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php index a8069c4..68f1853 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php +++ b/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php @@ -13,6 +13,4 @@ * @property-read bool $isExternal Gets {@see true} in case of the reference points outside of the described * codebase, such as an external web page. */ -interface ReferenceInterface extends \Stringable -{ -} +interface ReferenceInterface extends \Stringable {} diff --git a/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php index 39c53ad..0f7b5ce 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php @@ -13,6 +13,4 @@ * * @property-read ReferenceInterface $reference The reference the tag points to. */ -interface ReferencedTagInterface extends TagInterface -{ -} +interface ReferencedTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/TagInterface.php b/libs/phpdoc/src/DocBlock/Tag/TagInterface.php index f88cf09..c404f3e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/TagInterface.php @@ -14,6 +14,4 @@ * @property-read ?DescriptionInterface $description Gets an optional description object or {@see null} in case of * description is not defined in the entry. */ -interface TagInterface extends ComponentInterface -{ -} +interface TagInterface extends ComponentInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php index 923aa1b..9533afa 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php @@ -11,6 +11,4 @@ * * @property-read TypeNode $type The type declared by the tag. */ -interface TypedTagInterface extends TagInterface -{ -} +interface TypedTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php index ec9de9b..c45bca0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php @@ -9,6 +9,4 @@ * * @property-read non-empty-string $variable The referenced variable name, without the leading "$". */ -interface VariableTagInterface extends TagInterface -{ -} +interface VariableTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php index ca1bfee..76bd9b3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php @@ -9,6 +9,4 @@ * * @property-read ?non-empty-string $version The version the tag refers to, if any. */ -interface VersionedTagInterface extends TagInterface -{ -} +interface VersionedTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php b/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php index 6eef03c..760d165 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php @@ -19,7 +19,6 @@ */ interface TagDefinitionInterface extends \Stringable { - /** * Builds the tag from the values captured while matching {@see $spec}. * diff --git a/libs/phpdoc/src/DocBlockParser.php b/libs/phpdoc/src/DocBlockParser.php index f0747d9..3c6b193 100644 --- a/libs/phpdoc/src/DocBlockParser.php +++ b/libs/phpdoc/src/DocBlockParser.php @@ -124,7 +124,7 @@ private function createTagRegistry(array $platforms): TagRegistryInterface } } - return (new TagRegistryBuilder($definitions, $aliases)) + return new TagRegistryBuilder($definitions, $aliases) ->build(); } diff --git a/libs/phpdoc/src/Exception/ParsingExceptionInterface.php b/libs/phpdoc/src/Exception/ParsingExceptionInterface.php index 5057d71..e1313b7 100644 --- a/libs/phpdoc/src/Exception/ParsingExceptionInterface.php +++ b/libs/phpdoc/src/Exception/ParsingExceptionInterface.php @@ -10,6 +10,4 @@ * @property-read string $source Gets the full docblock content in which the error occurred. * @property-read int<0, max> $offset Gets the offset at which the error occurred. */ -interface ParsingExceptionInterface extends PhpDocExceptionInterface -{ -} +interface ParsingExceptionInterface extends PhpDocExceptionInterface {} diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php b/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php index d7052a7..89a3b47 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php @@ -9,6 +9,4 @@ * * @property-read ?non-empty-string $alias The name the matched value is captured under, if any. */ -interface TerminalInterface extends RuleInterface -{ -} +interface TerminalInterface extends RuleInterface {} diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index eacafad..01cb6a3 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -167,14 +167,13 @@ public function isBuiltin(): bool /** * Gets {@see true} in case of name is fully qualified. * - * @deprecated Use the {@see $isFullyQualified} property instead. + * @deprecated use the {@see $isFullyQualified} property instead */ public function isFullQualified(): bool { return $this->isFullyQualified; } - /** * @param int<0, max> $offset * @param int<0, max>|null $length @@ -284,7 +283,7 @@ public function toArray(): array } /** - * @deprecated Use the {@see $parts} property instead. + * @deprecated use the {@see $parts} property instead * * @return non-empty-list */ @@ -308,7 +307,7 @@ public function toArrayStrings(): array } /** - * @deprecated Use the {@see toArrayStrings()} method instead. + * @deprecated use the {@see toArrayStrings()} method instead * * @return non-empty-list */ From bea0e06774bb5e01ebf162e6c7d21806982454f1 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 00:49:21 +0300 Subject: [PATCH 032/119] Upgrade phplrt version --- .gitattributes | 1 + composer.json | 2 +- libs/parser/.gitattributes | 1 + libs/parser/bin/build | 35 +- libs/parser/composer.json | 8 +- libs/parser/phpstan.neon | 5 + libs/parser/resources/grammar.php | 805 ------ libs/parser/resources/grammar.pp2 | 173 -- libs/parser/resources/grammar.pp3 | 221 ++ libs/parser/resources/grammar/attribute.pp2 | 44 - libs/parser/resources/grammar/callable.pp2 | 189 -- libs/parser/resources/grammar/callable.pp3 | 75 + libs/parser/resources/grammar/common.pp2 | 33 - libs/parser/resources/grammar/common.pp3 | 40 + libs/parser/resources/grammar/lexemes.pp2 | 53 - libs/parser/resources/grammar/lexemes.pp3 | 85 + libs/parser/resources/grammar/literals.pp2 | 111 - libs/parser/resources/grammar/literals.pp3 | 72 + libs/parser/resources/grammar/named-type.pp2 | 22 - libs/parser/resources/grammar/named-type.pp3 | 88 + .../parser/resources/grammar/shape-fields.pp2 | 128 - .../parser/resources/grammar/shape-fields.pp3 | 169 ++ ...e-arguments.pp2 => template-arguments.pp3} | 30 +- libs/parser/resources/grammar/ternary.pp2 | 71 - libs/parser/resources/grammar/ternary.pp3 | 14 + libs/parser/src/Exception/Formatter.php | 24 +- .../src/Exception/InternalParseException.php | 2 +- .../src/Exception/SemanticException.php | 2 - .../src/Exception/SemanticParseException.php | 6 +- .../Exception/UnexpectedTokenException.php | 2 +- .../Exception/UnrecognizedSyntaxException.php | 2 +- .../Exception/UnrecognizedTokenException.php | 2 +- .../VariadicRedefinitionException.php | 20 - libs/parser/src/InMemoryTypeParser.php | 29 +- libs/parser/src/Internal/CompilerExecutor.php | 2283 +++++++++++++++++ libs/parser/src/Internal/ExecutionContext.php | 168 -- libs/parser/src/Internal/Executor.php | 163 ++ libs/parser/src/Internal/NodeBuilder.php | 38 - .../{StringParser.php => StringDecoder.php} | 13 +- .../PatternSequenceFetcher.php | 2 +- .../ScannerSequenceFetcher.php | 4 +- .../SequenceFetcherInterface.php | 4 +- .../{ => StringDecoder}/UtfCharRenderer.php | 6 +- libs/parser/src/Literal/IntLiteralParser.php | 63 - .../src/Literal/StringLiteralParser.php | 64 - libs/parser/src/Traverser/DumperVisitor.php | 3 +- libs/parser/src/TypeParser.php | 157 +- libs/parser/src/TypeParserFeatures.php | 5 - libs/parser/src/TypeResolver.php | 4 +- .../tests/Exception/ParseExceptionTest.php | 8 +- .../tests/Exception/SemanticExceptionTest.php | 19 - .../tests/Internal/SequenceFetcherTest.php | 12 +- .../tests/Internal/StringParserTest.php | 14 +- .../tests/Internal/UtfCharRendererTest.php | 2 +- ...teralParserTest.php => IntLiteralTest.php} | 70 +- ...alParserTest.php => StringLiteralTest.php} | 87 +- libs/parser/tests/Syntax/AttributeTest.php | 122 - libs/parser/tests/Syntax/CallableTest.php | 81 +- libs/parser/tests/Syntax/ConditionalTest.php | 2 +- .../parser/tests/Syntax/FeatureToggleTest.php | 1 - libs/parser/tests/Syntax/FloatLiteralTest.php | 2 +- libs/parser/tests/Syntax/NameTest.php | 2 +- libs/parser/tests/TestCase.php | 1 - .../tests/Traverser/MatcherVisitorTest.php | 4 +- .../Traverser/StringDumperVisitorTest.php | 8 +- libs/parser/tests/Traverser/TraverserTest.php | 2 +- .../tests/Traverser/TypeMapVisitorTest.php | 6 +- libs/parser/tests/TypeParserFeaturesTest.php | 6 - libs/parser/tests/TypeParserTest.php | 4 +- 69 files changed, 3596 insertions(+), 2398 deletions(-) delete mode 100644 libs/parser/resources/grammar.php delete mode 100644 libs/parser/resources/grammar.pp2 create mode 100644 libs/parser/resources/grammar.pp3 delete mode 100644 libs/parser/resources/grammar/attribute.pp2 delete mode 100644 libs/parser/resources/grammar/callable.pp2 create mode 100644 libs/parser/resources/grammar/callable.pp3 delete mode 100644 libs/parser/resources/grammar/common.pp2 create mode 100644 libs/parser/resources/grammar/common.pp3 delete mode 100644 libs/parser/resources/grammar/lexemes.pp2 create mode 100644 libs/parser/resources/grammar/lexemes.pp3 delete mode 100644 libs/parser/resources/grammar/literals.pp2 create mode 100644 libs/parser/resources/grammar/literals.pp3 delete mode 100644 libs/parser/resources/grammar/named-type.pp2 create mode 100644 libs/parser/resources/grammar/named-type.pp3 delete mode 100644 libs/parser/resources/grammar/shape-fields.pp2 create mode 100644 libs/parser/resources/grammar/shape-fields.pp3 rename libs/parser/resources/grammar/{template-arguments.pp2 => template-arguments.pp3} (52%) delete mode 100644 libs/parser/resources/grammar/ternary.pp2 create mode 100644 libs/parser/resources/grammar/ternary.pp3 delete mode 100644 libs/parser/src/Exception/VariadicRedefinitionException.php create mode 100644 libs/parser/src/Internal/CompilerExecutor.php delete mode 100644 libs/parser/src/Internal/ExecutionContext.php create mode 100644 libs/parser/src/Internal/Executor.php delete mode 100644 libs/parser/src/Internal/NodeBuilder.php rename libs/parser/src/Internal/{StringParser.php => StringDecoder.php} (79%) rename libs/parser/src/Internal/{ => StringDecoder}/PatternSequenceFetcher.php (97%) rename libs/parser/src/Internal/{ => StringDecoder}/ScannerSequenceFetcher.php (95%) rename libs/parser/src/Internal/{ => StringDecoder}/SequenceFetcherInterface.php (92%) rename libs/parser/src/Internal/{ => StringDecoder}/UtfCharRenderer.php (88%) delete mode 100644 libs/parser/src/Literal/IntLiteralParser.php delete mode 100644 libs/parser/src/Literal/StringLiteralParser.php rename libs/parser/tests/Literal/{IntLiteralParserTest.php => IntLiteralTest.php} (53%) rename libs/parser/tests/Literal/{StringLiteralParserTest.php => StringLiteralTest.php} (65%) delete mode 100644 libs/parser/tests/Syntax/AttributeTest.php diff --git a/.gitattributes b/.gitattributes index cfa4eea..11aad01 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ * text=auto eol=lf *.pp eol=lf linguist-language=EBNF *.pp2 eol=lf linguist-language=EBNF +*.pp3 eol=lf linguist-language=EBNF # GIT .editorconfig export-ignore diff --git a/composer.json b/composer.json index 4b94866..d75f0da 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "jetbrains/phpstorm-attributes": "^1.2", "phpbench/phpbench": "^1.2", "phpdocumentor/reflection-docblock": "^6.0", - "phplrt/compiler": "^3.7", + "phplrt/compiler": "^4.0", "phpstan/phpdoc-parser": "^2.3", "phpstan/phpstan": "^2.2", "phpstan/phpstan-deprecation-rules": "^2.0", diff --git a/libs/parser/.gitattributes b/libs/parser/.gitattributes index cfa4eea..11aad01 100644 --- a/libs/parser/.gitattributes +++ b/libs/parser/.gitattributes @@ -1,6 +1,7 @@ * text=auto eol=lf *.pp eol=lf linguist-language=EBNF *.pp2 eol=lf linguist-language=EBNF +*.pp3 eol=lf linguist-language=EBNF # GIT .editorconfig export-ignore diff --git a/libs/parser/bin/build b/libs/parser/bin/build index 9e453b2..d20ebdf 100644 --- a/libs/parser/bin/build +++ b/libs/parser/bin/build @@ -4,6 +4,9 @@ declare(strict_types=1); use Phplrt\Compiler\Compiler; +use Phplrt\Compiler\Generator\ClassModifier; +use Phplrt\Compiler\Generator\TargetPhpVersion; +use Phplrt\Source\FileSource; if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { trigger_error('The console should be invoked via the CLI ' @@ -31,21 +34,17 @@ if (!class_exists(Compiler::class)) { exit(-1); } -$grammar = (new Compiler()) - ->load(new SplFileInfo(__DIR__ . '/../resources/grammar.pp2')) - ->build() - ->withClassReference('TypeLang\\Type') - ->withClassReference('TypeLang\\Parser\\Exception') - ->withClassReference('TypeLang\\Parser\\Literal') - ->generate(); - -file_put_contents(__DIR__ . '/../resources/grammar.php', $grammar); - -// -// Postprocess and optimize output grammar -// - -$data = file_get_contents(__DIR__ . '/../resources/grammar.php'); - -// Replace functions to static one -file_put_contents(__DIR__ . '/../resources/grammar.php', $data); +(new Compiler()) + ->load(FileSource::createFromPathname(__DIR__ . '/../resources/grammar.pp3')) + ->generate() + ->withNamespaceName('TypeLang\\Parser\\Internal') + ->withClassName('CompilerExecutor') + ->withClassModifier(ClassModifier::Abstract) + // The parser carries the features it recognizes a source with, so it + // is declared as a mutable class rather than a readonly one. + ->withReadonly(false) + ->withTargetPhpVersion(TargetPhpVersion::Php81) + ->withClassImport('TypeLang\\Type') + ->withClassImport('TypeLang\\Parser\\Exception') + ->withClassImport('TypeLang\\Parser\\Literal') + ->save(__DIR__ . '/../src/Internal/CompilerExecutor.php'); diff --git a/libs/parser/composer.json b/libs/parser/composer.json index 769ba7f..6d77faf 100644 --- a/libs/parser/composer.json +++ b/libs/parser/composer.json @@ -11,9 +11,9 @@ "require": { "php": "^8.1", "type-lang/types": "^2.0", - "phplrt/lexer": "^3.7", - "phplrt/parser": "^3.7", - "phplrt/source": "^3.7" + "phplrt/lexer": "^4.0", + "phplrt/parser": "^4.0", + "phplrt/source": "^4.0" }, "autoload": { "psr-4": { @@ -22,7 +22,7 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", - "phplrt/compiler": "^3.7", + "phplrt/compiler": "^4.0", "phpstan/phpstan": "^2.2", "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, diff --git a/libs/parser/phpstan.neon b/libs/parser/phpstan.neon index c899209..5c59721 100644 --- a/libs/parser/phpstan.neon +++ b/libs/parser/phpstan.neon @@ -8,4 +8,9 @@ parameters: - php paths: - src + excludePaths: + analyse: + # The parser is generated out of the grammar by the phplrt compiler and + # is not written by hand. + - src/Internal/CompilerExecutor.php reportUnmatchedIgnoredErrors: false diff --git a/libs/parser/resources/grammar.php b/libs/parser/resources/grammar.php deleted file mode 100644 index bb9e0e6..0000000 --- a/libs/parser/resources/grammar.php +++ /dev/null @@ -1,805 +0,0 @@ -, - * ... - * }, - * skip: list, - * grammar: array, - * reducers: array, - * transitions?: array - * } - */ -return [ - 'initial' => 59, - 'tokens' => [ - 'default' => [ - 'T_DQ_STRING_LITERAL' => '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"', - 'T_SQ_STRING_LITERAL' => '\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'', - 'T_PFX_FLOAT_LITERAL' => '\\-?[0-9]++\\.[0-9]*+(?:[eE]-?[0-9]++)?', - 'T_SFX_FLOAT_LITERAL' => '\\-?[0-9]*+\\.[0-9]++(?:[eE]-?[0-9]++)?', - 'T_EXP_LITERAL' => '\\-?[0-9]++[eE]-?[0-9]++', - 'T_BIN_INT_LITERAL' => '\\-?0[bB][01_]++', - 'T_OCT_INT_LITERAL' => '\\-?0[oO][0-7_]++', - 'T_HEX_INT_LITERAL' => '\\-?0[xX][0-9a-fA-F_]++', - 'T_DEC_INT_LITERAL' => '\\-?[0-9][0-9_]*+', - 'T_BOOL_LITERAL' => '(?i)(?:true|false)(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_NULL_LITERAL' => '(?i)null(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_NEQ' => '(?i)is\\h++not(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_EQ' => '(?i)is(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_THIS' => '\\$this\\b', - 'T_VARIABLE' => '\\$[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+', - 'T_NAME_WITH_SPACE' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+\\s++', - 'T_NAME' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+', - 'T_LTE' => '<=', - 'T_GTE' => '>=', - 'T_ANGLE_BRACKET_OPEN' => '<', - 'T_ANGLE_BRACKET_CLOSE' => '>', - 'T_PARENTHESIS_OPEN' => '\\(', - 'T_PARENTHESIS_CLOSE' => '\\)', - 'T_BRACE_OPEN' => '\\{', - 'T_BRACE_CLOSE' => '\\}', - 'T_ATTR_OPEN' => '#\\[', - 'T_SQUARE_BRACKET_OPEN' => '\\[', - 'T_SQUARE_BRACKET_CLOSE' => '\\]', - 'T_COMMA' => ',', - 'T_ELLIPSIS' => '\\.\\.\\.', - 'T_DOUBLE_COLON' => '::', - 'T_COLON' => ':', - 'T_ASSIGN' => '=', - 'T_NS_DELIMITER' => '\\\\', - 'T_QMARK' => '\\?', - 'T_OR' => '\\|', - 'T_AMP' => '&', - 'T_ASTERISK' => '\\*', - 'T_COMMENT' => '(?://|#)[^\\r\\n]*+', - 'T_DOC_COMMENT' => '/\\*.*?\\*/', - 'T_WHITESPACE' => '\\s++', - ], - ], - 'skip' => [ - 'T_COMMENT', - 'T_DOC_COMMENT', - 'T_WHITESPACE', - ], - 'transitions' => [], - 'grammar' => [ - new Phplrt\Parser\Grammar\Concatenation([6, 3, 7]), - new Phplrt\Parser\Grammar\Concatenation([3, 10]), - new Phplrt\Parser\Grammar\Alternation([0, 1]), - new Phplrt\Parser\Grammar\Alternation([11, 12, 13, 14, 15]), - new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), - new Phplrt\Parser\Grammar\Concatenation([4, 3]), - new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), - new Phplrt\Parser\Grammar\Repetition(5, 0, INF), - new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), - new Phplrt\Parser\Grammar\Concatenation([8, 3]), - new Phplrt\Parser\Grammar\Repetition(9, 0, INF), - new Phplrt\Parser\Grammar\Lexeme('T_NAME', true), - new Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true), - new Phplrt\Parser\Grammar\Lexeme('T_EQ', true), - new Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true), - new Phplrt\Parser\Grammar\Alternation([21, 22, 23, 24, 25]), - new Phplrt\Parser\Grammar\Concatenation([2, 39]), - new Phplrt\Parser\Grammar\Concatenation([2, 43, 44]), - new Phplrt\Parser\Grammar\Alternation([17, 18, 19]), - new Phplrt\Parser\Grammar\Alternation([30, 31]), - new Phplrt\Parser\Grammar\Alternation([32, 33, 34]), - new Phplrt\Parser\Grammar\Alternation([35, 36, 37, 38]), - new Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_VARIABLE', true), - new Phplrt\Parser\Grammar\Lexeme('T_THIS', true), - new Phplrt\Parser\Grammar\Alternation([26, 27]), - new Phplrt\Parser\Grammar\Lexeme('T_THIS', true), - new Phplrt\Parser\Grammar\Lexeme('T_DQ_STRING_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_SQ_STRING_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_PFX_FLOAT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_SFX_FLOAT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_EXP_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_BIN_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_OCT_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_HEX_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_DEC_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', false), - new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true), - new Phplrt\Parser\Grammar\Concatenation([3, 40]), - new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true), - new Phplrt\Parser\Grammar\Lexeme('T_DOUBLE_COLON', false), - new Phplrt\Parser\Grammar\Alternation([41, 3, 42]), - new Phplrt\Parser\Grammar\Concatenation([57, 58]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([46, 45]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', false), - new Phplrt\Parser\Grammar\Repetition(47, 0, INF), - new Phplrt\Parser\Grammar\Optional(48), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([49, 45, 50, 51, 52]), - new Phplrt\Parser\Grammar\Repetition(155, 1, INF), - new Phplrt\Parser\Grammar\Concatenation([16, 59]), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Optional(54), - new Phplrt\Parser\Grammar\Alternation([55, 56]), - new Phplrt\Parser\Grammar\Concatenation([175]), - new Phplrt\Parser\Grammar\Concatenation([67, 71, 72]), - new Phplrt\Parser\Grammar\Concatenation([107, 59]), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), - new Phplrt\Parser\Grammar\Optional(60), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), - new Phplrt\Parser\Grammar\Optional(61), - new Phplrt\Parser\Grammar\Concatenation([2, 62, 63, 64, 65]), - new Phplrt\Parser\Grammar\Concatenation([74, 73]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([68, 67]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Repetition(69, 0, INF), - new Phplrt\Parser\Grammar\Optional(70), - new Phplrt\Parser\Grammar\Concatenation([78, 79]), - new Phplrt\Parser\Grammar\Optional(54), - new Phplrt\Parser\Grammar\Concatenation([93, 92]), - new Phplrt\Parser\Grammar\Alternation([83, 86, 88, 90, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ASSIGN', true), - new Phplrt\Parser\Grammar\Alternation([75, 76]), - new Phplrt\Parser\Grammar\Optional(77), - new Phplrt\Parser\Grammar\Concatenation([28]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Concatenation([81, 82, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Concatenation([84, 85, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Concatenation([87, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Concatenation([89, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Concatenation([94, 95]), - new Phplrt\Parser\Grammar\Optional(91), - new Phplrt\Parser\Grammar\Concatenation([96, 106]), - new Phplrt\Parser\Grammar\Optional(28), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Optional(97), - new Phplrt\Parser\Grammar\Concatenation([98, 99]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Optional(101), - new Phplrt\Parser\Grammar\Concatenation([102, 103]), - new Phplrt\Parser\Grammar\Alternation([100, 104]), - new Phplrt\Parser\Grammar\Optional(105), - new Phplrt\Parser\Grammar\Lexeme('T_COLON', false), - new Phplrt\Parser\Grammar\Concatenation([123, 126]), - new Phplrt\Parser\Grammar\Concatenation([121, 122]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([110, 109]), - new Phplrt\Parser\Grammar\Optional(111), - new Phplrt\Parser\Grammar\Concatenation([108, 112]), - new Phplrt\Parser\Grammar\Optional(109), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_BRACE_OPEN', false), - new Phplrt\Parser\Grammar\Alternation([113, 114]), - new Phplrt\Parser\Grammar\Optional(115), - new Phplrt\Parser\Grammar\Lexeme('T_BRACE_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([116, 117, 118, 119]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Optional(53), - new Phplrt\Parser\Grammar\Concatenation([129, 130]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([124, 123]), - new Phplrt\Parser\Grammar\Repetition(125, 0, INF), - new Phplrt\Parser\Grammar\Concatenation([131, 134, 135, 133]), - new Phplrt\Parser\Grammar\Concatenation([133]), - new Phplrt\Parser\Grammar\Optional(54), - new Phplrt\Parser\Grammar\Alternation([127, 128]), - new Phplrt\Parser\Grammar\Alternation([18, 19, 3, 23, 21]), - new Phplrt\Parser\Grammar\Lexeme('T_QMARK', true), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Optional(132), - new Phplrt\Parser\Grammar\Lexeme('T_COLON', false), - new Phplrt\Parser\Grammar\Alternation([53, 120]), - new Phplrt\Parser\Grammar\Optional(136), - new Phplrt\Parser\Grammar\Concatenation([2, 137]), - new Phplrt\Parser\Grammar\Concatenation([176]), - new Phplrt\Parser\Grammar\Optional(142), - new Phplrt\Parser\Grammar\Concatenation([139, 140]), - new Phplrt\Parser\Grammar\Concatenation([145, 146, 147, 59, 148, 59]), - new Phplrt\Parser\Grammar\Concatenation([28, 142]), - new Phplrt\Parser\Grammar\Alternation([141, 143]), - new Phplrt\Parser\Grammar\Alternation([149, 150, 151, 152, 153, 154]), - new Phplrt\Parser\Grammar\Alternation([59, 28]), - new Phplrt\Parser\Grammar\Lexeme('T_QMARK', false), - new Phplrt\Parser\Grammar\Lexeme('T_COLON', false), - new Phplrt\Parser\Grammar\Lexeme('T_EQ', true), - new Phplrt\Parser\Grammar\Lexeme('T_NEQ', true), - new Phplrt\Parser\Grammar\Lexeme('T_GTE', true), - new Phplrt\Parser\Grammar\Lexeme('T_LTE', true), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', true), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', true), - new Phplrt\Parser\Grammar\Concatenation([158, 156, 159, 160]), - new Phplrt\Parser\Grammar\Concatenation([161, 164]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_ATTR_OPEN', false), - new Phplrt\Parser\Grammar\Optional(157), - new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([2, 166]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([162, 161]), - new Phplrt\Parser\Grammar\Repetition(163, 0, INF), - new Phplrt\Parser\Grammar\Concatenation([171, 167, 172, 173, 174]), - new Phplrt\Parser\Grammar\Optional(165), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([168, 167]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), - new Phplrt\Parser\Grammar\Repetition(169, 0, INF), - new Phplrt\Parser\Grammar\Optional(170), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([144]), - new Phplrt\Parser\Grammar\Concatenation([177, 180]), - new Phplrt\Parser\Grammar\Concatenation([181, 184]), - new Phplrt\Parser\Grammar\Lexeme('T_OR', false), - new Phplrt\Parser\Grammar\Concatenation([178, 176]), - new Phplrt\Parser\Grammar\Optional(179), - new Phplrt\Parser\Grammar\Concatenation([185]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', false), - new Phplrt\Parser\Grammar\Concatenation([182, 177]), - new Phplrt\Parser\Grammar\Optional(183), - new Phplrt\Parser\Grammar\Alternation([188, 186]), - new Phplrt\Parser\Grammar\Concatenation([189, 191]), - new Phplrt\Parser\Grammar\Lexeme('T_QMARK', true), - new Phplrt\Parser\Grammar\Concatenation([187, 186]), - new Phplrt\Parser\Grammar\Alternation([197, 29, 20, 66, 138]), - new Phplrt\Parser\Grammar\Concatenation([192, 193, 194]), - new Phplrt\Parser\Grammar\Repetition(190, 0, INF), - new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_OPEN', true), - new Phplrt\Parser\Grammar\Optional(59), - new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([195, 59, 196]), - ], - 'reducers' => [ - 0 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Name($children, true); - }, - 1 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Name($children, false); - }, - 3 => static function (Phplrt\Parser\Context $ctx, $children) { - return Type\Identifier::createFromString($children->getValue()); - }, - 16 => static function (Phplrt\Parser\Context $ctx, $children) { - return Type\Identifier::createFromString($children->getValue()); - }, - 17 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if ($this->features->literals === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); - } - - return $children; - }, - 18 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\ConstMaskNode($children[0]); - }, - 19 => static function (Phplrt\Parser\Context $ctx, $children) { - // :: "*" - if (\count($children) === 3) { - return new Type\ClassConstMaskNode( - $children[0], - $children[1], - ); - } - - // :: - if ($children[1] instanceof Type\Identifier) { - return new Type\ClassConstNode( - $children[0], - $children[1], - ); - } - - // :: "*" - return new Type\ClassConstMaskNode($children[0]); - }, - 21 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return $this->stringPool[$token] ??= $children; - }, - 22 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return new Type\Literal\FloatLiteralNode((float) $token->getValue(), $token->getValue()); - }, - 23 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return $this->integerPool[$token] ??= Literal\IntLiteralParser::parse($token->getValue()); - }, - 24 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return new Type\Literal\BoolLiteralNode(\strtolower($token->getValue()) === 'true', $token->getValue()); - }, - 25 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Literal\NullLiteralNode($children->getValue()); - }, - 28 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); - }, - 29 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); - }, - 30 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Literal\StringLiteralParser::createFromDoubleQuotedString($token->getValue()); - }, - 31 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Literal\StringLiteralParser::createFromSingleQuotedString($token->getValue()); - }, - 45 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $hint = $attributes = null; - - if (\reset($children) instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument attributes', $offset); - } - - $attributes = \array_shift($children); - } - - $type = \array_pop($children); - - if (\reset($children) !== false) { - if ($this->features->hints === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); - } - - $hint = \reset($children); - } - - return new Type\Template\TemplateArgumentNode( - $type, - $hint, - $attributes, - ); - }, - 53 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if ($this->features->generics === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset); - } - - return new Type\Template\TemplateArgumentListNode($children); - }, - 54 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeGroupListNode($children); - }, - 60 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Callable\CallableParameterListNode($children); - }, - 66 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $name = \array_shift($children); - - if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); - } - - $parameters = isset($children[0]) && $children[0] instanceof Type\Callable\CallableParameterListNode - ? \array_shift($children) - : new Type\Callable\CallableParameterListNode(); - - return new Type\CallableTypeNode( - name: $name, - parameters: $parameters, - type: $children[0] ?? null, - ); - }, - 67 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable parameter attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; - }, - 73 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[0]->isVariadic) { - throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); - } - - $children[0]->isOptional = true; - - return $children[0]; - }, - 75 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[1]->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - - $children[1]->isVariadic = true; - - return $children[1]; - }, - 76 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (!\is_array($children)) { - return $children; - } - - $result = \end($children); - - foreach ($children as $modifier) { - if ($modifier instanceof Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; - }, - 80 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Callable\CallableParameterNode(null, $children[0]); - }, - 92 => static function (Phplrt\Parser\Context $ctx, $children) { - if (\count($children) === 1) { - return $children[0]; - } - - $children[0]->name = $children[1]; - - return $children[0]; - }, - 94 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $result = \reset($children); - - foreach ($children as $modifier) { - if ($modifier instanceof Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; - }, - 96 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Callable\CallableParameterNode($children[0]); - }, - 108 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $explicit = []; - $implicit = false; - - foreach ($children as $field) { - if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->getIndex(); - - if (\in_array($key, $explicit, true)) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); - } - - $explicit[] = $key; - } else { - $implicit = true; - } - } - - if ($explicit !== [] && $implicit) { - throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); - } - - return new Type\Shape\FieldsListNode($children); - }, - 120 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if ($children === []) { - return new Type\Shape\FieldsListNode(); - } - - if ($this->features->shapes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); - } - - $parameters = null; - - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - $fields = \reset($children) instanceof Type\Shape\FieldsListNode - ? \array_shift($children) - : new Type\Shape\FieldsListNode(); - - if ($children !== []) { - $fields->isSealed = false; - } - - return \array_filter([$parameters, $fields]); - }, - 123 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape field attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; - }, - 127 => static function (Phplrt\Parser\Context $ctx, $children) { - $name = $children[0]; - $value = \array_pop($children); - - // In case of "nullable" suffix defined - $optional = \count($children) === 2; - - return match (true) { - $name instanceof Type\Literal\IntLiteralNode - => new Type\Shape\NumericFieldNode($name, $value, $optional), - $name instanceof Type\Literal\StringLiteralNode - => new Type\Shape\StringNamedFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstNode - => new Type\Shape\ClassConstFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstMaskNode - => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional), - $name instanceof Type\ConstMaskNode - => new Type\Shape\ConstMaskFieldNode($name, $value, $optional), - default => new Type\Shape\NamedFieldNode($name, $value, $optional), - }; - }, - 128 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Shape\ImplicitFieldNode($children[0]); - }, - 138 => static function (Phplrt\Parser\Context $ctx, $children) { - $fields = $parameters = null; - - // Shape fields - if (\end($children) instanceof Type\Shape\FieldsListNode) { - $fields = \array_pop($children); - } - - // Template parameters - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - return new Type\NamedTypeNode( - $children[0], - $parameters, - $fields, - ); - }, - 144 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $count = \count($children); - - if ($count === 1) { - return $children[0]; - } - - if ($this->features->conditions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); - } - - $condition = match ($children[1]->getName()) { - 'T_EQ' => new Type\Condition\EqualConditionNode( - $children[0], - $children[2], - ), - 'T_NEQ' => new Type\Condition\NotEqualConditionNode( - $children[0], - $children[2], - ), - 'T_GTE' => new Type\Condition\GreaterThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_CLOSE' => new Type\Condition\GreaterThanConditionNode( - $children[0], - $children[2], - ), - 'T_LTE' => new Type\Condition\LessThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_OPEN' => new Type\Condition\LessThanConditionNode( - $children[0], - $children[2], - ), - default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( - $children[1]->getValue(), - $offset, - ), - }; - - return new Type\TernaryExpressionNode( - $condition, - $children[3], - $children[4], - ); - }, - 155 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeGroupNode($children); - }, - 161 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeNode( - $children[0], - ); - }, - 165 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeArgumentListNode($children); - }, - 167 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeArgumentNode($children[0]); - }, - 176 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 2) { - if ($this->features->unions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); - } - - return new Type\UnionTypeNode($children[0], $children[1]); - } - - return $children; - }, - 177 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 2) { - if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); - } - - return new Type\IntersectionTypeNode($children[0], $children[1]); - } - - return $children; - }, - 185 => static function (Phplrt\Parser\Context $ctx, $children) { - if (\is_array($children)) { - return new Type\NullableTypeNode($children[1]); - } - - return $children; - }, - 186 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $statement = \array_shift($children); - - foreach ($children as $child) { - switch (true) { - // In case of list type - case $child === true: - if ($this->features->lists === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); - } - - $statement = new Type\TypesListNode($statement); - break; - // In case of offset access type - case $child instanceof Type\TypeNode: - if ($this->features->offsets === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); - } - - $statement = new Type\TypeOffsetAccessNode($statement, $child); - break; - default: - throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( - \get_debug_type($child), - $offset, - ); - } - } - - return $statement; - }, - 190 => static function (Phplrt\Parser\Context $ctx, $children) { - return $children[1] ?? true; - }, - ], -]; diff --git a/libs/parser/resources/grammar.pp2 b/libs/parser/resources/grammar.pp2 deleted file mode 100644 index 0c02b43..0000000 --- a/libs/parser/resources/grammar.pp2 +++ /dev/null @@ -1,173 +0,0 @@ - -/** - * ----------------------------------------------------------------------------- - * Language Syntax Summary - * ----------------------------------------------------------------------------- - */ - -%include grammar/lexemes // Lexeme/Token Definitions -%include grammar/common // Common Utils -%include grammar/literals // Literal Definitions -%include grammar/template-arguments // T -%include grammar/callable // callable(mixed): void -%include grammar/shape-fields // array { key: int, ... } -%include grammar/named-type // Map, non-empty-string> -%include grammar/ternary // T is A ? B : C -%include grammar/attribute // #[attr] - -%pragma root Type - -/** - * ----------------------------------------------------------------------------- - * Type Statement - * ----------------------------------------------------------------------------- - * - * Constant references to external definitions or - * describe the type in some way. - * - */ - -Type - : Expression() - ; - -/** - * ----------------------------------------------------------------------------- - * Ternary Expression - * ----------------------------------------------------------------------------- - * - * Ternary conditional expressions, like: - * - T is A ? B : C - for equality type conditions. - * - T is not A ? B ? C - for non-equality type conditions. - * - */ - -Expression - : TernaryExpressionOrLogicalType() - ; - - -/** - * ----------------------------------------------------------------------------- - * Logical Statements - * ----------------------------------------------------------------------------- - * - * Logical statements denote union or intersection types, like: - * - A | B | C - for union type references. - * - A & B & C - for intersection type references. - * - */ - -LogicalType - : UnionType() - ; - -UnionType -> { - if (\count($children) === 2) { - if ($this->features->unions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); - } - - return new Type\UnionTypeNode($children[0], $children[1]); - } - - return $children; -} - : IntersectionType() (::T_OR:: UnionType())? - ; - -IntersectionType -> { - if (\count($children) === 2) { - if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); - } - - return new Type\IntersectionTypeNode($children[0], $children[1]); - } - - return $children; -} - : UnaryType() (::T_AMP:: IntersectionType())? - ; - -/** - * ----------------------------------------------------------------------------- - * Unary Statements - * ----------------------------------------------------------------------------- - * - * Unary statements denote terminal types, like: - * - A - for type reference. - * - ?A - for nullable type reference. - * - 'some' - for string literal reference. - * - etc... - * - */ - -UnaryType - : PrefixedNullableType() - ; - -// stmt = ?Type -PrefixedNullableType -> { - if (\is_array($children)) { - return new Type\NullableTypeNode($children[1]); - } - - return $children; -} - : TypesList() - | TypesList() - ; - -TypesList -> { - $statement = \array_shift($children); - - foreach ($children as $child) { - switch (true) { - // In case of list type - case $child === true: - if ($this->features->lists === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); - } - - $statement = new Type\TypesListNode($statement); - break; - // In case of offset access type - case $child instanceof Type\TypeNode: - if ($this->features->offsets === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); - } - - $statement = new Type\TypeOffsetAccessNode($statement, $child); - break; - default: - throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( - \get_debug_type($child), - $offset, - ); - } - } - - return $statement; -} - : PrimaryType() - TypeListOrOffsetSuffix()* - ; - -// - Returns `true` in case of a list type (`T[]`). -// - Returns `NamedTypeNode` in case of an offset access type (`T[Y]`). -TypeListOrOffsetSuffix -> { - return $children[1] ?? true; -} - : - Type()? - ::T_SQUARE_BRACKET_CLOSE:: - ; - -PrimaryType - : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: - | ThisLiteral() - | Literal() - | CallableType() - | NamedType() - ; diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3 new file mode 100644 index 0000000..a7b9118 --- /dev/null +++ b/libs/parser/resources/grammar.pp3 @@ -0,0 +1,221 @@ + +/** + * ----------------------------------------------------------------------------- + * Language Syntax Summary + * ----------------------------------------------------------------------------- + */ + +%include grammar/lexemes // Lexeme/Token Definitions +%include grammar/common // Common Utils +%include grammar/literals // Literal Definitions +%include grammar/template-arguments // T +%include grammar/callable // callable(mixed): void +%include grammar/shape-fields // array { key: int, ... } +%include grammar/named-type // Map, non-empty-string> +%include grammar/ternary // T is A ? B : C + +%pragma root Type + +/** + * ----------------------------------------------------------------------------- + * Type Statement + * ----------------------------------------------------------------------------- + * + * Constant references to external definitions or + * describe the type in some way. + * + */ + +Type -> { + if (\count($children) === 1) { + return $children[0]; + } + + if ($this->features->conditions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); + } + + $condition = match ($children[1]->id) { + self::T_EQ => new Type\Condition\EqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_NEQ => new Type\Condition\NotEqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_GTE => new Type\Condition\GreaterThanOrEqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_ANGLE_BRACKET_CLOSE => new Type\Condition\GreaterThanConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_LTE => new Type\Condition\LessThanOrEqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_ANGLE_BRACKET_OPEN => new Type\Condition\LessThanConditionNode( + $children[0], + $children[2], + $offset, + ), + default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( + $children[1]->value, + $offset, + ), + }; + + return new Type\TernaryExpressionNode( + $condition, + $children[3], + $children[4], + $offset, + ); +} + : LogicalType() TernaryExpressionSuffix()? + | VariableLiteral() TernaryExpressionSuffix() + ; + +/** + * ----------------------------------------------------------------------------- + * Logical Statements + * ----------------------------------------------------------------------------- + * + * Logical statements denote union or intersection types, like: + * - A | B | C - for union type references. + * - A & B & C - for intersection type references. + * + * A repetition rather than a right recursion: every member of a statement is + * collected in a single list, so the node is built once instead of being + * rebuilt (and re-flattened) at each of its levels. + * + */ + +LogicalType + : UnionType() + ; + +UnionType -> { + if (\count($children) === 1) { + return $children[0]; + } + + if ($this->features->unions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); + } + + // A logical statement takes an arbitrary number of types, so its + // offset is assigned rather than passed to the constructor + $statement = new Type\UnionTypeNode($children); + $statement->offset = $offset; + + return $statement; +} + : IntersectionType() (::T_OR:: IntersectionType())* + ; + +IntersectionType -> { + if (\count($children) === 1) { + return $children[0]; + } + + if ($this->features->intersections === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); + } + + // A logical statement takes an arbitrary number of types, so its + // offset is assigned rather than passed to the constructor + $statement = new Type\IntersectionTypeNode($children); + $statement->offset = $offset; + + return $statement; +} + : UnaryType() (::T_AMP:: UnaryType())* + ; + +/** + * ----------------------------------------------------------------------------- + * Unary Statements + * ----------------------------------------------------------------------------- + * + * Unary statements denote terminal types, like: + * - A - for type reference. + * - ?A - for nullable type reference. + * - 'some' - for string literal reference. + * - etc... + * + */ + +UnaryType + : NullableType() + | TypesList() + ; + +// stmt = ?Type +NullableType -> { + return new Type\NullableTypeNode($children[0], $offset); +} + : ::T_QMARK:: TypesList() + ; + +TypesList -> { + $statement = $children[0]; + $count = \count($children); + + for ($i = 1; $i < $count; ++$i) { + $child = $children[$i]; + + switch (true) { + // In case of list type + case $child === true: + if ($this->features->lists === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); + } + + $statement = new Type\TypesListNode($statement, $offset); + break; + // In case of offset access type + case $child instanceof Type\TypeNode: + if ($this->features->offsets === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); + } + + $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset); + break; + default: + throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( + \get_debug_type($child), + $offset, + ); + } + } + + return $statement; +} + : PrimaryType() + TypeListOrOffsetSuffix()* + ; + +// - Returns `true` in case of a list type (`T[]`). +// - Returns `TypeNode` in case of an offset access type (`T[Y]`). +TypeListOrOffsetSuffix -> { + return $children[0] ?? true; +} + : ::T_SQUARE_BRACKET_OPEN:: + Type()? + ::T_SQUARE_BRACKET_CLOSE:: + ; + +PrimaryType + : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: + | ThisLiteral() + | Literal() + | NamedType() + ; diff --git a/libs/parser/resources/grammar/attribute.pp2 b/libs/parser/resources/grammar/attribute.pp2 deleted file mode 100644 index 51319c6..0000000 --- a/libs/parser/resources/grammar/attribute.pp2 +++ /dev/null @@ -1,44 +0,0 @@ - -AttributeGroupsList -> { - return new Type\Attribute\AttributeGroupListNode($children); -} - : AttributeGroup()+ - ; - -AttributeGroup -> { - return new Type\Attribute\AttributeGroupNode($children); -} - : ::T_ATTR_OPEN:: - AttributesList() ::T_COMMA::? - ::T_SQUARE_BRACKET_CLOSE:: - ; - -AttributesList - : Attribute() ( - ::T_COMMA:: Attribute() - )* - ; - -Attribute -> { - return new Type\Attribute\AttributeNode( - $children[0], - ); -} - : Name() AttributeArguments()? - ; - -AttributeArguments -> { - return new Type\Attribute\AttributeArgumentListNode($children); -} - : ::T_PARENTHESIS_OPEN:: - AttributeArgument() ( - ::T_COMMA:: AttributeArgument() - )* ::T_COMMA::? - ::T_PARENTHESIS_CLOSE:: - ; - -AttributeArgument -> { - return new Type\Attribute\AttributeArgumentNode($children[0]); -} - : Type() - ; diff --git a/libs/parser/resources/grammar/callable.pp2 b/libs/parser/resources/grammar/callable.pp2 deleted file mode 100644 index b33a316..0000000 --- a/libs/parser/resources/grammar/callable.pp2 +++ /dev/null @@ -1,189 +0,0 @@ - -CallableType -> { - $name = \array_shift($children); - - if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); - } - - $parameters = isset($children[0]) && $children[0] instanceof Type\Callable\CallableParameterListNode - ? \array_shift($children) - : new Type\Callable\CallableParameterListNode(); - - return new Type\CallableTypeNode( - name: $name, - parameters: $parameters, - type: $children[0] ?? null, - ); -} - : Name() - ::T_PARENTHESIS_OPEN:: - CallableParameters()? - ::T_PARENTHESIS_CLOSE:: - CallableReturnType()? - ; - -CallableParameters -> { - return new Type\Callable\CallableParameterListNode($children); -} - : CallableParameter() - ( ::T_COMMA:: CallableParameter() )* - ::T_COMMA::? - ; - -CallableParameter -> { - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable parameter attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; -} - : AttributeGroupsList()? - MaybeDefaultCallableParameter() - ; - -// Expects expression: -// - "" -// - " =" -MaybeDefaultCallableParameter -> { - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[0]->isVariadic) { - throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); - } - - $children[0]->isOptional = true; - return $children[0]; -} - : ( MaybePrefixedVariadicTypedNamedCallableParameter() - | MaybeModifiersNamedCallableParameter() ) - ? - ; - -// Expects expression: -// - "&..." -// - "...&" -// - "..." -// - "&" -// - "" -MaybeModifiersNamedCallableParameter -> { - if (!\is_array($children)) { - return $children; - } - - $result = \end($children); - - foreach ($children as $modifier) { - if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; -} - : MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - ; - -// Expects expression: -// - "$" -MaybeNamedCallableParameter -> { - return new Type\Callable\CallableParameterNode(null, $children[0]); -} - : VariableLiteral() - ; - -MaybePrefixedVariadicTypedNamedCallableParameter -> { - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[1]->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - - $children[1]->isVariadic = true; - return $children[1]; -} - : ? MaybeTypedNamedCallableParameter() - ; - -// Expects expression: -// - " $" -// - "" -MaybeTypedNamedCallableParameter -> { - if (\count($children) === 1) { - return $children[0]; - } - - $children[0]->name = $children[1]; - return $children[0]; -} - : MaybeModifiersTypedCallableParameter() - VariableLiteral()? - ; - -// Expects expression: -// - "...&" -// - "&..." -// - "&" -// - "..." -// - "" -MaybeModifiersTypedCallableParameter -> { - $result = \reset($children); - - foreach ($children as $modifier) { - if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; -} - : TypedCallableParameter() - ( ? - | ? )? - ; - -// Expects expression: -// - "" -TypedCallableParameter -> { - return new Type\Callable\CallableParameterNode($children[0]); -} - : Type() - ; - -CallableReturnType - : ::T_COLON:: Type() - ; diff --git a/libs/parser/resources/grammar/callable.pp3 b/libs/parser/resources/grammar/callable.pp3 new file mode 100644 index 0000000..9476069 --- /dev/null +++ b/libs/parser/resources/grammar/callable.pp3 @@ -0,0 +1,75 @@ + +// The "(T, U): V" of a callable type +CallableSuffix + : ::T_PARENTHESIS_OPEN:: + CallableParameters() + ::T_PARENTHESIS_CLOSE:: + CallableReturnType()? + ; + +CallableParameters -> { + return new Type\Callable\CallableParameterListNode($children, $offset); +} + : ( CallableParameter() + ( ::T_COMMA:: CallableParameter() )* + ::T_COMMA::? + )? + ; + +/** + * A parameter is written as a type, optionally followed by the reference and + * the variadic markers, by a name and by the default marker - in that order + * and in no other: + * + * - "" + * - "&" + * - "..." + * - "&..." + * - " $name" + * - "&...$name=" + * + */ +CallableParameter -> { + $parameter = new Type\Callable\CallableParameterNode($children[0], offset: $offset); + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + $child = $children[$i]; + + if ($child instanceof Type\Literal\VariableLiteralNode) { + $parameter->name = $child; + + continue; + } + + switch ($child->id) { + case self::T_AMP: + $parameter->isOutput = true; + break; + + case self::T_ELLIPSIS: + $parameter->isVariadic = true; + break; + + // A variadic parameter is optional in itself, so a default marker + // beside one describes the same parameter in two different ways + default: + if ($parameter->isVariadic) { + throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); + } + + $parameter->isOptional = true; + } + } + + return $parameter; +} + : Type() + ? + ? + VariableLiteral()? + ? + ; + +CallableReturnType + : ::T_COLON:: Type() + ; diff --git a/libs/parser/resources/grammar/common.pp2 b/libs/parser/resources/grammar/common.pp2 deleted file mode 100644 index 8662158..0000000 --- a/libs/parser/resources/grammar/common.pp2 +++ /dev/null @@ -1,33 +0,0 @@ - -Name - : FullQualifiedName() - | RelativeName() - ; - -FullQualifiedName -> { - return new Type\Name($children, true); -} - : ::T_NS_DELIMITER:: Identifier() (::T_NS_DELIMITER:: Identifier())* - ; - -RelativeName -> { - return new Type\Name($children, false); -} - : Identifier() (::T_NS_DELIMITER:: Identifier())* - ; - -Identifier -> { - return Type\Identifier::createFromString($children->getValue()); -} - : - | - | - | - | - ; - -IdentifierWithExtraSpace -> { - return Type\Identifier::createFromString($children->getValue()); -} - : - ; diff --git a/libs/parser/resources/grammar/common.pp3 b/libs/parser/resources/grammar/common.pp3 new file mode 100644 index 0000000..5142b52 --- /dev/null +++ b/libs/parser/resources/grammar/common.pp3 @@ -0,0 +1,40 @@ + +Name + : FullQualifiedName() + | RelativeName() + ; + +FullQualifiedName -> { + return new Type\Name($children, true, $offset); +} + : ::T_NS_DELIMITER:: Identifier() (::T_NS_DELIMITER:: Identifier())* + ; + +RelativeName -> { + return new Type\Name($children, false, $offset); +} + : Identifier() (::T_NS_DELIMITER:: Identifier())* + ; + +Identifier -> { + // Only a name written with a trailing space carries one, so the value of + // every other token is the identifier itself + return new Type\Identifier( + $children->id === self::T_NAME_WITH_SPACE + ? \rtrim($children->value) + : $children->value, + $offset, + ); +} + : + | + | + | + | + ; + +IdentifierWithExtraSpace -> { + return new Type\Identifier(\rtrim($children->value), $offset); +} + : + ; diff --git a/libs/parser/resources/grammar/lexemes.pp2 b/libs/parser/resources/grammar/lexemes.pp2 deleted file mode 100644 index 4edb9e5..0000000 --- a/libs/parser/resources/grammar/lexemes.pp2 +++ /dev/null @@ -1,53 +0,0 @@ - -// Literals - -%token T_DQ_STRING_LITERAL "([^"\\]*(?:\\.[^"\\]*)*)" -%token T_SQ_STRING_LITERAL '([^'\\]*(?:\\.[^'\\]*)*)' -%token T_PFX_FLOAT_LITERAL \-?[0-9]++\.[0-9]*+(?:[eE]-?[0-9]++)? -%token T_SFX_FLOAT_LITERAL \-?[0-9]*+\.[0-9]++(?:[eE]-?[0-9]++)? -%token T_EXP_LITERAL \-?[0-9]++[eE]-?[0-9]++ -%token T_BIN_INT_LITERAL \-?0[bB][01_]++ -%token T_OCT_INT_LITERAL \-?0[oO][0-7_]++ -%token T_HEX_INT_LITERAL \-?0[xX][0-9a-fA-F_]++ -%token T_DEC_INT_LITERAL \-?[0-9][0-9_]*+ -%token T_BOOL_LITERAL (?i)(?:true|false)(?![a-zA-Z0-9\-_\x80-\xff]) -%token T_NULL_LITERAL (?i)null(?![a-zA-Z0-9\-_\x80-\xff]) - -// Identifier - -%token T_NEQ (?i)is\h++not(?![a-zA-Z0-9\-_\x80-\xff]) -%token T_EQ (?i)is(?![a-zA-Z0-9\-_\x80-\xff]) -%token T_THIS \$this\b -%token T_VARIABLE \$[a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+ -%token T_NAME_WITH_SPACE [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+\s++ -%token T_NAME [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+ - -// Special Chars - -%token T_LTE <= -%token T_GTE >= -%token T_ANGLE_BRACKET_OPEN < -%token T_ANGLE_BRACKET_CLOSE > -%token T_PARENTHESIS_OPEN \( -%token T_PARENTHESIS_CLOSE \) -%token T_BRACE_OPEN \{ -%token T_BRACE_CLOSE \} -%token T_ATTR_OPEN #\[ -%token T_SQUARE_BRACKET_OPEN \[ -%token T_SQUARE_BRACKET_CLOSE \] -%token T_COMMA , -%token T_ELLIPSIS \.\.\. -%token T_DOUBLE_COLON :: -%token T_COLON : -%token T_ASSIGN = -%token T_NS_DELIMITER \\ -%token T_QMARK \? -%token T_OR \| -%token T_AMP & -%token T_ASTERISK \* - -// Other - -%skip T_COMMENT (?://|#)[^\r\n]*+ -%skip T_DOC_COMMENT /\*.*?\*/ -%skip T_WHITESPACE \s++ diff --git a/libs/parser/resources/grammar/lexemes.pp3 b/libs/parser/resources/grammar/lexemes.pp3 new file mode 100644 index 0000000..55e0070 --- /dev/null +++ b/libs/parser/resources/grammar/lexemes.pp3 @@ -0,0 +1,85 @@ + +/** + * ----------------------------------------------------------------------------- + * Pattern Fragments + * ----------------------------------------------------------------------------- + * + * The pieces the token definitions below are spelled out of. A fragment + * recognizes nothing on its own and reaches no token stream. + * + */ + +%fragment IDENT_HEAD [a-zA-Z_\x80-\xff] +%fragment IDENT_TAIL [a-zA-Z0-9\-_\x80-\xff] +%fragment DIGIT [0-9] +%fragment EXPONENT [eE]-?(?&DIGIT)++ + +// A keyword ends where an identifier would go on, so that "isEmpty" is a name +// rather than the "is" operator followed by one. +%fragment KEYWORD_END (?!(?&IDENT_TAIL)) + +/** + * ----------------------------------------------------------------------------- + * Lexeme/Token Definitions + * ----------------------------------------------------------------------------- + * + * The lexer takes the first pattern that matches, so the order below is both + * a matter of meaning (a longer token above the shorter one it starts with) + * and a matter of speed (a token that occurs more often above one that does + * not). + * + */ + +// Whitespace is the most frequent thing there is in a type statement +%skip T_WHITESPACE \s++ + +// Keywords, above the identifier they would otherwise be read as +%token T_NEQ (?i:is\h++not)(?&KEYWORD_END) +%token T_EQ (?i:is)(?&KEYWORD_END) +%token T_BOOL_LITERAL (?i:true|false)(?&KEYWORD_END) +%token T_NULL_LITERAL (?i:null)(?&KEYWORD_END) + +// Identifiers +%token T_NAME_WITH_SPACE (?&IDENT_HEAD)(?&IDENT_TAIL)*+\s++ +%token T_NAME (?&IDENT_HEAD)(?&IDENT_TAIL)*+ + +// Punctuation, ordered by how often a type statement contains it +%token T_NS_DELIMITER \\ +%token T_OR \| +%token T_COMMA , +%token T_DOUBLE_COLON :: +%token T_COLON : +%token T_LTE <= +%token T_GTE >= +%token T_ANGLE_BRACKET_OPEN < +%token T_ANGLE_BRACKET_CLOSE > +%token T_QMARK \? +%token T_BRACE_OPEN \{ +%token T_BRACE_CLOSE \} +%token T_PARENTHESIS_OPEN \( +%token T_PARENTHESIS_CLOSE \) +%token T_SQUARE_BRACKET_OPEN \[ +%token T_SQUARE_BRACKET_CLOSE \] +%token T_AMP & +%token T_ELLIPSIS \.\.\. +%token T_ASSIGN = +%token T_ASTERISK \* + +// Variables +%token T_THIS \$this\b +%token T_VARIABLE \$(?&IDENT_HEAD)(?&IDENT_TAIL)*+ + +// Literals +%token T_DQ_STRING_LITERAL "[^"\\]*+(?:\\.[^"\\]*+)*+" +%token T_SQ_STRING_LITERAL '[^'\\]*+(?:\\.[^'\\]*+)*+' +%token T_PFX_FLOAT_LITERAL -?(?&DIGIT)++\.(?&DIGIT)*+(?:(?&EXPONENT))? +%token T_SFX_FLOAT_LITERAL -?(?&DIGIT)*+\.(?&DIGIT)++(?:(?&EXPONENT))? +%token T_EXP_LITERAL -?(?&DIGIT)++(?&EXPONENT) +%token T_BIN_INT_LITERAL -?0[bB][01_]++ +%token T_OCT_INT_LITERAL -?0[oO][0-7_]++ +%token T_HEX_INT_LITERAL -?0[xX][0-9a-fA-F_]++ +%token T_DEC_INT_LITERAL -?(?&DIGIT)[0-9_]*+ + +// Comments +%skip T_COMMENT (?://|#)[^\r\n]*+ +%skip T_DOC_COMMENT /\*.*?\*/ diff --git a/libs/parser/resources/grammar/literals.pp2 b/libs/parser/resources/grammar/literals.pp2 deleted file mode 100644 index 8536fad..0000000 --- a/libs/parser/resources/grammar/literals.pp2 +++ /dev/null @@ -1,111 +0,0 @@ - -Literal - : RawLiteral() - | ConstMaskLiteral() - | ClassConstLiteral() - ; - -RawLiteral -> { - if ($this->features->literals === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); - } - return $children; -} - : StringLiteral() - | FloatLiteral() - | IntLiteral() - | BoolLiteral() - | NullLiteral() - ; - -VariableLiteral -> { - return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); -} - : - | - ; - -ThisLiteral -> { - return new Type\Literal\VariableLiteralNode(\substr($token->getValue(), 1)); -} - : - ; - -StringLiteral -> { return $this->stringPool[$token] ??= $children; } - : DoubleQuotedStringLiteral() - | SingleQuotedStringLiteral() - ; - -DoubleQuotedStringLiteral -> { - return Literal\StringLiteralParser::createFromDoubleQuotedString($token->getValue()); -} - : - ; - -SingleQuotedStringLiteral -> { - return Literal\StringLiteralParser::createFromSingleQuotedString($token->getValue()); -} - : - ; - -FloatLiteral -> { - return new Type\Literal\FloatLiteralNode((float) $token->getValue(), $token->getValue()); -} - : - | - | - ; - -IntLiteral -> { - return $this->integerPool[$token] ??= Literal\IntLiteralParser::parse($token->getValue()); -} - : - | - | - | - ; - -BoolLiteral -> { - return new Type\Literal\BoolLiteralNode(\strtolower($token->getValue()) === 'true', $token->getValue()); -} - : - ; - -NullLiteral -> { - return new Type\Literal\NullLiteralNode($children->getValue()); -} - : - ; - -ConstMaskLiteral -> { - return new Type\ConstMaskNode($children[0]); -} - : Name() ::T_ASTERISK:: - ; - -ClassConstLiteral -> { - // :: "*" - if (\count($children) === 3) { - return new Type\ClassConstMaskNode( - $children[0], - $children[1], - ); - } - - // :: - if ($children[1] instanceof Type\Identifier) { - return new Type\ClassConstNode( - $children[0], - $children[1], - ); - } - - // :: "*" - return new Type\ClassConstMaskNode($children[0]); -} - : Name() ::T_DOUBLE_COLON:: - ( Identifier() - | Identifier() - | - ) - ; diff --git a/libs/parser/resources/grammar/literals.pp3 b/libs/parser/resources/grammar/literals.pp3 new file mode 100644 index 0000000..968a4c4 --- /dev/null +++ b/libs/parser/resources/grammar/literals.pp3 @@ -0,0 +1,72 @@ + +Literal -> { + if ($this->features->literals === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); + } + return $children; +} + : StringLiteral() + | FloatLiteral() + | IntLiteral() + | BoolLiteral() + | NullLiteral() + ; + +VariableLiteral -> { + return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); +} + : + | + ; + +ThisLiteral -> { + return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); +} + : + ; + +StringLiteral + : DoubleQuotedStringLiteral() + | SingleQuotedStringLiteral() + ; + +DoubleQuotedStringLiteral -> { + return $this->createDoubleQuotedString($children->value, $offset); +} + : + ; + +SingleQuotedStringLiteral -> { + return $this->createSingleQuotedString($children->value, $offset); +} + : + ; + +FloatLiteral -> { + return new Type\Literal\FloatLiteralNode((float) $children->value, $children->value, $offset); +} + : + | + | + ; + +IntLiteral -> { + return $this->createInt($children->value, $offset); +} + : + | + | + | + ; + +BoolLiteral -> { + return $this->createBool($children->value, $offset); +} + : + ; + +NullLiteral -> { + return new Type\Literal\NullLiteralNode($children->value, $offset); +} + : + ; diff --git a/libs/parser/resources/grammar/named-type.pp2 b/libs/parser/resources/grammar/named-type.pp2 deleted file mode 100644 index 300654d..0000000 --- a/libs/parser/resources/grammar/named-type.pp2 +++ /dev/null @@ -1,22 +0,0 @@ - -NamedType -> { - $fields = $parameters = null; - - // Shape fields - if (\end($children) instanceof Type\Shape\FieldsListNode) { - $fields = \array_pop($children); - } - - // Template parameters - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - return new Type\NamedTypeNode( - $children[0], - $parameters, - $fields, - ); -} - : Name() (TemplateArguments() | ShapeFields())? - ; diff --git a/libs/parser/resources/grammar/named-type.pp3 b/libs/parser/resources/grammar/named-type.pp3 new file mode 100644 index 0000000..8a483f7 --- /dev/null +++ b/libs/parser/resources/grammar/named-type.pp3 @@ -0,0 +1,88 @@ + +/** + * ----------------------------------------------------------------------------- + * Named Types + * ----------------------------------------------------------------------------- + * + * Everything a name may open is read by a single rule, so that the name is + * recognized once instead of once per alternative that begins with it. + * + * Each suffix begins with a token of its own ("*", "::", "(", "<" and "{"), + * so which one to read is decided by the token the reading is at rather than + * by trying them in turn. + * + */ + +NamedType -> { + $suffix = $children[1] ?? null; + + switch (true) { + // Some\Any + case $suffix instanceof Type\Template\TemplateArgumentListNode: + return new Type\NamedTypeNode($children[0], $suffix, null, $offset); + + // Some\Any{name: T, ...} + case $suffix instanceof Type\Shape\FieldsListNode: + return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); + + // Some\Any(T, U): V + case $suffix instanceof Type\Callable\CallableParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); + } + + return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, $offset); + + // Some\Any::CONST and Some\Any::CONST_* + case $suffix instanceof Type\Identifier: + if (isset($children[2])) { + return new Type\ClassConstMaskNode($children[0], $suffix, $offset); + } + + return new Type\ClassConstNode($children[0], $suffix, $offset); + + // Some\Any::* + case $suffix === true: + return new Type\ClassConstMaskNode($children[0], offset: $offset); + + // SOME_ANY_* + case $suffix === false: + return new Type\ConstMaskNode($children[0], $offset); + + // Some\Any + default: + return new Type\NamedTypeNode($children[0], null, null, $offset); + } +} + : Name() NamedTypeSuffix()? + ; + +NamedTypeSuffix + : ConstMaskSuffix() + | ClassConstSuffix() + | CallableSuffix() + | TemplateArguments() + | ShapeFields() + ; + +// The "*" of a constant mask, like "SOME_*" +ConstMaskSuffix -> { + return false; +} + : + ; + +// The "::CONST", "::CONST_*" and "::*" of a class constant +ClassConstSuffix + : ::T_DOUBLE_COLON:: + ( Identifier() ClassConstMaskSuffix()? + | ClassConstMaskSuffix() + ) + ; + +// The "*" of a class constant mask, like "Some::CONST_*" or "Some::*" +ClassConstMaskSuffix -> { + return true; +} + : + ; diff --git a/libs/parser/resources/grammar/shape-fields.pp2 b/libs/parser/resources/grammar/shape-fields.pp2 deleted file mode 100644 index 2e79b19..0000000 --- a/libs/parser/resources/grammar/shape-fields.pp2 +++ /dev/null @@ -1,128 +0,0 @@ - -ShapeFields -> { - if ($children === []) { - return new Type\Shape\FieldsListNode(); - } - - if ($this->features->shapes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); - } - - $parameters = null; - - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - $fields = \reset($children) instanceof Type\Shape\FieldsListNode - ? \array_shift($children) - : new Type\Shape\FieldsListNode(); - - if ($children !== []) { - $fields->sealed = false; - } - - return \array_filter([$parameters, $fields]); -} - : ::T_BRACE_OPEN:: - ( - (ShapeFieldsList() (::T_COMMA:: ShapeFieldsUnsealed())?) - | ShapeFieldsUnsealed()? - ) - ::T_COMMA::? - ::T_BRACE_CLOSE:: - ; - -ShapeFieldsUnsealed - : TemplateArguments()? - ; - -ShapeFieldsList -> { - $explicit = []; - $implicit = false; - - foreach ($children as $field) { - if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->getIndex(); - - if (\in_array($key, $explicit, true)) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); - } - - $explicit[] = $key; - } else { - $implicit = true; - } - } - - if ($explicit !== [] && $implicit) { - throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); - } - - return new Type\Shape\FieldsListNode($children); -} - : ShapeField() ( - ::T_COMMA:: ShapeField() - )* - ; - -ShapeField -> { - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape field attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; -} - : AttributeGroupsList()? ( - ExplicitField() - | ImplicitField() - ) - ; - -ExplicitField -> { - $name = $children[0]; - $value = \array_pop($children); - - // In case of "nullable" suffix defined - $optional = \count($children) === 2; - - return match (true) { - $name instanceof Type\Literal\IntLiteralNode - => new Type\Shape\NumericFieldNode($name, $value, $optional), - $name instanceof Type\Literal\StringLiteralNode - => new Type\Shape\StringNamedFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstNode - => new Type\Shape\ClassConstFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstMaskNode - => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional), - $name instanceof Type\ConstMaskNode - => new Type\Shape\ConstMaskFieldNode($name, $value, $optional), - default => new Type\Shape\NamedFieldNode($name, $value, $optional), - }; -} - : ShapeKey() ()? ::T_COLON:: ShapeValue() - ; - -ImplicitField -> { - return new Type\Shape\ImplicitFieldNode($children[0]); -} - : ShapeValue() - ; - -ShapeKey - : ConstMaskLiteral() - | ClassConstLiteral() - | Identifier() - | IntLiteral() - | StringLiteral() - ; - -ShapeValue - : Type() - ; diff --git a/libs/parser/resources/grammar/shape-fields.pp3 b/libs/parser/resources/grammar/shape-fields.pp3 new file mode 100644 index 0000000..396335b --- /dev/null +++ b/libs/parser/resources/grammar/shape-fields.pp3 @@ -0,0 +1,169 @@ + +ShapeFields -> { + $count = \count($children); + + // An empty shape carries no fields and needs no support for them + if ($count === 0) { + return [new Type\Shape\FieldsListNode(offset: $offset)]; + } + + if ($this->features->shapes === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); + } + + $parameters = null; + + // The "..." suffix, in case of the shape is unsealed by a typed rest + if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) { + $parameters = $children[$count - 1]; + --$count; + } + + if ($children[0] instanceof Type\Shape\FieldsListNode) { + $fields = $children[0]; + // Whatever is left beside the fields is the "..." of an unsealed shape + $unsealed = $count > 1; + } else { + $fields = new Type\Shape\FieldsListNode(offset: $offset); + $unsealed = $count > 0; + } + + if ($unsealed) { + $fields->isSealed = false; + } + + if ($parameters === null) { + return [$fields]; + } + + return [$fields, $parameters]; +} + : ::T_BRACE_OPEN:: + ( + (ShapeFieldsList() (::T_COMMA:: ShapeFieldsUnsealed())?) + | ShapeFieldsUnsealed()? + ) + ::T_COMMA::? + ::T_BRACE_CLOSE:: + ; + +ShapeFieldsUnsealed + : TemplateArguments()? + ; + +ShapeFieldsList -> { + $explicit = []; + $implicit = false; + + foreach ($children as $field) { + if ($field instanceof Type\Shape\ExplicitFieldNode) { + $key = $field->getIndex(); + + if (isset($explicit[$key])) { + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); + } + + $explicit[$key] = true; + } else { + $implicit = true; + } + } + + if ($explicit !== [] && $implicit) { + throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); + } + + return new Type\Shape\FieldsListNode($children, offset: $offset); +} + : ShapeField() ( + ::T_COMMA:: ShapeField() + )* + ; + +ShapeField + : ExplicitField() + | ImplicitField() + ; + +ExplicitField -> { + $count = \count($children); + $name = $children[0]; + $value = $children[$count - 1]; + + // In case of "nullable" suffix defined + $optional = $count === 3; + + return match (true) { + $name instanceof Type\Literal\IntLiteralNode + => new Type\Shape\NumericFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\Literal\StringLiteralNode + => new Type\Shape\StringNamedFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\ClassConstNode + => new Type\Shape\ClassConstFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\ClassConstMaskNode + => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\ConstMaskNode + => new Type\Shape\ConstMaskFieldNode($name, $value, $optional, offset: $offset), + default => new Type\Shape\NamedFieldNode($name, $value, $optional, offset: $offset), + }; +} + : ShapeKey() ()? ::T_COLON:: ShapeValue() + ; + +ImplicitField -> { + return new Type\Shape\ImplicitFieldNode($children[0], offset: $offset); +} + : ShapeValue() + ; + +/** + * A key written as a bare name is by far the most common one, and telling it + * apart from a constant reference costs a whole name to read. + * + * The predicate answers the same question by the token that follows the name + * instead: a key ends where its value begins, while "SOME_*" and "Some::CONST" + * go on. + */ +ShapeKey + : Identifier() &ShapeKeyEnd() + | ShapeConstantKey() + | IntLiteral() + | StringLiteral() + ; + +// The "?:" and ":" a shape key is followed by +ShapeKeyEnd + : ::T_QMARK::? ::T_COLON:: + ; + +/** + * A constant reference used as a shape key: the name is read once for every + * form it may take rather than once per form. + */ +ShapeConstantKey -> { + $suffix = $children[1]; + + switch (true) { + // Some\Any::CONST and Some\Any::CONST_* + case $suffix instanceof Type\Identifier: + if (isset($children[2])) { + return new Type\ClassConstMaskNode($children[0], $suffix, $offset); + } + + return new Type\ClassConstNode($children[0], $suffix, $offset); + + // Some\Any::* + case $suffix === true: + return new Type\ClassConstMaskNode($children[0], offset: $offset); + + // SOME_ANY_* + default: + return new Type\ConstMaskNode($children[0], $offset); + } +} + : Name() ( ConstMaskSuffix() | ClassConstSuffix() ) + ; + +ShapeValue + : Type() + ; diff --git a/libs/parser/resources/grammar/template-arguments.pp2 b/libs/parser/resources/grammar/template-arguments.pp3 similarity index 52% rename from libs/parser/resources/grammar/template-arguments.pp2 rename to libs/parser/resources/grammar/template-arguments.pp3 index e0cecce..607eeba 100644 --- a/libs/parser/resources/grammar/template-arguments.pp2 +++ b/libs/parser/resources/grammar/template-arguments.pp3 @@ -4,7 +4,7 @@ TemplateArguments -> { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset); } - return new Type\Template\TemplateArgumentListNode($children); + return new Type\Template\TemplateArgumentListNode($children, $offset); } : ::T_ANGLE_BRACKET_OPEN:: TemplateArgument() ( @@ -14,36 +14,26 @@ TemplateArguments -> { ; TemplateArgument -> { - $hint = $attributes = null; + $count = \count($children); + $hint = null; - if (\reset($children) instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument attributes', $offset); - } - - $attributes = \array_shift($children); - } - - $type = \array_pop($children); - - if (\reset($children) !== false) { + // An "in T" or an "out T" argument puts a hint in front of the type + if ($count === 2) { if ($this->features->hints === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); } - $hint = \reset($children); + $hint = $children[0]; } return new Type\Template\TemplateArgumentNode( - $type, + $children[$count - 1], $hint, - $attributes, + $offset, ); } - : AttributeGroupsList()? ( - TemplateHintedArgument() - | TemplateSimpleArgument() - ) + : TemplateHintedArgument() + | TemplateSimpleArgument() ; TemplateSimpleArgument diff --git a/libs/parser/resources/grammar/ternary.pp2 b/libs/parser/resources/grammar/ternary.pp2 deleted file mode 100644 index cd86fdc..0000000 --- a/libs/parser/resources/grammar/ternary.pp2 +++ /dev/null @@ -1,71 +0,0 @@ - -TernaryExpressionOrLogicalType -> { - $count = \count($children); - - if ($count === 1) { - return $children[0]; - } - - if ($this->features->conditions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); - } - - $condition = match ($children[1]->getName()) { - 'T_EQ' => new Type\Condition\EqualConditionNode( - $children[0], - $children[2], - ), - 'T_NEQ' => new Type\Condition\NotEqualConditionNode( - $children[0], - $children[2], - ), - 'T_GTE' => new Type\Condition\GreaterThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_CLOSE' => new Type\Condition\GreaterThanConditionNode( - $children[0], - $children[2], - ), - 'T_LTE' => new Type\Condition\LessThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_OPEN' => new Type\Condition\LessThanConditionNode( - $children[0], - $children[2], - ), - default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( - $children[1]->getValue(), - $offset, - ), - }; - - return new Type\TernaryExpressionNode( - $condition, - $children[3], - $children[4], - ); -} - : LogicalType() OptionalTernaryExpressionSuffix() - | VariableLiteral() TernaryExpressionSuffix() - ; - -OptionalTernaryExpressionSuffix - : TernaryExpressionSuffix()? - ; - -TernaryExpressionSuffix - : TernaryExpressionOperator() (Type() | VariableLiteral()) - ::T_QMARK:: Type() - ::T_COLON:: Type() - ; - -TernaryExpressionOperator - : - | - | - | - | - | - ; diff --git a/libs/parser/resources/grammar/ternary.pp3 b/libs/parser/resources/grammar/ternary.pp3 new file mode 100644 index 0000000..c7e6f4c --- /dev/null +++ b/libs/parser/resources/grammar/ternary.pp3 @@ -0,0 +1,14 @@ +TernaryExpressionSuffix + : TernaryExpressionOperator() (Type() | VariableLiteral()) + ::T_QMARK:: Type() + ::T_COLON:: Type() + ; + +TernaryExpressionOperator + : + | + | + | + | + | + ; diff --git a/libs/parser/src/Exception/Formatter.php b/libs/parser/src/Exception/Formatter.php index 0a8539d..9df22ee 100644 --- a/libs/parser/src/Exception/Formatter.php +++ b/libs/parser/src/Exception/Formatter.php @@ -4,9 +4,11 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Lexer\Token\Renderer; +use Phplrt\Contracts\Lexer\Channel; +use Phplrt\Lexer\Token\Printer\PrettyTokenPrinter; use Phplrt\Lexer\Token\Token; -use Phplrt\Position\Position; +use Phplrt\Position\PositionFactory; +use Phplrt\Source\StringSource; /** * @internal this is an internal library class, please do not use it in your code @@ -34,9 +36,14 @@ public static function source(string $statement): string return ''; } - $renderer = new Renderer(); + $printer = new PrettyTokenPrinter(); - return $renderer->value(new Token('', $statement, 0)); + return $printer->printValue(new Token( + id: 0, + name: null, + channel: Channel::Default, + value: $statement, + )); } /** @@ -46,9 +53,14 @@ public static function source(string $statement): string public static function suffix(string $statement, int $offset): string { if (\str_contains($statement, "\n")) { - $pos = Position::fromOffset($statement, $offset); + $positions = new PositionFactory(); - return \sprintf('on line %d at column %d', $pos->getLine(), $pos->getColumn()); + $pos = $positions->createFromOffset( + source: StringSource::createFromString($statement), + offset: $offset, + ); + + return \sprintf('on line %d at column %d', $pos->line, $pos->column); } return \sprintf('at column %d', $offset + 1); diff --git a/libs/parser/src/Exception/InternalParseException.php b/libs/parser/src/Exception/InternalParseException.php index 06a388f..969fa27 100644 --- a/libs/parser/src/Exception/InternalParseException.php +++ b/libs/parser/src/Exception/InternalParseException.php @@ -4,7 +4,7 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; final class InternalParseException extends ParseException { diff --git a/libs/parser/src/Exception/SemanticException.php b/libs/parser/src/Exception/SemanticException.php index 22847e8..b40aec6 100644 --- a/libs/parser/src/Exception/SemanticException.php +++ b/libs/parser/src/Exception/SemanticException.php @@ -16,8 +16,6 @@ abstract class SemanticException extends \LogicException implements ParserExcept final public const ERROR_CODE_VARIADIC_WITH_DEFAULT = 0x03; - final public const ERROR_CODE_VARIADIC_ALREADY_VARIADIC = 0x04; - final public const ERROR_CODE_INVALID_OPERATOR = 0x05; protected const CODE_LAST = self::ERROR_CODE_INVALID_OPERATOR; diff --git a/libs/parser/src/Exception/SemanticParseException.php b/libs/parser/src/Exception/SemanticParseException.php index a1b86dd..6732e7e 100644 --- a/libs/parser/src/Exception/SemanticParseException.php +++ b/libs/parser/src/Exception/SemanticParseException.php @@ -4,8 +4,8 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; use Phplrt\Contracts\Source\ReadableInterface; -use Phplrt\Contracts\Source\SourceExceptionInterface; final class SemanticParseException extends ParseException { @@ -19,8 +19,8 @@ public static function becauseSemanticErrorOccurs(SemanticException $e, Readable { $message = \vsprintf('%s in %s %s', [ \ucfirst($e->getMessage()), - Formatter::source($source->getContents()), - Formatter::suffix($source->getContents(), $e->getOffset()), + Formatter::source($source->content), + Formatter::suffix($source->content, $e->getOffset()), ]); return new self($message, self::ERROR_CODE_SEMANTIC_ERROR_BASE + $e->getCode()); diff --git a/libs/parser/src/Exception/UnexpectedTokenException.php b/libs/parser/src/Exception/UnexpectedTokenException.php index bc1f932..0e46508 100644 --- a/libs/parser/src/Exception/UnexpectedTokenException.php +++ b/libs/parser/src/Exception/UnexpectedTokenException.php @@ -4,7 +4,7 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; final class UnexpectedTokenException extends ParseException { diff --git a/libs/parser/src/Exception/UnrecognizedSyntaxException.php b/libs/parser/src/Exception/UnrecognizedSyntaxException.php index a6b6e83..010fc65 100644 --- a/libs/parser/src/Exception/UnrecognizedSyntaxException.php +++ b/libs/parser/src/Exception/UnrecognizedSyntaxException.php @@ -4,7 +4,7 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; final class UnrecognizedSyntaxException extends ParseException { diff --git a/libs/parser/src/Exception/UnrecognizedTokenException.php b/libs/parser/src/Exception/UnrecognizedTokenException.php index 5973c13..3f26041 100644 --- a/libs/parser/src/Exception/UnrecognizedTokenException.php +++ b/libs/parser/src/Exception/UnrecognizedTokenException.php @@ -4,7 +4,7 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; final class UnrecognizedTokenException extends ParseException { diff --git a/libs/parser/src/Exception/VariadicRedefinitionException.php b/libs/parser/src/Exception/VariadicRedefinitionException.php deleted file mode 100644 index 9f88f5e..0000000 --- a/libs/parser/src/Exception/VariadicRedefinitionException.php +++ /dev/null @@ -1,20 +0,0 @@ - $offset - */ - public static function becauseVariadicIsRedefined(int $offset = 0): self - { - $message = 'Either prefix or postfix variadic syntax should be used, but not both'; - - return new self($offset, $message, self::ERROR_CODE_VARIADIC_ALREADY_VARIADIC); - } -} diff --git a/libs/parser/src/InMemoryTypeParser.php b/libs/parser/src/InMemoryTypeParser.php index 05b10f3..3128250 100644 --- a/libs/parser/src/InMemoryTypeParser.php +++ b/libs/parser/src/InMemoryTypeParser.php @@ -5,7 +5,8 @@ namespace TypeLang\Parser; use JetBrains\PhpStorm\Language; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; +use Phplrt\Contracts\Source\ReadableInterface; use Phplrt\Contracts\Source\SourceFactoryInterface; use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\ParserExceptionInterface; @@ -13,6 +14,11 @@ final class InMemoryTypeParser implements TypeParserInterface { + /** + * @var non-empty-string + */ + private const HASH_ALGORITHM = 'xxh128'; + /** * @var array */ @@ -23,10 +29,14 @@ final class InMemoryTypeParser implements TypeParserInterface */ private array $sequences = []; + private readonly SourceFactoryInterface $sources; + public function __construct( private readonly TypeParserInterface $parser = new TypeParser(), - private readonly SourceFactoryInterface $sources = new SourceFactory(), - ) {} + ?SourceFactoryInterface $sources = null, + ) { + $this->sources = $sources ?? SourceFactory::createDefault(); + } /** * @throws ParserExceptionInterface @@ -37,7 +47,7 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode { $instance = $this->sources->create($source); - return $this->types[$instance->getHash()] ??= $this->parser->parse($source); + return $this->types[$this->hash($instance)] ??= $this->parser->parse($source); } /** @@ -49,6 +59,15 @@ public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult { $instance = $this->sources->create($source); - return $this->sequences[$instance->getHash()] ??= $this->parser->parseTolerant($source); + return $this->sequences[$this->hash($instance)] ??= $this->parser->parseTolerant($source); + } + + /** + * @return non-empty-string + * @throws SourceExceptionInterface in case of source content reading error + */ + private function hash(ReadableInterface $source): string + { + return \hash(self::HASH_ALGORITHM, $source->content); } } diff --git a/libs/parser/src/Internal/CompilerExecutor.php b/libs/parser/src/Internal/CompilerExecutor.php new file mode 100644 index 0000000..9c5c369 --- /dev/null +++ b/libs/parser/src/Internal/CompilerExecutor.php @@ -0,0 +1,2283 @@ + + */ +abstract class CompilerExecutor implements \Phplrt\Contracts\Parser\ParserInterface +{ + /** @var int */ + public const T_WHITESPACE = 0; + /** @var int */ + public const T_NEQ = 1; + /** @var int */ + public const T_EQ = 2; + /** @var int */ + public const T_BOOL_LITERAL = 3; + /** @var int */ + public const T_NULL_LITERAL = 4; + /** @var int */ + public const T_NAME_WITH_SPACE = 5; + /** @var int */ + public const T_NAME = 6; + /** @var int */ + public const T_NS_DELIMITER = 7; + /** @var int */ + public const T_OR = 8; + /** @var int */ + public const T_COMMA = 9; + /** @var int */ + public const T_DOUBLE_COLON = 10; + /** @var int */ + public const T_COLON = 11; + /** @var int */ + public const T_LTE = 12; + /** @var int */ + public const T_GTE = 13; + /** @var int */ + public const T_ANGLE_BRACKET_OPEN = 14; + /** @var int */ + public const T_ANGLE_BRACKET_CLOSE = 15; + /** @var int */ + public const T_QMARK = 16; + /** @var int */ + public const T_BRACE_OPEN = 17; + /** @var int */ + public const T_BRACE_CLOSE = 18; + /** @var int */ + public const T_PARENTHESIS_OPEN = 19; + /** @var int */ + public const T_PARENTHESIS_CLOSE = 20; + /** @var int */ + public const T_SQUARE_BRACKET_OPEN = 21; + /** @var int */ + public const T_SQUARE_BRACKET_CLOSE = 22; + /** @var int */ + public const T_AMP = 23; + /** @var int */ + public const T_ELLIPSIS = 24; + /** @var int */ + public const T_ASSIGN = 25; + /** @var int */ + public const T_ASTERISK = 26; + /** @var int */ + public const T_THIS = 27; + /** @var int */ + public const T_VARIABLE = 28; + /** @var int */ + public const T_DQ_STRING_LITERAL = 29; + /** @var int */ + public const T_SQ_STRING_LITERAL = 30; + /** @var int */ + public const T_PFX_FLOAT_LITERAL = 31; + /** @var int */ + public const T_SFX_FLOAT_LITERAL = 32; + /** @var int */ + public const T_EXP_LITERAL = 33; + /** @var int */ + public const T_BIN_INT_LITERAL = 34; + /** @var int */ + public const T_OCT_INT_LITERAL = 35; + /** @var int */ + public const T_HEX_INT_LITERAL = 36; + /** @var int */ + public const T_DEC_INT_LITERAL = 37; + /** @var int */ + public const T_COMMENT = 38; + /** @var int */ + public const T_DOC_COMMENT = 39; + + /** + * @var \Phplrt\Contracts\Parser\ParserInterface + */ + protected readonly \Phplrt\Contracts\Parser\ParserInterface $parser; + + protected readonly \Phplrt\Contracts\Lexer\LexerInterface $lexer; + + public function __construct() + { + $this->lexer = new \Phplrt\Lexer\Lexer( + pattern: '/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:(?i:is\\h++not)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:1))|(?:(?:(?i:is)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:2))|(?:(?:(?i:true|false)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:3))|(?:(?:(?i:null)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:4))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+\\s++)(*MARK:5))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:6))|(?:(?:\\\\)(*MARK:7))|(?:(?:\\|)(*MARK:8))|(?:(?:,)(*MARK:9))|(?:(?:::)(*MARK:10))|(?:(?::)(*MARK:11))|(?:(?:<=)(*MARK:12))|(?:(?:>=)(*MARK:13))|(?:(?:<)(*MARK:14))|(?:(?:>)(*MARK:15))|(?:(?:\\?)(*MARK:16))|(?:(?:\\{)(*MARK:17))|(?:(?:\\})(*MARK:18))|(?:(?:\\()(*MARK:19))|(?:(?:\\))(*MARK:20))|(?:(?:\\[)(*MARK:21))|(?:(?:\\])(*MARK:22))|(?:(?:&)(*MARK:23))|(?:(?:\\.\\.\\.)(*MARK:24))|(?:(?:=)(*MARK:25))|(?:(?:\\*)(*MARK:26))|(?:(?:\\$this\\b)(*MARK:27))|(?:(?:\\$(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:28))|(?:(?:"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+")(*MARK:29))|(?:(?:\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\')(*MARK:30))|(?:(?:-?(?:[0-9])++\\.(?:[0-9])*+(?:(?:[eE]-?(?:[0-9])++))?)(*MARK:31))|(?:(?:-?(?:[0-9])*+\\.(?:[0-9])++(?:(?:[eE]-?(?:[0-9])++))?)(*MARK:32))|(?:(?:-?(?:[0-9])++(?:[eE]-?(?:[0-9])++))(*MARK:33))|(?:(?:-?0[bB][01_]++)(*MARK:34))|(?:(?:-?0[oO][0-7_]++)(*MARK:35))|(?:(?:-?0[xX][0-9a-fA-F_]++)(*MARK:36))|(?:(?:-?(?:[0-9])[0-9_]*+)(*MARK:37))|(?:(?:(?:\\/\\/|\\#)[^\\r\\n]*+)(*MARK:38))|(?:(?:\\/\\*.*?\\*\\/)(*MARK:39))|(?:(?:[^\\s]++)(*MARK:40)))/Ssum', + channels: [ + 'Hidden', + 38 => 'Hidden', + 'Hidden', + 'Unknown', + ], + names: [ + 'T_WHITESPACE', + 'T_NEQ', + 'T_EQ', + 'T_BOOL_LITERAL', + 'T_NULL_LITERAL', + 'T_NAME_WITH_SPACE', + 'T_NAME', + 'T_NS_DELIMITER', + 'T_OR', + 'T_COMMA', + 'T_DOUBLE_COLON', + 'T_COLON', + 'T_LTE', + 'T_GTE', + 'T_ANGLE_BRACKET_OPEN', + 'T_ANGLE_BRACKET_CLOSE', + 'T_QMARK', + 'T_BRACE_OPEN', + 'T_BRACE_CLOSE', + 'T_PARENTHESIS_OPEN', + 'T_PARENTHESIS_CLOSE', + 'T_SQUARE_BRACKET_OPEN', + 'T_SQUARE_BRACKET_CLOSE', + 'T_AMP', + 'T_ELLIPSIS', + 'T_ASSIGN', + 'T_ASTERISK', + 'T_THIS', + 'T_VARIABLE', + 'T_DQ_STRING_LITERAL', + 'T_SQ_STRING_LITERAL', + 'T_PFX_FLOAT_LITERAL', + 'T_SFX_FLOAT_LITERAL', + 'T_EXP_LITERAL', + 'T_BIN_INT_LITERAL', + 'T_OCT_INT_LITERAL', + 'T_HEX_INT_LITERAL', + 'T_DEC_INT_LITERAL', + 'T_COMMENT', + 'T_DOC_COMMENT', + ], + ); + + $this->parser = new \Phplrt\Parser\Parser( + lexer: $this->lexer, + grammar: [ + new \Phplrt\Parser\Grammar\Alternation([1, 125]), + new \Phplrt\Parser\Grammar\Concatenation([2, 116]), + new \Phplrt\Parser\Grammar\Concatenation([3, 113]), + new \Phplrt\Parser\Grammar\Concatenation([4, 110]), + new \Phplrt\Parser\Grammar\Alternation([5, 7]), + new \Phplrt\Parser\Grammar\Concatenation([6, 7]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false), + new \Phplrt\Parser\Grammar\Concatenation([8, 105]), + new \Phplrt\Parser\Grammar\Alternation([9, 12, 13, 28]), + new \Phplrt\Parser\Grammar\Concatenation([10, 0, 11]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false), + new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), + new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), + new \Phplrt\Parser\Grammar\Alternation([14, 17, 21, 26, 27]), + new \Phplrt\Parser\Grammar\Alternation([15, 16]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DQ_STRING_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SQ_STRING_LITERAL, true), + new \Phplrt\Parser\Grammar\Alternation([18, 19, 20]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_PFX_FLOAT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SFX_FLOAT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_EXP_LITERAL, true), + new \Phplrt\Parser\Grammar\Alternation([22, 23, 24, 25]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BIN_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_OCT_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_HEX_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DEC_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BOOL_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), + new \Phplrt\Parser\Grammar\Concatenation([29, 41]), + new \Phplrt\Parser\Grammar\Alternation([30, 40]), + new \Phplrt\Parser\Grammar\Concatenation([31, 32, 38]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), + new \Phplrt\Parser\Grammar\Alternation([33, 34, 35, 36, 37]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BOOL_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), + new \Phplrt\Parser\Grammar\Repetition(39, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([31, 32]), + new \Phplrt\Parser\Grammar\Concatenation([32, 38]), + new \Phplrt\Parser\Grammar\Optional(42), + new \Phplrt\Parser\Grammar\Alternation([43, 44, 50, 71, 80]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), + new \Phplrt\Parser\Grammar\Concatenation([45, 46]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, false), + new \Phplrt\Parser\Grammar\Alternation([47, 49]), + new \Phplrt\Parser\Grammar\Concatenation([32, 48]), + new \Phplrt\Parser\Grammar\Optional(49), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), + new \Phplrt\Parser\Grammar\Concatenation([10, 51, 11, 68]), + new \Phplrt\Parser\Grammar\Optional(52), + new \Phplrt\Parser\Grammar\Concatenation([53, 64, 67]), + new \Phplrt\Parser\Grammar\Concatenation([0, 54, 56, 58, 62]), + new \Phplrt\Parser\Grammar\Optional(55), + new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true), + new \Phplrt\Parser\Grammar\Optional(57), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true), + new \Phplrt\Parser\Grammar\Optional(59), + new \Phplrt\Parser\Grammar\Alternation([60, 61]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), + new \Phplrt\Parser\Grammar\Optional(63), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true), + new \Phplrt\Parser\Grammar\Repetition(65, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([66, 53]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false), + new \Phplrt\Parser\Grammar\Optional(66), + new \Phplrt\Parser\Grammar\Optional(69), + new \Phplrt\Parser\Grammar\Concatenation([70, 0]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), + new \Phplrt\Parser\Grammar\Concatenation([72, 73, 77, 67, 79]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false), + new \Phplrt\Parser\Grammar\Alternation([74, 76]), + new \Phplrt\Parser\Grammar\Concatenation([75, 0]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Repetition(78, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([66, 73]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), + new \Phplrt\Parser\Grammar\Concatenation([81, 82, 67, 104]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false), + new \Phplrt\Parser\Grammar\Alternation([83, 103]), + new \Phplrt\Parser\Grammar\Concatenation([84, 99]), + new \Phplrt\Parser\Grammar\Concatenation([85, 97]), + new \Phplrt\Parser\Grammar\Alternation([86, 96]), + new \Phplrt\Parser\Grammar\Concatenation([87, 94, 70, 0]), + new \Phplrt\Parser\Grammar\Alternation([88, 92, 21, 14]), + new \Phplrt\Parser\Grammar\Concatenation([32, 89]), + new \Phplrt\Parser\Grammar\Predicate(90, true), + new \Phplrt\Parser\Grammar\Concatenation([91, 70]), + new \Phplrt\Parser\Grammar\Optional(6), + new \Phplrt\Parser\Grammar\Concatenation([29, 93]), + new \Phplrt\Parser\Grammar\Alternation([43, 44]), + new \Phplrt\Parser\Grammar\Optional(95), + new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Repetition(98, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([66, 85]), + new \Phplrt\Parser\Grammar\Optional(100), + new \Phplrt\Parser\Grammar\Concatenation([66, 101]), + new \Phplrt\Parser\Grammar\Concatenation([57, 102]), + new \Phplrt\Parser\Grammar\Optional(71), + new \Phplrt\Parser\Grammar\Optional(101), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false), + new \Phplrt\Parser\Grammar\Repetition(106, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([107, 108, 109]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false), + new \Phplrt\Parser\Grammar\Optional(0), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false), + new \Phplrt\Parser\Grammar\Repetition(111, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([112, 4]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, false), + new \Phplrt\Parser\Grammar\Repetition(114, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([115, 3]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, false), + new \Phplrt\Parser\Grammar\Optional(117), + new \Phplrt\Parser\Grammar\Concatenation([118, 124, 6, 0, 70, 0]), + new \Phplrt\Parser\Grammar\Alternation([35, 119, 120, 121, 122, 123]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NEQ, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_GTE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_LTE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, true), + new \Phplrt\Parser\Grammar\Alternation([0, 59]), + new \Phplrt\Parser\Grammar\Concatenation([59, 117]), + ], + initial: 0, + reducers: [ + 0 => $this->reduceType(...), + 2 => $this->reduceUnionType(...), + 3 => $this->reduceIntersectionType(...), + 5 => self::reduceNullableType(...), + 7 => $this->reduceTypesList(...), + 12 => self::reduceThisLiteral(...), + 13 => $this->reduceLiteral(...), + 15 => $this->reduceDoubleQuotedStringLiteral(...), + 16 => $this->reduceSingleQuotedStringLiteral(...), + 17 => self::reduceFloatLiteral(...), + 21 => $this->reduceIntLiteral(...), + 26 => $this->reduceBoolLiteral(...), + 27 => self::reduceNullLiteral(...), + 28 => $this->reduceNamedType(...), + 30 => self::reduceFullQualifiedName(...), + 32 => self::reduceIdentifier(...), + 40 => self::reduceRelativeName(...), + 43 => self::reduceConstMaskSuffix(...), + 49 => self::reduceClassConstMaskSuffix(...), + 51 => self::reduceCallableParameters(...), + 53 => self::reduceCallableParameter(...), + 59 => self::reduceVariableLiteral(...), + 71 => $this->reduceTemplateArguments(...), + 73 => $this->reduceTemplateArgument(...), + 75 => self::reduceIdentifierWithExtraSpace(...), + 80 => $this->reduceShapeFields(...), + 84 => self::reduceShapeFieldsList(...), + 86 => self::reduceExplicitField(...), + 92 => self::reduceShapeConstantKey(...), + 96 => self::reduceImplicitField(...), + 106 => self::reduceTypeListOrOffsetSuffix(...), + ], + lookahead: [ + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + 29 => true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + 29 => true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + 29 => true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + 29 => true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 16 => true, + ], + [ + 16 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 19 => true, + 27 => true, + 29 => true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 19 => true, + 27 => true, + 29 => true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 19 => true, + ], + [ + 19 => true, + ], + [ + 20 => true, + ], + [ + 27 => true, + ], + [ + 3 => true, + true, + 29 => true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 29 => true, + true, + ], + [ + 29 => true, + ], + [ + 30 => true, + ], + [ + 31 => true, + true, + true, + ], + [ + 31 => true, + ], + [ + 32 => true, + ], + [ + 33 => true, + ], + [ + 34 => true, + true, + true, + true, + ], + [ + 34 => true, + ], + [ + 35 => true, + ], + [ + 36 => true, + ], + [ + 37 => true, + ], + [ + 3 => true, + ], + [ + 4 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + ], + [ + 7 => true, + ], + [ + 7 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + ], + [ + 6 => true, + ], + [ + 5 => true, + ], + [ + 2 => true, + ], + [ + 3 => true, + ], + [ + 4 => true, + ], + null, + [ + 7 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + ], + null, + [ + 10 => true, + 14 => true, + 17 => true, + 19 => true, + 26 => true, + ], + [ + 26 => true, + ], + [ + 10 => true, + ], + [ + 10 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + 26 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + ], + null, + [ + 26 => true, + ], + [ + 19 => true, + ], + null, + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 23 => true, + ], + null, + [ + 24 => true, + ], + null, + [ + 27 => true, + true, + ], + [ + 28 => true, + ], + [ + 27 => true, + ], + null, + [ + 25 => true, + ], + null, + [ + 9 => true, + ], + [ + 9 => true, + ], + null, + null, + [ + 11 => true, + ], + [ + 11 => true, + ], + [ + 14 => true, + ], + [ + 14 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 5 => true, + ], + [ + 5 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 9 => true, + ], + [ + 15 => true, + ], + [ + 17 => true, + ], + [ + 17 => true, + ], + null, + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 29 => true, + true, + 34 => true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 29 => true, + true, + 34 => true, + true, + true, + true, + ], + [ + 2 => true, + true, + true, + true, + true, + ], + null, + [ + 11 => true, + 16 => true, + ], + null, + [ + 2 => true, + true, + true, + true, + true, + true, + ], + [ + 10 => true, + 26 => true, + ], + null, + [ + 16 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 9 => true, + ], + null, + [ + 9 => true, + ], + [ + 24 => true, + ], + null, + null, + [ + 18 => true, + ], + null, + [ + 21 => true, + ], + [ + 21 => true, + ], + null, + [ + 22 => true, + ], + null, + [ + 23 => true, + ], + [ + 23 => true, + ], + null, + [ + 8 => true, + ], + [ + 8 => true, + ], + null, + [ + 1 => true, + true, + 12 => true, + true, + true, + true, + ], + [ + 1 => true, + true, + 12 => true, + true, + true, + true, + ], + [ + 1 => true, + ], + [ + 13 => true, + ], + [ + 12 => true, + ], + [ + 14 => true, + ], + [ + 15 => true, + ], + [ + 2 => true, + true, + true, + true, + true, + true, + 16 => true, + 19 => true, + 27 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 27 => true, + true, + ], + ], + kept: [ + true, + true, + true, + true, + false, + true, + false, + true, + false, + false, + false, + false, + true, + true, + false, + true, + true, + true, + false, + false, + false, + true, + false, + false, + false, + false, + true, + true, + true, + false, + true, + false, + true, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, + false, + false, + true, + false, + true, + true, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + true, + true, + true, + true, + false, + false, + false, + true, + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + ], + choicePrediction: [ + [ + 2 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + 16 => [ + 1, + ], + 19 => [ + 1, + ], + 29 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + 28 => [ + 125, + ], + ], + 4 => [ + 16 => [ + 5, + ], + 2 => [ + 7, + ], + 3 => [ + 7, + ], + 4 => [ + 7, + ], + 5 => [ + 7, + ], + 6 => [ + 7, + ], + 7 => [ + 7, + ], + 19 => [ + 7, + ], + 27 => [ + 7, + ], + 29 => [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + ], + 8 => [ + 19 => [ + 9, + ], + 27 => [ + 12, + ], + 3 => [ + 13, + 28, + ], + 4 => [ + 13, + 28, + ], + 29 => [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + 2 => [ + 28, + ], + 5 => [ + 28, + ], + 6 => [ + 28, + ], + 7 => [ + 28, + ], + ], + 13 => [ + 29 => [ + 14, + ], + [ + 14, + ], + [ + 17, + ], + [ + 17, + ], + [ + 17, + ], + [ + 21, + ], + [ + 21, + ], + [ + 21, + ], + [ + 21, + ], + 3 => [ + 26, + ], + 4 => [ + 27, + ], + ], + [ + 29 => [ + 15, + ], + [ + 16, + ], + ], + 17 => [ + 31 => [ + 18, + ], + [ + 19, + ], + [ + 20, + ], + ], + 21 => [ + 34 => [ + 22, + ], + [ + 23, + ], + [ + 24, + ], + [ + 25, + ], + ], + 29 => [ + 7 => [ + 30, + ], + 2 => [ + 40, + ], + 3 => [ + 40, + ], + 4 => [ + 40, + ], + 5 => [ + 40, + ], + 6 => [ + 40, + ], + ], + 32 => [ + 6 => [ + 33, + ], + 5 => [ + 34, + ], + 2 => [ + 35, + ], + 3 => [ + 36, + ], + 4 => [ + 37, + ], + ], + 42 => [ + 26 => [ + 43, + ], + 10 => [ + 44, + ], + 19 => [ + 50, + ], + 14 => [ + 71, + ], + 17 => [ + 80, + ], + ], + 46 => [ + 2 => [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + 26 => [ + 49, + ], + ], + 59 => [ + 28 => [ + 60, + ], + 27 => [ + 61, + ], + ], + 73 => [ + 2 => [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + 6 => [ + 76, + ], + [ + 76, + ], + 16 => [ + 76, + ], + 19 => [ + 76, + ], + 27 => [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + [ + 76, + ], + ], + 85 => [ + 16 => [ + 96, + ], + 19 => [ + 96, + ], + 27 => [ + 96, + ], + [ + 96, + ], + 31 => [ + 96, + ], + [ + 96, + ], + [ + 96, + ], + ], + 87 => [ + 2 => [ + 88, + 92, + ], + [ + 88, + 92, + ], + [ + 88, + 92, + ], + [ + 88, + 92, + ], + [ + 88, + 92, + ], + [ + 92, + ], + 34 => [ + 21, + ], + [ + 21, + ], + [ + 21, + ], + [ + 21, + ], + 29 => [ + 14, + ], + 30 => [ + 14, + ], + ], + 93 => [ + 26 => [ + 43, + ], + 10 => [ + 44, + ], + ], + 118 => [ + 2 => [ + 35, + ], + 1 => [ + 119, + ], + 13 => [ + 120, + ], + 12 => [ + 121, + ], + [ + 122, + ], + [ + 123, + ], + ], + 124 => [ + 2 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + 16 => [ + 0, + ], + 19 => [ + 0, + ], + 29 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + ], + ], + expectations: [ + 'T_WHITESPACE', + 'T_NEQ', + 'T_EQ', + 'T_BOOL_LITERAL', + 'T_NULL_LITERAL', + 'T_NAME_WITH_SPACE', + 'T_NAME', + 'T_NS_DELIMITER', + 'T_OR', + 'T_COMMA', + 'T_DOUBLE_COLON', + 'T_COLON', + 'T_LTE', + 'T_GTE', + 'T_ANGLE_BRACKET_OPEN', + 'T_ANGLE_BRACKET_CLOSE', + 'T_QMARK', + 'T_BRACE_OPEN', + 'T_BRACE_CLOSE', + 'T_PARENTHESIS_OPEN', + 'T_PARENTHESIS_CLOSE', + 'T_SQUARE_BRACKET_OPEN', + 'T_SQUARE_BRACKET_CLOSE', + 'T_AMP', + 'T_ELLIPSIS', + 'T_ASSIGN', + 'T_ASTERISK', + 'T_THIS', + 'T_VARIABLE', + 'T_DQ_STRING_LITERAL', + 'T_SQ_STRING_LITERAL', + 'T_PFX_FLOAT_LITERAL', + 'T_SFX_FLOAT_LITERAL', + 'T_EXP_LITERAL', + 'T_BIN_INT_LITERAL', + 'T_OCT_INT_LITERAL', + 'T_HEX_INT_LITERAL', + 'T_DEC_INT_LITERAL', + 'T_COMMENT', + 'T_DOC_COMMENT', + '/[^\\s]++/', + ], + ); + } + + public function parse(\Phplrt\Contracts\Source\ReadableInterface $source): mixed + { + return $this->parser->parse($source); + } + + private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + if (\count($children) === 1) { + return $children[0]; + } + + if ($this->features->conditions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); + } + + $condition = match ($children[1]->id) { + self::T_EQ => new Type\Condition\EqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_NEQ => new Type\Condition\NotEqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_GTE => new Type\Condition\GreaterThanOrEqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_ANGLE_BRACKET_CLOSE => new Type\Condition\GreaterThanConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_LTE => new Type\Condition\LessThanOrEqualConditionNode( + $children[0], + $children[2], + $offset, + ), + self::T_ANGLE_BRACKET_OPEN => new Type\Condition\LessThanConditionNode( + $children[0], + $children[2], + $offset, + ), + default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( + $children[1]->value, + $offset, + ), + }; + + return new Type\TernaryExpressionNode( + $condition, + $children[3], + $children[4], + $offset, + ); + } + + private function reduceUnionType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + if (\count($children) === 1) { + return $children[0]; + } + + if ($this->features->unions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); + } + + // A logical statement takes an arbitrary number of types, so its + // offset is assigned rather than passed to the constructor + $statement = new Type\UnionTypeNode($children); + $statement->offset = $offset; + + return $statement; + } + + private function reduceIntersectionType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + if (\count($children) === 1) { + return $children[0]; + } + + if ($this->features->intersections === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); + } + + // A logical statement takes an arbitrary number of types, so its + // offset is assigned rather than passed to the constructor + $statement = new Type\IntersectionTypeNode($children); + $statement->offset = $offset; + + return $statement; + } + + private static function reduceNullableType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\NullableTypeNode($children[0], $offset); + } + + private function reduceTypesList(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $statement = $children[0]; + $count = \count($children); + + for ($i = 1; $i < $count; ++$i) { + $child = $children[$i]; + + switch (true) { + // In case of list type + case $child === true: + if ($this->features->lists === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); + } + + $statement = new Type\TypesListNode($statement, $offset); + break; + // In case of offset access type + case $child instanceof Type\TypeNode: + if ($this->features->offsets === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); + } + + $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset); + break; + default: + throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( + \get_debug_type($child), + $offset, + ); + } + } + + return $statement; + } + + private static function reduceThisLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); + } + + private function reduceLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + if ($this->features->literals === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); + } + + return $children; + } + + private function reduceDoubleQuotedStringLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return $this->createDoubleQuotedString($children->value, $offset); + } + + private function reduceSingleQuotedStringLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return $this->createSingleQuotedString($children->value, $offset); + } + + private static function reduceFloatLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\FloatLiteralNode((float) $children->value, $children->value, $offset); + } + + private function reduceIntLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return $this->createInt($children->value, $offset); + } + + private function reduceBoolLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return $this->createBool($children->value, $offset); + } + + private static function reduceNullLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\NullLiteralNode($children->value, $offset); + } + + private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $suffix = $children[1] ?? null; + + switch (true) { + // Some\Any + case $suffix instanceof Type\Template\TemplateArgumentListNode: + return new Type\NamedTypeNode($children[0], $suffix, null, $offset); + + // Some\Any{name: T, ...} + case $suffix instanceof Type\Shape\FieldsListNode: + return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); + + // Some\Any(T, U): V + case $suffix instanceof Type\Callable\CallableParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); + } + + return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, $offset); + + // Some\Any::CONST and Some\Any::CONST_* + case $suffix instanceof Type\Identifier: + if (isset($children[2])) { + return new Type\ClassConstMaskNode($children[0], $suffix, $offset); + } + + return new Type\ClassConstNode($children[0], $suffix, $offset); + + // Some\Any::* + case $suffix === true: + return new Type\ClassConstMaskNode($children[0], offset: $offset); + + // SOME_ANY_* + case $suffix === false: + return new Type\ConstMaskNode($children[0], $offset); + + // Some\Any + default: + return new Type\NamedTypeNode($children[0], null, null, $offset); + } + } + + private static function reduceFullQualifiedName(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Name($children, true, $offset); + } + + private static function reduceIdentifier(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + // Only a name written with a trailing space carries one, so the value of + // every other token is the identifier itself + return new Type\Identifier( + $children->id === self::T_NAME_WITH_SPACE + ? \rtrim($children->value) + : $children->value, + $offset, + ); + } + + private static function reduceRelativeName(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Name($children, false, $offset); + } + + private static function reduceConstMaskSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return false; + } + + private static function reduceClassConstMaskSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return true; + } + + private static function reduceCallableParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Callable\CallableParameterListNode($children, $offset); + } + + private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $parameter = new Type\Callable\CallableParameterNode($children[0], offset: $offset); + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + $child = $children[$i]; + + if ($child instanceof Type\Literal\VariableLiteralNode) { + $parameter->name = $child; + + continue; + } + + switch ($child->id) { + case self::T_AMP: + $parameter->isOutput = true; + break; + + case self::T_ELLIPSIS: + $parameter->isVariadic = true; + break; + + // A variadic parameter is optional in itself, so a default marker + // beside one describes the same parameter in two different ways + default: + if ($parameter->isVariadic) { + throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); + } + + $parameter->isOptional = true; + } + } + + return $parameter; + } + + private static function reduceVariableLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); + } + + private function reduceTemplateArguments(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + if ($this->features->generics === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset); + } + + return new Type\Template\TemplateArgumentListNode($children, $offset); + } + + private function reduceTemplateArgument(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $count = \count($children); + $hint = null; + + // An "in T" or an "out T" argument puts a hint in front of the type + if ($count === 2) { + if ($this->features->hints === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); + } + + $hint = $children[0]; + } + + return new Type\Template\TemplateArgumentNode( + $children[$count - 1], + $hint, + $offset, + ); + } + + private static function reduceIdentifierWithExtraSpace(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Identifier(\rtrim($children->value), $offset); + } + + private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $count = \count($children); + + // An empty shape carries no fields and needs no support for them + if ($count === 0) { + return [new Type\Shape\FieldsListNode(offset: $offset)]; + } + + if ($this->features->shapes === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); + } + + $parameters = null; + + // The "..." suffix, in case of the shape is unsealed by a typed rest + if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) { + $parameters = $children[$count - 1]; + --$count; + } + + if ($children[0] instanceof Type\Shape\FieldsListNode) { + $fields = $children[0]; + // Whatever is left beside the fields is the "..." of an unsealed shape + $unsealed = $count > 1; + } else { + $fields = new Type\Shape\FieldsListNode(offset: $offset); + $unsealed = $count > 0; + } + + if ($unsealed) { + $fields->isSealed = false; + } + + if ($parameters === null) { + return [$fields]; + } + + return [$fields, $parameters]; + } + + private static function reduceShapeFieldsList(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $explicit = []; + $implicit = false; + + foreach ($children as $field) { + if ($field instanceof Type\Shape\ExplicitFieldNode) { + $key = $field->getIndex(); + + if (isset($explicit[$key])) { + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); + } + + $explicit[$key] = true; + } else { + $implicit = true; + } + } + + if ($explicit !== [] && $implicit) { + throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); + } + + return new Type\Shape\FieldsListNode($children, offset: $offset); + } + + private static function reduceExplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $count = \count($children); + $name = $children[0]; + $value = $children[$count - 1]; + + // In case of "nullable" suffix defined + $optional = $count === 3; + + return match (true) { + $name instanceof Type\Literal\IntLiteralNode + => new Type\Shape\NumericFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\Literal\StringLiteralNode + => new Type\Shape\StringNamedFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\ClassConstNode + => new Type\Shape\ClassConstFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\ClassConstMaskNode + => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional, offset: $offset), + $name instanceof Type\ConstMaskNode + => new Type\Shape\ConstMaskFieldNode($name, $value, $optional, offset: $offset), + default => new Type\Shape\NamedFieldNode($name, $value, $optional, offset: $offset), + }; + } + + private static function reduceShapeConstantKey(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $suffix = $children[1]; + + switch (true) { + // Some\Any::CONST and Some\Any::CONST_* + case $suffix instanceof Type\Identifier: + if (isset($children[2])) { + return new Type\ClassConstMaskNode($children[0], $suffix, $offset); + } + + return new Type\ClassConstNode($children[0], $suffix, $offset); + + // Some\Any::* + case $suffix === true: + return new Type\ClassConstMaskNode($children[0], offset: $offset); + + // SOME_ANY_* + default: + return new Type\ConstMaskNode($children[0], $offset); + } + } + + private static function reduceImplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Shape\ImplicitFieldNode($children[0], offset: $offset); + } + + private static function reduceTypeListOrOffsetSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return $children[0] ?? true; + } +} diff --git a/libs/parser/src/Internal/ExecutionContext.php b/libs/parser/src/Internal/ExecutionContext.php deleted file mode 100644 index 46649f1..0000000 --- a/libs/parser/src/Internal/ExecutionContext.php +++ /dev/null @@ -1,168 +0,0 @@ -, - * ... - * }, - * skip: list, - * grammar: array, - * reducers: array|non-empty-string, callable(Context, mixed): mixed>, - * transitions?: array - * } - * @phpstan-type ParserConfigArrayType array - * - * @internal this is an internal library class, please do not use it in your code - * @psalm-internal TypeLang\Parser - */ -final class ExecutionContext -{ - /** - * @var ParserRuntime - */ - private readonly ParserRuntime $parser; - - private readonly LexerRuntime $lexer; - - /** - * In-memory string literal pool. - * - * @api this property is accessible inside the grammar reducers - * - * @var \WeakMap - */ - protected readonly \WeakMap $stringPool; - - /** - * In-memory integer literal pool. - * - * @api this property is accessible inside the grammar reducers - * - * @var \WeakMap - */ - protected readonly \WeakMap $integerPool; - - public function __construct( - /** - * @api this property is accessible inside the grammar reducers - */ - protected readonly TypeParserFeatures $features, - /** - * @api this property is accessible inside the grammar reducers - */ - protected readonly bool $tolerant, - ) { - /** @phpstan-var GrammarConfigArrayType $grammar */ - $grammar = require __DIR__ . '/../../resources/grammar.php'; - - $this->stringPool = new \WeakMap(); - $this->integerPool = new \WeakMap(); - - $this->lexer = $this->createLexer($grammar); - $this->parser = $this->createParser($this->lexer, $grammar); - } - - /** - * @param array|non-empty-string, callable(Context, mixed):mixed> $reducers - */ - private function createBuilder(array $reducers): BuilderInterface - { - return new NodeBuilder($reducers); - } - - /** - * @param GrammarConfigArrayType $grammar - * @return ParserRuntime - */ - private function createParser(LexerInterface $lexer, array $grammar): ParserRuntime - { - /** @var ParserRuntime */ - return new ParserRuntime( - lexer: $lexer, - grammar: $grammar['grammar'], - options: $this->createParserOptions($grammar), - ); - } - - /** - * @param GrammarConfigArrayType $grammar - * @return ParserConfigArrayType - */ - private function createParserOptions(array $grammar): array - { - return [ - ParserConfigsInterface::CONFIG_INITIAL_RULE => $grammar['initial'], - ParserConfigsInterface::CONFIG_ALLOW_TRAILING_TOKENS => $this->tolerant, - ParserConfigsInterface::CONFIG_AST_BUILDER => $this->createBuilder($grammar['reducers']), - ]; - } - - /** - * @param GrammarConfigArrayType $grammar - */ - private function createLexer(array $grammar): LexerRuntime - { - return new LexerRuntime( - tokens: $grammar['tokens']['default'], - skip: $grammar['skip'], - onUnknownToken: new PassthroughHandler(), - ); - } - - /** - * @return iterable - * @throws LexerExceptionInterface - * @throws LexerRuntimeExceptionInterface - */ - public function lex(ReadableInterface $source): iterable - { - return $this->lexer->lex($source); - } - - /** - * @throws ParserRuntimeExceptionInterface - * @throws ParserExceptionInterface - */ - public function parse(ReadableInterface $source): ?ParsedResult - { - foreach ($this->parser->parse($source) as $stmt) { - $offset = 0; - $context = $this->parser->getLastExecutionContext(); - - if ($context !== null) { - $token = $context->buffer->current(); - $offset = $token->getOffset(); - } - - return new ParsedResult($stmt, $offset); - } - - return null; - } -} diff --git a/libs/parser/src/Internal/Executor.php b/libs/parser/src/Internal/Executor.php new file mode 100644 index 0000000..9a4f974 --- /dev/null +++ b/libs/parser/src/Internal/Executor.php @@ -0,0 +1,163 @@ + + */ +final class Executor extends CompilerExecutor +{ + public function __construct( + /** + * @api this property is accessible inside the grammar reducers + */ + protected readonly TypeParserFeatures $features, + ) { + parent::__construct(); + } + + /** + * Reads as much of the given source as the grammar describes and reports + * what it has been made of. + * + * @throws ParserExceptionInterface in case of the parser cannot be built + * @throws RuntimeExceptionInterface in case of the source cannot be read + * @throws \Throwable in case of an internal error occurs + */ + public function analyze( + ReadableInterface $source, + Mode $mode = Mode::Tolerant, + ): SuccessfulResult|FailureResult { + $parser = $this->parser; + + if (!$parser instanceof ParserRuntime) { + throw new \LogicException('The compiled grammar must be built on top of the phplrt parser'); + } + + return $parser->analyze($source, $mode); + } + + /** + * @param non-empty-string $value + * @param int<0, max> $offset + * @throws \InvalidArgumentException in case of value parsing error occurs + */ + protected function createDoubleQuotedString(string $value, int $offset = 0): StringLiteralNode + { + if (\strlen($value) < 2) { + throw new \InvalidArgumentException('Could not parse non-quoted string'); + } + + return new StringLiteralNode( + value: StringDecoder::decode(\substr($value, 1, -1)), + raw: $value, + offset: $offset, + ); + } + + /** + * @param non-empty-string $value + * @param int<0, max> $offset + * @throws \InvalidArgumentException in case of value parsing error occurs + */ + protected function createSingleQuotedString(string $value, int $offset = 0): StringLiteralNode + { + if (\strlen($value) < 2) { + throw new \InvalidArgumentException('Could not parse non-quoted string'); + } + + return new StringLiteralNode( + value: StringDecoder::unescape(\substr($value, 1, -1)), + raw: $value, + offset: $offset, + ); + } + + /** + * Parse raw integer literal string value: Decimal, hexadecimal + * (like a "0xFF"), octal (like a "0o17" or a "017") and binary + * (like a "0b1010") ones. + * + * @param int<0, max> $offset + */ + protected function createInt(string $value, int $offset = 0): IntLiteralNode + { + [$negative, $decimal] = self::split($value); + + $inverse = '-' . $decimal; + + if ($negative) { + if ((string) \PHP_INT_MIN === $inverse) { + return new IntLiteralNode(\PHP_INT_MIN, $value, $inverse, $offset); + } + + /** @phpstan-ignore-next-line : An "$inverse" variable contain numeric-string */ + return new IntLiteralNode((int) $inverse, $value, $inverse, $offset); + } + + return new IntLiteralNode((int) $decimal, $value, $decimal, $offset); + } + + /** + * @return array{bool, numeric-string} + */ + private static function split(string $literal): array + { + $literal = \str_replace('_', '', $literal); + + if ($negative = ($literal[0] === '-')) { + $literal = \substr($literal, 1); + } + + // One of: [ 0123, 0o23, 0x00, 0b01 ] + if ($literal[0] === '0' && isset($literal[1])) { + /** @var array{bool, numeric-string} */ + return [$negative, match ($literal[1]) { + // hexadecimal + 'x', 'X' => \base_convert(\substr($literal, 2), 16, 10), + // binary + 'b', 'B' => \base_convert(\substr($literal, 2), 2, 10), + // octal + 'o', 'O' => \base_convert(\substr($literal, 2), 8, 10), + // octal (legacy) + default => \base_convert($literal, 8, 10), + }]; + } + + /** @var array{bool, numeric-string} */ + return [$negative, $literal]; + } + + /** + * @param int<0, max> $offset + */ + protected function createBool(string $value, int $offset = 0): BoolLiteralNode + { + return new BoolLiteralNode( + \strtolower($value) === 'true', + $value, + $offset, + ); + } +} diff --git a/libs/parser/src/Internal/NodeBuilder.php b/libs/parser/src/Internal/NodeBuilder.php deleted file mode 100644 index f5dfcf2..0000000 --- a/libs/parser/src/Internal/NodeBuilder.php +++ /dev/null @@ -1,38 +0,0 @@ -|non-empty-string, callable(Context, mixed):mixed> $reducers - */ - public function __construct( - private readonly array $reducers, - ) {} - - public function build(Context $context, mixed $result): mixed - { - if (!isset($this->reducers[$context->state])) { - return $result; - } - - $result = ($this->reducers[$context->state])($context, $result); - - if ($result instanceof Node && $result->offset === 0) { - $result->offset = $context->lastProcessedToken->getOffset(); - } - - return $result; - } -} diff --git a/libs/parser/src/Internal/StringParser.php b/libs/parser/src/Internal/StringDecoder.php similarity index 79% rename from libs/parser/src/Internal/StringParser.php rename to libs/parser/src/Internal/StringDecoder.php index 54f445a..1775bb8 100644 --- a/libs/parser/src/Internal/StringParser.php +++ b/libs/parser/src/Internal/StringDecoder.php @@ -4,7 +4,11 @@ namespace TypeLang\Parser\Internal; -final class StringParser +use TypeLang\Parser\Internal\StringDecoder\PatternSequenceFetcher; +use TypeLang\Parser\Internal\StringDecoder\ScannerSequenceFetcher; +use TypeLang\Parser\Internal\StringDecoder\SequenceFetcherInterface; + +final class StringDecoder { /** * @var non-empty-string @@ -19,6 +23,11 @@ final class StringParser */ private const SCANNER_THRESHOLD = 4; + public static function unescape(string $value): string + { + return \strtr($value, ["\'" => "'", '\\\\' => '\\']); + } + /** * Method for parsing and decode all escaped character sequences: Special * chars (like a "\n"), hexadecimal (like a "\xFF"), octal (like a "\101") @@ -26,7 +35,7 @@ final class StringParser * * @link https://www.php.net/manual/en/language.types.string.php */ - public static function parse(string $body): string + public static function decode(string $body): string { if (!\str_contains($body, '\\')) { return $body; diff --git a/libs/parser/src/Internal/PatternSequenceFetcher.php b/libs/parser/src/Internal/StringDecoder/PatternSequenceFetcher.php similarity index 97% rename from libs/parser/src/Internal/PatternSequenceFetcher.php rename to libs/parser/src/Internal/StringDecoder/PatternSequenceFetcher.php index 9128870..3bfca9d 100644 --- a/libs/parser/src/Internal/PatternSequenceFetcher.php +++ b/libs/parser/src/Internal/StringDecoder/PatternSequenceFetcher.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace TypeLang\Parser\Internal; +namespace TypeLang\Parser\Internal\StringDecoder; /** * Fetches sequences by a single regexp pass over the whole string. diff --git a/libs/parser/src/Internal/ScannerSequenceFetcher.php b/libs/parser/src/Internal/StringDecoder/ScannerSequenceFetcher.php similarity index 95% rename from libs/parser/src/Internal/ScannerSequenceFetcher.php rename to libs/parser/src/Internal/StringDecoder/ScannerSequenceFetcher.php index 11f4d0b..a01f1c4 100644 --- a/libs/parser/src/Internal/ScannerSequenceFetcher.php +++ b/libs/parser/src/Internal/StringDecoder/ScannerSequenceFetcher.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace TypeLang\Parser\Internal; +namespace TypeLang\Parser\Internal\StringDecoder; /** - * Fetches sequences by a hand written scanner: Each escaped char is visited + * Fetches sequences by a handwritten scanner: Each escaped char is visited * only once, so the string is not traversed as a whole. */ final class ScannerSequenceFetcher implements SequenceFetcherInterface diff --git a/libs/parser/src/Internal/SequenceFetcherInterface.php b/libs/parser/src/Internal/StringDecoder/SequenceFetcherInterface.php similarity index 92% rename from libs/parser/src/Internal/SequenceFetcherInterface.php rename to libs/parser/src/Internal/StringDecoder/SequenceFetcherInterface.php index 03af20b..1d5a439 100644 --- a/libs/parser/src/Internal/SequenceFetcherInterface.php +++ b/libs/parser/src/Internal/StringDecoder/SequenceFetcherInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace TypeLang\Parser\Internal; +namespace TypeLang\Parser\Internal\StringDecoder; interface SequenceFetcherInterface { @@ -19,7 +19,7 @@ interface SequenceFetcherInterface '\e' => "\e", '\f' => "\f", '\$' => '$', - '\"' => '"', + '\\"' => '"', '\\\\' => '\\', ]; diff --git a/libs/parser/src/Internal/UtfCharRenderer.php b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php similarity index 88% rename from libs/parser/src/Internal/UtfCharRenderer.php rename to libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php index ac82a9f..33c6fb6 100644 --- a/libs/parser/src/Internal/UtfCharRenderer.php +++ b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace TypeLang\Parser\Internal; +namespace TypeLang\Parser\Internal\StringDecoder; final class UtfCharRenderer { @@ -19,8 +19,8 @@ final class UtfCharRenderer /** * Method for encoding an utf-8 character by its code. * - * Codes above the last unicode code point are encoded as a replacement - * char, because such a sequence cannot be expressed in utf-8. + * Codes above the last Unicode code point are encoded as a replacement + * char because such a sequence cannot be expressed in utf-8. * * @param int<0, max> $code */ diff --git a/libs/parser/src/Literal/IntLiteralParser.php b/libs/parser/src/Literal/IntLiteralParser.php deleted file mode 100644 index d919b32..0000000 --- a/libs/parser/src/Literal/IntLiteralParser.php +++ /dev/null @@ -1,63 +0,0 @@ - \base_convert(\substr($literal, 2), 16, 10), - // binary - 'b', 'B' => \base_convert(\substr($literal, 2), 2, 10), - // octal - 'o', 'O' => \base_convert(\substr($literal, 2), 8, 10), - // octal (legacy) - default => \base_convert($literal, 8, 10), - }]; - } - - /** @var array{bool, numeric-string} */ - return [$negative, $literal]; - } -} diff --git a/libs/parser/src/Literal/StringLiteralParser.php b/libs/parser/src/Literal/StringLiteralParser.php deleted file mode 100644 index b396889..0000000 --- a/libs/parser/src/Literal/StringLiteralParser.php +++ /dev/null @@ -1,64 +0,0 @@ - "'", - '\\\\' => '\\', - ]), - raw: $value, - ); - } -} diff --git a/libs/parser/src/Traverser/DumperVisitor.php b/libs/parser/src/Traverser/DumperVisitor.php index a51c2da..be5f757 100644 --- a/libs/parser/src/Traverser/DumperVisitor.php +++ b/libs/parser/src/Traverser/DumperVisitor.php @@ -46,7 +46,6 @@ public function enter(Node $node): ?Command } /** - * @param \Stringable $node * @return non-empty-string */ private function printStringableNodeSuffix(\Stringable $node): string @@ -62,7 +61,7 @@ private function printNodePropertiesSuffix(Node $node): string { $result = []; - foreach ((new \ReflectionObject($node))->getProperties() as $property) { + foreach (new \ReflectionObject($node)->getProperties() as $property) { // Skip readonly + static and builtin "offset" properties if ($property->isStatic() || $property->isReadOnly() || $property->getName() === 'offset') { continue; diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php index 2e1e98b..45dab9f 100644 --- a/libs/parser/src/TypeParser.php +++ b/libs/parser/src/TypeParser.php @@ -5,12 +5,15 @@ namespace TypeLang\Parser; use JetBrains\PhpStorm\Language; -use Phplrt\Contracts\Parser\ParserRuntimeExceptionInterface; +use Phplrt\Contracts\Lexer\Channel; +use Phplrt\Contracts\Parser\Exception\RuntimeExceptionInterface as ParserRuntimeExceptionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; use Phplrt\Contracts\Source\ReadableInterface; -use Phplrt\Contracts\Source\SourceExceptionInterface; use Phplrt\Contracts\Source\SourceFactoryInterface; -use Phplrt\Parser\Exception\UnexpectedTokenException as LexerUnexpectedTokenException; -use Phplrt\Parser\Exception\UnrecognizedTokenException as LexerUnrecognizedTokenException; +use Phplrt\Parser\Analysis\Result\FailureResult; +use Phplrt\Parser\Analysis\Result\PartialResult; +use Phplrt\Parser\Analysis\Result\SuccessfulResult; +use Phplrt\Parser\Exception\UnexpectedTokenException as GrammarUnexpectedTokenException; use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\InternalParseException; use TypeLang\Parser\Exception\ParseException; @@ -19,19 +22,21 @@ use TypeLang\Parser\Exception\UnexpectedTokenException; use TypeLang\Parser\Exception\UnrecognizedSyntaxException; use TypeLang\Parser\Exception\UnrecognizedTokenException; -use TypeLang\Parser\Internal\ExecutionContext; +use TypeLang\Parser\Internal\Executor; use TypeLang\Type\TypeNode; final class TypeParser implements TypeParserInterface { - private ?ExecutionContext $strict = null; + private readonly SourceFactoryInterface $sources; - private ?ExecutionContext $tolerant = null; + private ?Executor $executor = null; public function __construct( public readonly TypeParserFeatures $features = new TypeParserFeatures(), - private readonly SourceFactoryInterface $sources = new SourceFactory(), - ) {} + ?SourceFactoryInterface $sources = null, + ) { + $this->sources = $sources ?? SourceFactory::createDefault(); + } /** * Returns a new parser with an overridden parser feature flag. @@ -53,87 +58,124 @@ public function withFeatures(bool ...$features): self public function parse(#[Language('PHP')] mixed $source): TypeNode { - $result = $this->execute($this->strictContext(), $source); + $result = $this->execute($source, false); return $result->type; } public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult { - return $this->execute($this->tolerantContext(), $source); + return $this->execute($source, true); } - private function execute(ExecutionContext $context, mixed $source): ParsedResult + /** + * @throws ParseException in case of the source cannot be recognized + */ + private function execute(mixed $source, bool $tolerant): ParsedResult { try { $instance = $this->sources->create($source); + } catch (SourceExceptionInterface $e) { + throw InternalParseException::becauseSourceIsUnreadable($e); + } - try { - return $context->parse($instance) - ?? throw InternalParseException::becauseTypeStatementIsUnreadable(); - } catch (LexerUnexpectedTokenException $e) { - throw $this->unexpectedTokenError($e, $instance); - } catch (LexerUnrecognizedTokenException $e) { - throw $this->unrecognizedTokenError($e, $instance); - } catch (ParserRuntimeExceptionInterface $e) { - throw $this->parserRuntimeError($e, $instance); - } catch (SemanticException $e) { - throw $this->semanticError($e, $instance); - } catch (\Throwable $e) { - throw $this->internalError($e, $instance); + try { + $result = $this->executor()->analyze($instance); + + // A source the grammar reads only in part is a syntax error unless + // the tolerant mode has been asked for. + if ($result instanceof FailureResult + || (!$tolerant && $result instanceof PartialResult) + ) { + throw $this->grammarError($result->error, $instance); } + + return new ParsedResult( + type: $this->fetchTypeStatement($result), + offset: $this->fetchLastOffset($result, $instance), + ); + } catch (ParseException $e) { + throw $e; + } catch (SemanticException $e) { + throw $this->semanticError($e, $instance); } catch (SourceExceptionInterface $e) { throw InternalParseException::becauseSourceIsUnreadable($e); + } catch (\Throwable $e) { + throw $this->internalError($e, $instance); } } /** - * @throws SourceExceptionInterface in case of source content reading error + * @param SuccessfulResult $result + * @throws InternalParseException in case of the grammar has built + * something else than a type statement */ - private function unexpectedTokenError(LexerUnexpectedTokenException $e, ReadableInterface $source): ParseException + private function fetchTypeStatement(SuccessfulResult $result): TypeNode { - $token = $e->getToken(); + $statement = $result->value; - return UnexpectedTokenException::becauseTokenIsUnexpected( - token: $token->getValue(), - statement: $source->getContents(), - offset: $token->getOffset(), - ); + if (!$statement instanceof TypeNode) { + throw InternalParseException::becauseTypeStatementIsUnreadable(); + } + + return $statement; } /** + * Returns the offset the analysis has stopped at. + * + * @param SuccessfulResult $result + * @return int<0, max> * @throws SourceExceptionInterface in case of source content reading error */ - private function unrecognizedTokenError(LexerUnrecognizedTokenException $e, ReadableInterface $source): ParseException + private function fetchLastOffset(SuccessfulResult $result, ReadableInterface $source): int { - $token = $e->getToken(); + // A source that has been read in full is left at its very end. + if (!$result instanceof PartialResult) { + /** @var int<0, max> */ + return \strlen($source->content); + } - return UnrecognizedTokenException::becauseTokenIsUnrecognized( - token: $token->getValue(), - statement: $source->getContents(), - offset: $token->getOffset(), - ); + return $result->token->offset; } /** + * Converts the error of the grammar into the error of this parser. + * * @throws SourceExceptionInterface in case of source content reading error */ - private function semanticError(SemanticException $e, ReadableInterface $source): ParseException + private function grammarError(ParserRuntimeExceptionInterface $e, ReadableInterface $source): ParseException { - return SemanticParseException::becauseSemanticErrorOccurs($e, $source); + if (!$e instanceof GrammarUnexpectedTokenException) { + return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( + statement: $source->content, + offset: $e->token->offset, + ); + } + + // An input the lexer says nothing about is reported as an unrecognized + // one rather than as a token in a wrong place. + if ($e->token->channel === Channel::Unknown) { + return UnrecognizedTokenException::becauseTokenIsUnrecognized( + token: $e->token->value, + statement: $source->content, + offset: $e->token->offset, + ); + } + + return UnexpectedTokenException::becauseTokenIsUnexpected( + token: $e->token->value, + statement: $source->content, + offset: $e->token->offset, + ); } /** * @throws SourceExceptionInterface in case of source content reading error */ - private function parserRuntimeError(ParserRuntimeExceptionInterface $e, ReadableInterface $source): ParseException + private function semanticError(SemanticException $e, ReadableInterface $source): ParseException { - $token = $e->getToken(); - - return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( - statement: $source->getContents(), - offset: $token->getOffset(), - ); + return SemanticParseException::becauseSemanticErrorOccurs($e, $source); } /** @@ -142,24 +184,17 @@ private function parserRuntimeError(ParserRuntimeExceptionInterface $e, Readable private function internalError(\Throwable $e, ReadableInterface $source): ParseException { return InternalParseException::becauseInternalErrorOccurs( - statement: $source->getContents(), + statement: $source->content, e: $e, ); } /** - * Returns a lazily created execution context for the strict mode. - */ - private function strictContext(): ExecutionContext - { - return $this->strict ??= new ExecutionContext($this->features, false); - } - - /** - * Returns a lazily created execution context for the tolerant mode. + * Returns a lazily created parser recognizing a source with the features + * of this one. */ - private function tolerantContext(): ExecutionContext + private function executor(): Executor { - return $this->tolerant ??= new ExecutionContext($this->features, true); + return $this->executor ??= new Executor($this->features); } } diff --git a/libs/parser/src/TypeParserFeatures.php b/libs/parser/src/TypeParserFeatures.php index ab7bc36..8c87aca 100644 --- a/libs/parser/src/TypeParserFeatures.php +++ b/libs/parser/src/TypeParserFeatures.php @@ -32,7 +32,6 @@ final class TypeParserFeatures public const LIST_FEATURES_DEFAULT_VALUE = true; public const OFFSETS_FEATURES_DEFAULT_VALUE = true; public const HINTS_FEATURES_DEFAULT_VALUE = true; - public const ATTRIBUTES_FEATURES_DEFAULT_VALUE = true; public function __construct( /** @@ -75,10 +74,6 @@ public function __construct( * Enables or disables support for generic variance hints such as `T` */ public readonly bool $hints = self::HINTS_FEATURES_DEFAULT_VALUE, - /** - * Enables or disables support for attributes such as `#[attr]` - */ - public readonly bool $attributes = self::ATTRIBUTES_FEATURES_DEFAULT_VALUE, ) {} /** diff --git a/libs/parser/src/TypeResolver.php b/libs/parser/src/TypeResolver.php index f592de8..1a2490b 100644 --- a/libs/parser/src/TypeResolver.php +++ b/libs/parser/src/TypeResolver.php @@ -116,7 +116,7 @@ public function withTypeImportAs(string $name, string $alias): self */ public function withTypeImportsFromClass(\ReflectionClass $class): self { - $statements = (new PhpUseStatementsReader()) + $statements = new PhpUseStatementsReader() ->getClassUseStatements($class); return new self([...$this->imports, ...$statements]); @@ -162,7 +162,7 @@ public function withTypeImportsFromFunction(\ReflectionFunctionAbstract $functio return $this->withTypeImportsFromClass($function->getDeclaringClass()); } - $statements = (new PhpUseStatementsReader()) + $statements = new PhpUseStatementsReader() ->getFunctionUseStatements($function); return new self([...$this->imports, ...$statements]); diff --git a/libs/parser/tests/Exception/ParseExceptionTest.php b/libs/parser/tests/Exception/ParseExceptionTest.php index 7e91e4f..e3865bb 100644 --- a/libs/parser/tests/Exception/ParseExceptionTest.php +++ b/libs/parser/tests/Exception/ParseExceptionTest.php @@ -152,7 +152,7 @@ public function theUnreadableStatementIsReported(): void public function theUnreadableSourceIsReportedUsingTheOriginalMessage(): void { $previous = new class ('source is unreadable') extends \RuntimeException implements - \Phplrt\Contracts\Source\SourceExceptionInterface {}; + \Phplrt\Contracts\Source\Exception\SourceExceptionInterface {}; $exception = InternalParseException::becauseSourceIsUnreadable($previous); @@ -166,7 +166,7 @@ public function theSemanticErrorIsRebasedOntoTheWholeSource(): void { $exception = SemanticParseException::becauseSemanticErrorOccurs( e: ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a', 10), - source: (new SourceFactory())->create('array{a: int, a: int}'), + source: SourceFactory::createDefault()->create('array{a: int, a: int}'), ); self::assertSame( @@ -182,7 +182,7 @@ public function theSemanticErrorCodeIsShiftedByTheBaseValue(): void $exception = SemanticParseException::becauseSemanticErrorOccurs( e: $semantic, - source: (new SourceFactory())->create('array{a: int, a: int}'), + source: SourceFactory::createDefault()->create('array{a: int, a: int}'), ); self::assertSame( @@ -196,7 +196,7 @@ public function theSemanticErrorMessageIsCapitalized(): void { $exception = SemanticParseException::becauseSemanticErrorOccurs( e: ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a'), - source: (new SourceFactory())->create('array{a: int, a: int}'), + source: SourceFactory::createDefault()->create('array{a: int, a: int}'), ); self::assertStringStartsWith('Duplicate', $exception->getMessage()); diff --git a/libs/parser/tests/Exception/SemanticExceptionTest.php b/libs/parser/tests/Exception/SemanticExceptionTest.php index 448d5cc..22b6584 100644 --- a/libs/parser/tests/Exception/SemanticExceptionTest.php +++ b/libs/parser/tests/Exception/SemanticExceptionTest.php @@ -13,7 +13,6 @@ use TypeLang\Parser\Exception\SemanticException; use TypeLang\Parser\Exception\ShapeFieldDuplicationException; use TypeLang\Parser\Exception\ShapeKeysMixingException; -use TypeLang\Parser\Exception\VariadicRedefinitionException; use TypeLang\Parser\Exception\VariadicWithDefaultException; use TypeLang\Parser\Tests\TestCase; @@ -49,11 +48,6 @@ public static function factoryDataProvider(): iterable => ShapeKeysMixingException::becauseShapeKeysAreMixed($offset), ]; - yield 'variadic redefinition' => [ - static fn(int $offset): SemanticException - => VariadicRedefinitionException::becauseVariadicIsRedefined($offset), - ]; - yield 'variadic with default' => [ static fn(int $offset): SemanticException => VariadicWithDefaultException::becauseVariadicHasDefault($offset), @@ -148,18 +142,6 @@ public function theMixedShapeKeysAreReported(): void self::assertSame(SemanticException::ERROR_CODE_SHAPE_KEY_MIX, $exception->getCode()); } - #[Test] - public function theRedefinedVariadicIsReported(): void - { - $exception = VariadicRedefinitionException::becauseVariadicIsRedefined(); - - self::assertSame( - 'Either prefix or postfix variadic syntax should be used, but not both', - $exception->getMessage(), - ); - self::assertSame(SemanticException::ERROR_CODE_VARIADIC_ALREADY_VARIADIC, $exception->getCode()); - } - #[Test] public function theVariadicWithADefaultIsReported(): void { @@ -183,7 +165,6 @@ public function everyErrorCodeIsUnique(): void SemanticException::ERROR_CODE_SHAPE_KEY_DUPLICATION, SemanticException::ERROR_CODE_SHAPE_KEY_MIX, SemanticException::ERROR_CODE_VARIADIC_WITH_DEFAULT, - SemanticException::ERROR_CODE_VARIADIC_ALREADY_VARIADIC, SemanticException::ERROR_CODE_INVALID_OPERATOR, ]; diff --git a/libs/parser/tests/Internal/SequenceFetcherTest.php b/libs/parser/tests/Internal/SequenceFetcherTest.php index 4fb88a0..4c08a59 100644 --- a/libs/parser/tests/Internal/SequenceFetcherTest.php +++ b/libs/parser/tests/Internal/SequenceFetcherTest.php @@ -6,10 +6,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use TypeLang\Parser\Internal\PatternSequenceFetcher; -use TypeLang\Parser\Internal\ScannerSequenceFetcher; -use TypeLang\Parser\Internal\SequenceFetcherInterface; -use TypeLang\Parser\Internal\StringParser; +use TypeLang\Parser\Internal\StringDecoder; +use TypeLang\Parser\Internal\StringDecoder\PatternSequenceFetcher; +use TypeLang\Parser\Internal\StringDecoder\ScannerSequenceFetcher; +use TypeLang\Parser\Internal\StringDecoder\SequenceFetcherInterface; use TypeLang\Parser\Tests\TestCase; final class SequenceFetcherTest extends TestCase @@ -91,7 +91,7 @@ public function scannerSkipsSequencesBehindAnEscapedBackslash(): void self::assertArrayNotHasKey('\x41', ScannerSequenceFetcher::fetch($body)); self::assertArrayHasKey('\x41', PatternSequenceFetcher::fetch($body)); - self::assertSame('a\x41b', StringParser::parse($body)); + self::assertSame('a\x41b', StringDecoder::decode($body)); } #[Test] @@ -102,7 +102,7 @@ public function bothFetchersDecodeTheBodyIdentically(string $body): void $pattern = \strtr($body, PatternSequenceFetcher::fetch($body)); self::assertSame($scanner, $pattern); - self::assertSame($scanner, StringParser::parse($body)); + self::assertSame($scanner, StringDecoder::decode($body)); } #[Test] diff --git a/libs/parser/tests/Internal/StringParserTest.php b/libs/parser/tests/Internal/StringParserTest.php index 455110e..9a78ee9 100644 --- a/libs/parser/tests/Internal/StringParserTest.php +++ b/libs/parser/tests/Internal/StringParserTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use TypeLang\Parser\Internal\StringParser; +use TypeLang\Parser\Internal\StringDecoder; use TypeLang\Parser\Tests\TestCase; final class StringParserTest extends TestCase @@ -56,27 +56,27 @@ public static function provideBodies(): iterable #[DataProvider('provideBodies')] public function bodyIsDecoded(string $body, string $expected): void { - self::assertSame($expected, StringParser::parse($body)); + self::assertSame($expected, StringDecoder::decode($body)); } #[Test] public function rawNullByteIsNotTouched(): void { - self::assertSame("a\0b", StringParser::parse("a\0b")); - self::assertSame("a\0\\b", StringParser::parse("a\0" . '\\\\b')); + self::assertSame("a\0b", StringDecoder::decode("a\0b")); + self::assertSame("a\0\\b", StringDecoder::decode("a\0" . '\\\\b')); } #[Test] public function invalidUtf8IsNotTouched(): void { - self::assertSame("\xFF\xFE", StringParser::parse("\xFF\xFE")); + self::assertSame("\xFF\xFE", StringDecoder::decode("\xFF\xFE")); } #[Test] public function decodingIsIdempotentForStringsWithoutBackslashes(): void { - $decoded = StringParser::parse('a\nb'); + $decoded = StringDecoder::decode('a\nb'); - self::assertSame($decoded, StringParser::parse($decoded)); + self::assertSame($decoded, StringDecoder::decode($decoded)); } } diff --git a/libs/parser/tests/Internal/UtfCharRendererTest.php b/libs/parser/tests/Internal/UtfCharRendererTest.php index 5b0e962..6ee2411 100644 --- a/libs/parser/tests/Internal/UtfCharRendererTest.php +++ b/libs/parser/tests/Internal/UtfCharRendererTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use TypeLang\Parser\Internal\UtfCharRenderer; +use TypeLang\Parser\Internal\StringDecoder\UtfCharRenderer; use TypeLang\Parser\Tests\TestCase; final class UtfCharRendererTest extends TestCase diff --git a/libs/parser/tests/Literal/IntLiteralParserTest.php b/libs/parser/tests/Literal/IntLiteralTest.php similarity index 53% rename from libs/parser/tests/Literal/IntLiteralParserTest.php rename to libs/parser/tests/Literal/IntLiteralTest.php index a4f0f14..b8389a9 100644 --- a/libs/parser/tests/Literal/IntLiteralParserTest.php +++ b/libs/parser/tests/Literal/IntLiteralTest.php @@ -6,12 +6,27 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use TypeLang\Parser\Literal\IntLiteralParser; use TypeLang\Parser\Tests\TestCase; use TypeLang\Type\Literal\IntLiteralNode; -final class IntLiteralParserTest extends TestCase +/** + * Tests for the integer literals the parser builds out of their raw + * representation. + */ +final class IntLiteralTest extends TestCase { + /** + * @throws \Throwable + */ + private function literal(string $code): IntLiteralNode + { + $node = $this->parse($code); + + self::assertInstanceOf(IntLiteralNode::class, $node); + + return $node; + } + /** * @return iterable */ @@ -32,44 +47,75 @@ public static function provideIntegers(): iterable yield 'underscored hexadecimal' => ['0xFF_FF', 65535, '65535']; } + /** + * @param non-empty-string $literal + * @param non-empty-string $decimal + * @throws \Throwable + */ #[Test] #[DataProvider('provideIntegers')] public function integerIsParsedToItsDecimalValue(string $literal, int $value, string $decimal): void { - $node = IntLiteralParser::parse($literal); + $node = $this->literal($literal); self::assertSame($value, $node->value); self::assertSame($decimal, $node->decimal); } + /** + * @param non-empty-string $literal + * @param non-empty-string $decimal + * @throws \Throwable + */ #[Test] #[DataProvider('provideIntegers')] public function integerParsingKeepsTheOriginalRepresentation(string $literal, int $value, string $decimal): void { - self::assertSame($literal, IntLiteralParser::parse($literal)->raw); + self::assertSame($literal, $this->literal($literal)->raw); } + /** + * @throws \Throwable + */ #[Test] public function integerParsingSupportsPhpIntMin(): void { - $node = IntLiteralParser::parse((string) \PHP_INT_MIN); - - self::assertSame(\PHP_INT_MIN, $node->value); + self::assertSame(\PHP_INT_MIN, $this->literal((string) \PHP_INT_MIN)->value); } + /** + * @throws \Throwable + */ #[Test] public function integerParsingSupportsPhpIntMax(): void { - $node = IntLiteralParser::parse((string) \PHP_INT_MAX); - - self::assertSame(\PHP_INT_MAX, $node->value); + self::assertSame(\PHP_INT_MAX, $this->literal((string) \PHP_INT_MAX)->value); } + /** + * @throws \Throwable + */ #[Test] public function negativeZeroIsParsedAsZero(): void { - $node = IntLiteralParser::parse('-0'); + self::assertSame(0, $this->literal('-0')->value); + } + + /** + * @throws \Throwable + */ + #[Test] + public function theOffsetOfALiteralIsTheOneItIsWrittenAt(): void + { + $node = $this->parse('array{a: 42}'); + + self::assertInstanceOf(\TypeLang\Type\NamedTypeNode::class, $node); + self::assertNotNull($node->fields); + + $field = $node->fields->items[0]; - self::assertSame(0, $node->value); + self::assertInstanceOf(\TypeLang\Type\Shape\NamedFieldNode::class, $field); + self::assertInstanceOf(IntLiteralNode::class, $field->type); + self::assertSame(9, $field->type->offset); } } diff --git a/libs/parser/tests/Literal/StringLiteralParserTest.php b/libs/parser/tests/Literal/StringLiteralTest.php similarity index 65% rename from libs/parser/tests/Literal/StringLiteralParserTest.php rename to libs/parser/tests/Literal/StringLiteralTest.php index ff2627d..789b48d 100644 --- a/libs/parser/tests/Literal/StringLiteralParserTest.php +++ b/libs/parser/tests/Literal/StringLiteralTest.php @@ -6,12 +6,28 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; -use TypeLang\Parser\Literal\StringLiteralParser; use TypeLang\Parser\Tests\TestCase; use TypeLang\Type\Literal\StringLiteralNode; -final class StringLiteralParserTest extends TestCase +/** + * Tests for the string literals the parser builds out of their raw + * representation, including every escape sequence a double quoted one may + * carry. + */ +final class StringLiteralTest extends TestCase { + /** + * @throws \Throwable + */ + private function literal(string $code): StringLiteralNode + { + $node = $this->parse($code); + + self::assertInstanceOf(StringLiteralNode::class, $node); + + return $node; + } + /** * @return iterable */ @@ -38,7 +54,6 @@ public static function provideDoubleQuotedStrings(): iterable yield 'null byte hexadecimal sequence' => ['"\x00"', "\0"]; yield 'null byte unicode sequence' => ['"a\u{0}b"', "a\0b"]; yield 'binary sequence does not break the next one' => ['"\xFF\u{42}"', "\xFF" . 'B']; - yield 'raw null byte is kept as is' => ["\"a\0\\\\b\"", "a\0" . '\\' . 'b']; yield 'escaped backslash before escaped quote' => ['"a\\\\\"b"', 'a\"b']; yield 'escaped backslash before escape sequence' => ['"a\\\\nb"', 'a\nb']; yield 'octal sequence' => ['"\101"', 'A']; @@ -48,23 +63,20 @@ public static function provideDoubleQuotedStrings(): iterable yield 'code point above the unicode range' => ['"\u{110000}"', "\u{FFFD}"]; } + /** + * @param non-empty-string $literal + * @throws \Throwable + */ #[Test] #[DataProvider('provideDoubleQuotedStrings')] public function doubleQuotedStringIsDecoded(string $literal, string $value): void { - $node = StringLiteralParser::parse($literal); + $node = $this->literal($literal); self::assertSame($value, $node->value); self::assertSame($literal, $node->raw); } - #[Test] - #[DataProvider('provideDoubleQuotedStrings')] - public function doubleQuotedFactoryIsTheSameAsParsing(string $literal, string $value): void - { - self::assertSame($value, StringLiteralParser::createFromDoubleQuotedString($literal)->value); - } - /** * @return iterable */ @@ -79,60 +91,31 @@ public static function provideSingleQuotedStrings(): iterable yield 'dollar sign is not decoded' => ["'a\$b'", 'a$b']; } + /** + * @param non-empty-string $literal + * @throws \Throwable + */ #[Test] #[DataProvider('provideSingleQuotedStrings')] public function singleQuotedStringIsDecoded(string $literal, string $value): void { - $node = StringLiteralParser::parse($literal); + $node = $this->literal($literal); self::assertSame($value, $node->value); self::assertSame($literal, $node->raw); } - #[Test] - #[DataProvider('provideSingleQuotedStrings')] - public function singleQuotedFactoryIsTheSameAsParsing(string $literal, string $value): void - { - self::assertSame($value, StringLiteralParser::createFromSingleQuotedString($literal)->value); - } - + /** + * A node built by hand derives the raw representation from its value, and + * reading that representation back gives the very same value. + * + * @throws \Throwable + */ #[Test] public function derivedRawValueCanBeParsedBack(): void { $node = new StringLiteralNode('a"b'); - self::assertSame($node->value, StringLiteralParser::parse($node->raw)->value); - } - - #[Test] - public function stringParsingThrowsOnEmptyLiteral(): void - { - $this->expectException(\InvalidArgumentException::class); - - StringLiteralParser::parse(''); - } - - #[Test] - public function stringParsingThrowsOnSingleCharacterLiteral(): void - { - $this->expectException(\InvalidArgumentException::class); - - StringLiteralParser::parse('"'); - } - - #[Test] - public function doubleQuotedFactoryThrowsOnTooShortLiteral(): void - { - $this->expectException(\InvalidArgumentException::class); - - StringLiteralParser::createFromDoubleQuotedString('"'); - } - - #[Test] - public function singleQuotedFactoryThrowsOnTooShortLiteral(): void - { - $this->expectException(\InvalidArgumentException::class); - - StringLiteralParser::createFromSingleQuotedString("'"); + self::assertSame($node->value, $this->literal($node->raw)->value); } } diff --git a/libs/parser/tests/Syntax/AttributeTest.php b/libs/parser/tests/Syntax/AttributeTest.php deleted file mode 100644 index 73e6432..0000000 --- a/libs/parser/tests/Syntax/AttributeTest.php +++ /dev/null @@ -1,122 +0,0 @@ -parseAndPrint('HashMap<#[name("key")] T>')); - } - - public function testMultipleAttributesInOneGroup(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(HashMap) - Template\TemplateArgumentListNode - Template\TemplateArgumentNode - NamedTypeNode - Name(T) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(name) - Attribute\AttributeNode - Name(out) - AST, $this->parseAndPrint('HashMap<#[name("key"), out] T>')); - } - - public function testMultipleAttributeGroups(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(HashMap) - Template\TemplateArgumentListNode - Template\TemplateArgumentNode - NamedTypeNode - Name(T) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(name) - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(out) - AST, $this->parseAndPrint('HashMap<#[name("key")] #[out] T>')); - } - - public function testSingleAttributeOnShapeField(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(App\Domain\User) - Shape\FieldsListNode(isSealed=true) - Shape\NamedFieldNode(isOptional=false) - Identifier(userName) - NamedTypeNode - Name(non-empty-string) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(name) - AST, $this->parseAndPrint('App\\Domain\\User{#[name("user_name")] userName: non-empty-string}')); - } - - public function testMultipleAttributeGroupsOnShapeField(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(array) - Shape\FieldsListNode(isSealed=true) - Shape\NamedFieldNode(isOptional=true) - Identifier(test) - NamedTypeNode - Name(App\Domain\User) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(serialize) - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(deserialize) - AST, $this->parseAndPrint('array{#[serialize("onSerialize")] #[deserialize("onDeserialize")] test?: App\\Domain\\User}')); - } - - public function testTemplateArgumentAttributeAllowsOnlyIdentifiers(): void - { - $this->expectParsingException('unexpected "42"'); - - $this->parse('Collection<#[42] User>'); - } - - public function testShapeFieldAttributeAllowsOnlyIdentifiers(): void - { - $this->expectParsingException('unexpected "42"'); - - $this->parse('Collection{#[42] test?: User}'); - } -} diff --git a/libs/parser/tests/Syntax/CallableTest.php b/libs/parser/tests/Syntax/CallableTest.php index a101ebc..d5aa385 100644 --- a/libs/parser/tests/Syntax/CallableTest.php +++ b/libs/parser/tests/Syntax/CallableTest.php @@ -134,19 +134,14 @@ public function testOptionalParameter(): void AST, $this->parseAndPrint('foo(T=)')); } - public function testVariadicParameterPrefixSyntax(): void + public function testVariadicMarkerCannotPrecedeTheParameterType(): void { - self::assertSame(<<<'AST' - CallableTypeNode - Name(foo) - Callable\CallableParameterListNode - Callable\CallableParameterNode(isOutput=false, isVariadic=true, isOptional=false) - NamedTypeNode - Name(T) - AST, $this->parseAndPrint('foo(...T)')); + $this->expectParsingException('unexpected "..."'); + + $this->parse('foo(...T)'); } - public function testVariadicParameterPostfixSyntax(): void + public function testVariadicParameter(): void { self::assertSame(<<<'AST' CallableTypeNode @@ -158,20 +153,7 @@ public function testVariadicParameterPostfixSyntax(): void AST, $this->parseAndPrint('foo(T...)')); } - public function testVariadicNamedOutputParameter(): void - { - self::assertSame(<<<'AST' - CallableTypeNode - Name(foo) - Callable\CallableParameterListNode - Callable\CallableParameterNode(isOutput=true, isVariadic=true, isOptional=false) - NamedTypeNode - Name(T) - Literal\VariableLiteralNode($name) - AST, $this->parseAndPrint('foo(...T &$name)')); - } - - public function testParameterWithoutTypeIsNotAllowed(): void + public function testNameCannotFollowTheDefaultMarker(): void { $this->expectParsingException('unexpected "$name"'); @@ -180,16 +162,16 @@ public function testParameterWithoutTypeIsNotAllowed(): void public function testAmpersandMustFollowParameterType(): void { - $this->expectParsingException('unexpected "T"'); + $this->expectParsingException('unexpected "&"'); $this->parse('foo(&T)'); } - public function testVariadicCannotBeBothPrefixAndPostfix(): void + public function testParameterWithoutATypeIsNotAllowed(): void { - $this->expectParsingException('Either prefix or postfix variadic syntax should be used, but not both'); + $this->expectParsingException('unexpected ")"'); - $this->parse('foo(...T...)'); + $this->parse('foo($name)'); } public function testVariadicParameterCannotHaveDefault(): void @@ -205,4 +187,47 @@ public function testLeadingCommaIsNotAllowed(): void $this->parse('foo(,T)'); } + + /** + * A reference marker precedes the variadic one, the way PHP itself writes + * it: {@code &...$name} rather than {@code ...&$name}. + */ + public function testReferenceAndVariadicMarkersOfATypedParameterAreOrdered(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(foo) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=true, isVariadic=true, isOptional=false) + NamedTypeNode + Name(T) + Literal\VariableLiteralNode($name) + AST, $this->parseAndPrint('foo(T &...$name)')); + } + + public function testTypedParameterCannotPutTheReferenceAfterTheVariadic(): void + { + $this->expectParsingException('unexpected "&"'); + + $this->parse('foo(T ...&$name)'); + } + + public function testModifiersWithoutATypeAreNotAllowed(): void + { + $this->expectParsingException('unexpected "&"'); + + $this->parse('foo(&...$name)'); + } + + public function testTypedParameterAllowsBothMarkersWithoutAName(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(foo) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=true, isVariadic=true, isOptional=false) + NamedTypeNode + Name(T) + AST, $this->parseAndPrint('foo(T &...)')); + } } diff --git a/libs/parser/tests/Syntax/ConditionalTest.php b/libs/parser/tests/Syntax/ConditionalTest.php index f426967..9aee273 100644 --- a/libs/parser/tests/Syntax/ConditionalTest.php +++ b/libs/parser/tests/Syntax/ConditionalTest.php @@ -67,7 +67,7 @@ public function testEqualityOperatorIsNotAllowed(): void public function testInequalityOperatorIsNotAllowed(): void { - $this->expectParsingException('unexpected "!"'); + $this->expectParsingException('unrecognized "!="'); $this->parse('A != B ? C : D'); } diff --git a/libs/parser/tests/Syntax/FeatureToggleTest.php b/libs/parser/tests/Syntax/FeatureToggleTest.php index 1fa42ae..da44097 100644 --- a/libs/parser/tests/Syntax/FeatureToggleTest.php +++ b/libs/parser/tests/Syntax/FeatureToggleTest.php @@ -31,7 +31,6 @@ public static function disabledFeatureDataProvider(): iterable yield 'lists' => [['lists' => false], 'int[]', 'Square bracket list types not allowed']; yield 'offsets' => [['offsets' => false], 'T[U]', 'Type offsets not allowed']; yield 'conditions' => [['conditions' => false], 'A is B ? C : D', 'Conditional expressions not allowed']; - yield 'attributes' => [['attributes' => false], 'T<#[a] U>', 'Template argument attributes not allowed']; yield 'hints' => [['hints' => false], 'T', 'Template argument hints not allowed']; } diff --git a/libs/parser/tests/Syntax/FloatLiteralTest.php b/libs/parser/tests/Syntax/FloatLiteralTest.php index 66f2dc9..8545dcc 100644 --- a/libs/parser/tests/Syntax/FloatLiteralTest.php +++ b/libs/parser/tests/Syntax/FloatLiteralTest.php @@ -40,7 +40,7 @@ public function testValidFloats(string $type, float $expected): void public function testLeadingAndTrailingNumberCannotBothBeOmitted(): void { - $this->expectParsingException('unexpected "."'); + $this->expectParsingException('unrecognized "."'); $this->parse('.'); } diff --git a/libs/parser/tests/Syntax/NameTest.php b/libs/parser/tests/Syntax/NameTest.php index bd34007..4ba827b 100644 --- a/libs/parser/tests/Syntax/NameTest.php +++ b/libs/parser/tests/Syntax/NameTest.php @@ -96,7 +96,7 @@ public function testNameCannotStartWithDigitFollowedByLetters(): void public function testNameCannotStartWithDash(): void { - $this->expectParsingException('unexpected "-"'); + $this->expectParsingException('unrecognized "-foo"'); $this->parse('-foo'); } diff --git a/libs/parser/tests/TestCase.php b/libs/parser/tests/TestCase.php index 0de06aa..fba3ad4 100644 --- a/libs/parser/tests/TestCase.php +++ b/libs/parser/tests/TestCase.php @@ -27,7 +27,6 @@ * list?: bool, * offsets?: bool, * hints?: bool, - * attributes?: bool, * } */ #[Group('unit'), Group('type-lang/parser')] diff --git a/libs/parser/tests/Traverser/MatcherVisitorTest.php b/libs/parser/tests/Traverser/MatcherVisitorTest.php index 6f7a7ed..a504780 100644 --- a/libs/parser/tests/Traverser/MatcherVisitorTest.php +++ b/libs/parser/tests/Traverser/MatcherVisitorTest.php @@ -122,7 +122,7 @@ public function theBreakConditionStopsTheWholeTraversal(): void break: static fn(Node $node): bool => $node instanceof UnionTypeNode, ), [ - new UnionTypeNode($this->type('A'), $this->type('B')), + new UnionTypeNode([$this->type('A'), $this->type('B')]), new Identifier('Example'), ], ); @@ -184,7 +184,7 @@ public function theBreakConditionDoesNotAffectTheNextTraversal(): void $traverser = Traverser::new([$visitor]); - $traverser->traverse([new UnionTypeNode($this->type('A'), $this->type('B'))]); + $traverser->traverse([new UnionTypeNode([$this->type('A'), $this->type('B')])]); $traverser->traverse([$this->type('Foo\\Bar')]); self::assertTrue($visitor->hasMatches()); diff --git a/libs/parser/tests/Traverser/StringDumperVisitorTest.php b/libs/parser/tests/Traverser/StringDumperVisitorTest.php index 2802302..4c80ec2 100644 --- a/libs/parser/tests/Traverser/StringDumperVisitorTest.php +++ b/libs/parser/tests/Traverser/StringDumperVisitorTest.php @@ -90,10 +90,10 @@ public function everyNestingLevelIsIndentedByTwoSpaces(): void Name(string) OUTPUT, $this->dump([ - new UnionTypeNode( + new UnionTypeNode([ new NamedTypeNode(Name::createFromString('int')), - new NamedTypeNode(Name::createFromString('string')), - ), + new NamedTypeNode(Name::createFromString('string')) + ]), ])); } @@ -149,6 +149,8 @@ public function theReadonlyPropertiesAreNotPrinted(): void public function __construct() { $this->hidden = true; + + parent::__construct(); } }; diff --git a/libs/parser/tests/Traverser/TraverserTest.php b/libs/parser/tests/Traverser/TraverserTest.php index 7b6a181..ce2b026 100644 --- a/libs/parser/tests/Traverser/TraverserTest.php +++ b/libs/parser/tests/Traverser/TraverserTest.php @@ -343,7 +343,7 @@ public function traverseVisitsTheNestedLogicalStatements(): void $visitor = $this->recorder(); Traverser::new([$visitor])->traverse([ - new UnionTypeNode($this->type('A'), $this->type('B')), + new UnionTypeNode([$this->type('A'), $this->type('B')]), ]); self::assertSame([ diff --git a/libs/parser/tests/Traverser/TypeMapVisitorTest.php b/libs/parser/tests/Traverser/TypeMapVisitorTest.php index 2b0ba7d..834b41d 100644 --- a/libs/parser/tests/Traverser/TypeMapVisitorTest.php +++ b/libs/parser/tests/Traverser/TypeMapVisitorTest.php @@ -105,10 +105,10 @@ public function theNullResultKeepsTheOriginalName(): void #[Test] public function theTransformationIsAppliedToEveryNestedType(): void { - $node = new UnionTypeNode( + $node = new UnionTypeNode([ new NamedTypeNode(Name::createFromString('A')), - new NamedTypeNode(Name::createFromString('B')), - ); + new NamedTypeNode(Name::createFromString('B')) + ]); Traverser::new([$this->alias()])->traverse([$node]); diff --git a/libs/parser/tests/TypeParserFeaturesTest.php b/libs/parser/tests/TypeParserFeaturesTest.php index 71dbeb7..11e6c24 100644 --- a/libs/parser/tests/TypeParserFeaturesTest.php +++ b/libs/parser/tests/TypeParserFeaturesTest.php @@ -78,12 +78,6 @@ public static function featureDataProvider(): iterable static fn(TypeParserFeatures $features): bool => $features->hints, TypeParserFeatures::HINTS_FEATURES_DEFAULT_VALUE, ]; - - yield 'attributes' => [ - 'attributes', - static fn(TypeParserFeatures $features): bool => $features->attributes, - TypeParserFeatures::ATTRIBUTES_FEATURES_DEFAULT_VALUE, - ]; } /** diff --git a/libs/parser/tests/TypeParserTest.php b/libs/parser/tests/TypeParserTest.php index fabbd25..6c40623 100644 --- a/libs/parser/tests/TypeParserTest.php +++ b/libs/parser/tests/TypeParserTest.php @@ -94,7 +94,7 @@ public function theSameParserCanBeUsedSeveralTimes(): void #[Test] public function theSourceFactoryCanBeOverridden(): void { - $parser = new TypeParser(sources: new SourceFactory()); + $parser = new TypeParser(sources: SourceFactory::createDefault()); self::assertInstanceOf(NamedTypeNode::class, $parser->parse('int')); } @@ -102,7 +102,7 @@ public function theSourceFactoryCanBeOverridden(): void #[Test] public function theSourceFactoryIsInheritedByTheNewInstance(): void { - $parser = (new TypeParser(sources: new SourceFactory())) + $parser = (new TypeParser(sources: SourceFactory::createDefault())) ->withFeatures(shapes: false); self::assertInstanceOf(NamedTypeNode::class, $parser->parse('int')); From fea5535da49ef689c21502a2e4dad592e3f1bef8 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 00:57:10 +0300 Subject: [PATCH 033/119] Upgrade spec (remove attributes and fix callable parameter syntax) --- .../Appendix B -- Grammar Summary.md | 32 +--- docs/Specification/Section 1 -- Overview.md | 2 - .../Section 2 -- Lexical Grammar.md | 10 +- docs/Specification/Section 3 -- Types.md | 11 +- .../Section 4 -- Structural Types.md | 145 +++------------ docs/Writerside/cfg/static/spec.html | 168 +++--------------- 6 files changed, 53 insertions(+), 315 deletions(-) diff --git a/docs/Specification/Appendix B -- Grammar Summary.md b/docs/Specification/Appendix B -- Grammar Summary.md index 61b2c81..42248ac 100644 --- a/docs/Specification/Appendix B -- Grammar Summary.md +++ b/docs/Specification/Appendix B -- Grammar Summary.md @@ -64,7 +64,7 @@ Punctuator :: one of - `?` `|` `&` `*` `,` `:` `=` - `(` `)` `[` `]` `{` `}` - `<` `>` `<=` `>=` -- `::` `\` `...` `#[` +- `::` `\` `...` NameToken :: NameStart NameContinue\* [lookahead != NameContinue] @@ -251,7 +251,7 @@ NamedType : Name (TemplateArguments | ShapeFields)? TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>` -TemplateArgument : AttributeGroups? (TemplateArgumentHint | TemplateArgumentType) +TemplateArgument : TemplateArgumentHint | TemplateArgumentType TemplateArgumentHint : Identifier Type @@ -288,7 +288,7 @@ ShapeBody : ShapeFieldList : ShapeField (`,` ShapeField)\* -ShapeField : AttributeGroups? (ExplicitField | ImplicitField) +ShapeField : ExplicitField | ImplicitField ExplicitField : ShapeKey `?`? `:` ShapeValue @@ -314,28 +314,4 @@ CallableParameters : CallableParameter (`,` CallableParameter)\* `,`? CallableReturnType : `:` Type -CallableParameter : AttributeGroups? CallableParameterBody `=`? - -CallableParameterBody : - -- `...`? Type ParameterModifiers? Variable? -- ParameterModifiers? Variable - -ParameterModifiers : - -- `&` `...`? -- `...` `&`? - -**Attributes** - -AttributeGroups : AttributeGroup+ - -AttributeGroup : `#[` AttributeList `,`? `]` - -AttributeList : Attribute (`,` Attribute)\* - -Attribute : Name AttributeArguments? - -AttributeArguments : `(` AttributeArgument (`,` AttributeArgument)\* `,`? `)` - -AttributeArgument : Type +CallableParameter : Type `&`? `...`? Variable? `=`? diff --git a/docs/Specification/Section 1 -- Overview.md b/docs/Specification/Section 1 -- Overview.md index b612849..28aa172 100644 --- a/docs/Specification/Section 1 -- Overview.md +++ b/docs/Specification/Section 1 -- Overview.md @@ -103,8 +103,6 @@ others: - Escape, hexadecimal and Unicode sequences inside double-quoted string literals. - Global constant masks such as `JSON_*`. -- Attributes (`#[...]`) on template arguments, shape fields and callable - parameters. - A uniform conditional (ternary) type syntax that also permits comparison operators and Yoda-style operands. diff --git a/docs/Specification/Section 2 -- Lexical Grammar.md b/docs/Specification/Section 2 -- Lexical Grammar.md index e68d8ac..73c6454 100644 --- a/docs/Specification/Section 2 -- Lexical Grammar.md +++ b/docs/Specification/Section 2 -- Lexical Grammar.md @@ -116,11 +116,6 @@ comment_ begins with `/*` and continues up to and including the next `*/`. Comments are {Ignored} and have no bearing on the meaning of a document. -Note: The `#` line comment marker and the `#[` attribute marker share a -leading character. Because lexical analysis prefers the longest match, the -sequence `#[` is always scanned as the start of an attribute (see -[Attributes](#sec-Attributes)) rather than as the start of a comment. - ## Lexical Tokens Token :: @@ -146,12 +141,11 @@ Punctuator :: one of - `?` `|` `&` `*` `,` `:` `=` - `(` `)` `[` `]` `{` `}` - `<` `>` `<=` `>=` -- `::` `\` `...` `#[` +- `::` `\` `...` TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, `<` and `<=`; `:` and `::`; `.` in -`...`; `#` in `#[`); in every such case, the longest matching punctuator is -taken. +`...`); in every such case, the longest matching punctuator is taken. ### Names diff --git a/docs/Specification/Section 3 -- Types.md b/docs/Specification/Section 3 -- Types.md index 3090ac6..c40ba32 100644 --- a/docs/Specification/Section 3 -- Types.md +++ b/docs/Specification/Section 3 -- Types.md @@ -348,7 +348,7 @@ implementation and is outside the scope of this specification. TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>` -TemplateArgument : AttributeGroups? (TemplateArgumentHint | TemplateArgumentType) +TemplateArgument : TemplateArgumentHint | TemplateArgumentType TemplateArgumentType : Type @@ -429,15 +429,6 @@ HashMap ParseException: Syntax error, unexpected "Request" ``` -### Template Argument Attributes - -Each template argument MAY additionally be prefixed with one or more -[attribute groups](#sec-Attributes), providing metadata for the argument. - -```typescript -HashMap<#[name("key")] T, #[name("value")] U> -``` - ## Literal Types LiteralType : diff --git a/docs/Specification/Section 4 -- Structural Types.md b/docs/Specification/Section 4 -- Structural Types.md index e38ac4e..b2173ca 100644 --- a/docs/Specification/Section 4 -- Structural Types.md +++ b/docs/Specification/Section 4 -- Structural Types.md @@ -1,9 +1,8 @@ # Structural Types -This section defines the structural type forms — _shapes_ and _callables_ — -and the _attribute_ syntax shared by several constructs. Each of these forms -builds on the [named type](#sec-Named-Types) grammar introduced in the -previous section. +This section defines the structural type forms — _shapes_ and _callables_. +Each of these forms builds on the [named type](#sec-Named-Types) grammar +introduced in the previous section. ## Shape Types @@ -45,7 +44,7 @@ array{} ### Shape Fields -ShapeField : AttributeGroups? (ExplicitField | ImplicitField) +ShapeField : ExplicitField | ImplicitField ExplicitField : ShapeKey `?`? `:` ShapeValue @@ -157,21 +156,6 @@ keys — using the same angle-bracket syntax as generics: array{ user: User, ... } ``` -### Shape Field Attributes - -Each shape field MAY be prefixed with one or more -[attribute groups](#sec-Attributes): - -```typescript -App\Domain\User{ - #[name("user_name")] - userName: non-empty-string, - #[skip_when_empty] - friends: list, - ... -} -``` - ## Callable Types CallableType : Name `(` CallableParameters? `)` CallableReturnType? @@ -199,20 +183,11 @@ Closure(int<0, max>, callable(?C): mixed): void ### Callable Parameters -CallableParameter : AttributeGroups? CallableParameterBody `=`? - -CallableParameterBody : - -- `...`? Type ParameterModifiers? Variable? -- ParameterModifiers? Variable +CallableParameter : Type `&`? `...`? Variable? `=`? -ParameterModifiers : - -- `&` `...`? -- `...` `&`? - -A parameter is described by its type, optionally followed by a name. A -parameter MAY also be given by name alone, without a type. +A parameter is described by its type, optionally followed by the reference +and the variadic markers, by a name and by the default marker — in that +order and in no other. The type is the only required part. ```typescript callable(Type) @@ -222,10 +197,17 @@ callable(Type) callable(Type $name) ``` -```typescript +**Counter-example.** A parameter given by name alone, without a type, is +not permitted. + +```typescript counter-example callable($name) ``` +``` +ParseException: Syntax error, unexpected ")" +``` + **Named Parameters.** A name beginning with `$` MAY follow the parameter's type, [permitting the argument to be passed by name](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments), exactly as in PHP. @@ -253,7 +235,7 @@ callable(&T) ``` ``` -ParseException: Syntax error, unexpected "T" +ParseException: Syntax error, unexpected "&" ``` **Optional Parameters.** A trailing `=` marks a parameter as optional: the @@ -267,13 +249,9 @@ callable(T=) callable(T &$name=) ``` -**Variadic Parameters.** An ellipsis (`...`) marks a parameter as variadic. It -MAY be written either before the type or after it, immediately before the -name. - -```typescript -callable(...T) -``` +**Variadic Parameters.** An ellipsis (`...`) placed after the parameter type +marks the parameter as variadic. Where a parameter carries both markers, the +ampersand comes first. ```typescript callable(T ...$name) @@ -283,16 +261,15 @@ callable(T ...$name) callable(T &...$name) ``` -**Counter-example.** The ellipsis may appear in only one position; a -parameter that is variadic in both the prefix and suffix positions is an -error. +**Counter-example.** The ellipsis must follow the type; it must not precede +it. ```typescript counter-example -callable(...T...) +callable(...T) ``` ``` -ParseException: Either prefix or postfix variadic syntax should be used, but not both +ParseException: Syntax error, unexpected "..." ``` **Counter-example.** A variadic parameter is already optional and therefore @@ -305,77 +282,3 @@ callable(T ...$name=) ``` ParseException: Cannot have variadic param with a default ``` - -### Callable Parameter Attributes - -Each callable parameter MAY be prefixed with one or more -[attribute groups](#sec-Attributes): - -```typescript -Example\Functor(#[type] int $a): void -``` - -## Attributes - -AttributeGroups : AttributeGroup+ - -AttributeGroup : `#[` AttributeList `,`? `]` - -AttributeList : Attribute (`,` Attribute)\* - -Attribute : Name AttributeArguments? - -AttributeArguments : `(` AttributeArgument (`,` AttributeArgument)\* `,`? `)` - -AttributeArgument : Type - -An _attribute_ attaches arbitrary, implementation-defined metadata to the -construct it precedes. The syntax mirrors -[PHP attributes](https://www.php.net/manual/en/language.attributes.php): each -attribute is a [name](#sec-Names-and-Namespaces) optionally followed by a -parenthesized list of arguments, and each argument is itself an arbitrary -{Type} — including, recursively, a type that carries attributes of its own. - -Attributes are written in groups delimited by `#[` and `]`. A construct MAY -carry several attributes within one group, separated by commas, and several -groups in sequence. Attributes may be attached to a -[template argument](#sec-Template-Argument-Attributes), a -[shape field](#sec-Shape-Field-Attributes), or a -[callable parameter](#sec-Callable-Parameter-Attributes). - -A single attribute with one argument: - -```typescript -Example\Functor(#[type] int $a): void -``` - -Several attributes within one group: - -```typescript -HashMap<#[name("key"), out] T> -``` - -Several groups in sequence: - -```typescript -array{ - #[serialize("onSerialize")] - #[deserialize("onDeserialize")] - test?: App\Domain\User, -} -``` - -**Counter-example.** An attribute name must be a valid {Name}; a literal is -not permitted in that position. - -```typescript counter-example -Collection<#[42] User> -``` - -``` -ParseException: Syntax error, unexpected "42" -``` - -Note: Because the `#[` attribute marker is scanned as a single token by the -[lexical grammar](#sec-Comments), it is never confused with the `#` -line-comment marker. diff --git a/docs/Writerside/cfg/static/spec.html b/docs/Writerside/cfg/static/spec.html index 4ad11cd..a1f5ce2 100644 --- a/docs/Writerside/cfg/static/spec.html +++ b/docs/Writerside/cfg/static/spec.html @@ -74,7 +74,6 @@
Statu
  • 5.7Named Types
  • 5.8Generic Types
    1. 5.8.1Template Argument Hints
    2. -
    3. 5.8.2Template Argument Attributes
  • 5.9Literal Types
  • @@ -91,15 +90,12 @@
    Statu
  • 6.1.1Shape Fields
  • 6.1.2Optional Fields
  • 6.1.3Unsealed Shapes
  • -
  • 6.1.4Shape Field Attributes
  • 6.2Callable Types
    1. 6.2.1Callable Parameters
    2. -
    3. 6.2.2Callable Parameter Attributes
  • -
  • 6.3Attributes
  • AAppendix: Notation Conventions
      @@ -176,7 +172,6 @@

      LineTerminator (or the end of the source, whichever occurs first). A block comment begins with /* and continues up to and including the next */.

      Comments are Ignored and have no bearing on the meaning of a document.

      -
      -Note -The # line comment marker and the #[ attribute marker share a leading character. Because lexical analysis prefers the longest match, the sequence #[ is always scanned as the start of an attribute (see Attributes) rather than as the start of a comment.
      @@ -313,10 +305,10 @@

      <><=>= -::\...#[ +::\... -

      TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, < and <=; : and ::; . in ...; # in #[); in every such case, the longest matching punctuator is taken.

      +

      TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, < and <=; : and ::; . in ...); in every such case, the longest matching punctuator is taken.

      4.3.2Names

      @@ -860,7 +852,7 @@

      TemplateArguments
      TemplateArgumentType @@ -901,12 +893,6 @@

      -
      -

      5.8.2Template Argument Attributes

      -

      Each template argument MAY additionally be prefixed with one or more attribute groups, providing metadata for the argument.

      -
      HashMap<#[name("key")] T, #[name("value")] U>
      -
      -

      5.9Literal Types

      @@ -990,7 +976,7 @@

      6Structural Types

      -

      This section defines the structural type forms — shapes and callables — and the attribute syntax shared by several constructs. Each of these forms builds on the named type grammar introduced in the previous section.

      +

      This section defines the structural type forms — shapes and callables. Each of these forms builds on the named type grammar introduced in the previous section.

      6.1Shape Types

      -
      -

      6.1.4Shape Field Attributes

      -

      Each shape field MAY be prefixed with one or more attribute groups:

      -
      App\Domain\User{
      -    #[name("user_name")]
      -    userName: non-empty-string,
      -    #[skip_when_empty]
      -    friends: list<App\Domain\User>,
      -    ...
      -}
      -
      -

      6.2Callable Types

      @@ -1120,22 +1094,17 @@

      6.2.1Callable Parameters

      - -
      -ParameterModifiers
      &...opt
      -
      ...&opt
      -
      -

      A parameter is described by its type, optionally followed by a name. A parameter MAY also be given by name alone, without a type.

      +

      A parameter is described by its type, optionally followed by the reference and the variadic markers, by a name and by the default marker — in that order and in no other. The type is the only required part.

      callable(Type)
       
      callable(Type $name)
       
      -
      callable($name)
      +

      Counter-example. A parameter given by name alone, without a type, is not permitted.

      +
      Counter Example № 17callable($name)
      +
      +
      ParseException: Syntax error, unexpected ")"
       

      Named Parameters. A name beginning with $ MAY follow the parameter’s type, permitting the argument to be passed by name, exactly as in PHP.

      callable(A $a, B, C)
      @@ -1146,83 +1115,31 @@ 

      callable(T &$name)

      Counter-example. The ampersand must follow the type; it must not precede it.

      -
      Counter Example № 17callable(&T)
      +
      Counter Example № 18callable(&T)
       
      -
      ParseException: Syntax error, unexpected "T"
      +
      ParseException: Syntax error, unexpected "&"
       

      Optional Parameters. A trailing = marks a parameter as optional: the caller MAY omit the corresponding argument.

      callable(T=)
       
      callable(T &$name=)
       
      -

      Variadic Parameters. An ellipsis (...) marks a parameter as variadic. It MAY be written either before the type or after it, immediately before the name.

      -
      callable(...T)
      -
      +

      Variadic Parameters. An ellipsis (...) placed after the parameter type marks the parameter as variadic. Where a parameter carries both markers, the ampersand comes first.

      callable(T ...$name)
       
      callable(T &...$name)
       
      -

      Counter-example. The ellipsis may appear in only one position; a parameter that is variadic in both the prefix and suffix positions is an error.

      -
      Counter Example № 18callable(...T...)
      +

      Counter-example. The ellipsis must follow the type; it must not precede it.

      +
      Counter Example № 19callable(...T)
       
      -
      ParseException: Either prefix or postfix variadic syntax should be used, but not both
      +
      ParseException: Syntax error, unexpected "..."
       

      Counter-example. A variadic parameter is already optional and therefore must not additionally carry a default marker.

      -
      Counter Example № 19callable(T ...$name=)
      +
      Counter Example № 20callable(T ...$name=)
       
      ParseException: Cannot have variadic param with a default
       

      -
      -

      6.2.2Callable Parameter Attributes

      -

      Each callable parameter MAY be prefixed with one or more attribute groups:

      -
      Example\Functor(#[type<int8>] int $a): void
      -
      -
      - -
      -

      6.3Attributes

      - - - - - - -

      An attribute attaches arbitrary, implementation-defined metadata to the construct it precedes. The syntax mirrors PHP attributes: each attribute is a name optionally followed by a parenthesized list of arguments, and each argument is itself an arbitrary Type — including, recursively, a type that carries attributes of its own.

      -

      Attributes are written in groups delimited by #[ and ]. A construct MAY carry several attributes within one group, separated by commas, and several groups in sequence. Attributes may be attached to a template argument, a shape field, or a callable parameter.

      -

      A single attribute with one argument:

      -
      Example\Functor(#[type<int8>] int $a): void
      -
      -

      Several attributes within one group:

      -
      HashMap<#[name("key"), out] T>
      -
      -

      Several groups in sequence:

      -
      array{
      -    #[serialize("onSerialize")]
      -    #[deserialize("onDeserialize")]
      -    test?: App\Domain\User,
      -}
      -
      -

      Counter-example. An attribute name must be a valid Name; a literal is not permitted in that position.

      -
      Counter Example № 20Collection<#[42] User>
      -
      -
      ParseException: Syntax error, unexpected "42"
      -
      -
      -Note -Because the #[ attribute marker is scanned as a single token by the lexical grammar, it is never confused with the # line-comment marker.
      @@ -1394,7 +1311,7 @@
      <><=>= -::\...#[ +::\... - -
      -ParameterModifiers
      &...opt
      -
      ...&opt
      -
      -
      -
      -
      Attributes
      - - - - - -
      @@ -1766,12 +1654,6 @@
      §Index
        -
      1. Attribute
      2. -
      3. AttributeArgument
      4. -
      5. AttributeArguments
      6. -
      7. AttributeGroup
      8. -
      9. AttributeGroups
      10. -
      11. AttributeList
      12. BinaryDigit
      13. BinaryIndicator
      14. BinaryIntLiteral
      15. @@ -1779,7 +1661,6 @@

      16. BlockCommentChar
      17. BoolLiteral
      18. CallableParameter
      19. -
      20. CallableParameterBody
      21. CallableParameters
      22. CallableReturnType
      23. CallableType
      24. @@ -1834,7 +1715,6 @@

      25. OctalIndicator
      26. OctalIntLiteral
      27. OffsetSuffix
      28. -
      29. ParameterModifiers
      30. PostfixType
      31. PrimaryType
      32. Punctuator
      33. @@ -1947,7 +1827,6 @@

        1. 5.8.1Template Argument Hints
        2. -
        3. 5.8.2Template Argument Attributes
      34. 5.9Literal Types
      35. @@ -1970,17 +1849,14 @@

      36. 6.1.1Shape Fields
      37. 6.1.2Optional Fields
      38. 6.1.3Unsealed Shapes
      39. -
      40. 6.1.4Shape Field Attributes
    1. 6.2Callable Types
      1. 6.2.1Callable Parameters
      2. -
      3. 6.2.2Callable Parameter Attributes
    2. -
    3. 6.3Attributes
  • AAppendix: Notation Conventions From f68a6d0841a08d6af92aa5eaa99805bd1b48fceb Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 00:57:24 +0300 Subject: [PATCH 034/119] Actualize docs (remove attributes and fix callable parameter syntax) --- .../topics/language/callable-types.md | 78 +++---------------- docs/Writerside/topics/language/comparison.md | 46 ----------- .../topics/language/generic-types.md | 43 ---------- .../Writerside/topics/language/shape-types.md | 59 -------------- docs/Writerside/topics/parser/features.md | 46 ----------- 5 files changed, 12 insertions(+), 260 deletions(-) diff --git a/docs/Writerside/topics/language/callable-types.md b/docs/Writerside/topics/language/callable-types.md index 092f87d..25eec22 100644 --- a/docs/Writerside/topics/language/callable-types.md +++ b/docs/Writerside/topics/language/callable-types.md @@ -148,8 +148,8 @@ parameter description. ### Variadic Parameters -Variadic parameters are indicated by the "`...`" and can be placed either -_before the type_ or _before the parameter name._ +Variadic parameters are indicated by the "`...`" placed after the type. Where +a parameter carries both markers, the ampersand ("`&`") comes first. > Variadic parameter cannot be optional since they are already optional. {style="warning"} @@ -157,24 +157,6 @@ _before the type_ or _before the parameter name._ -> Callable type with one variadic parameter. -> ```typescript -> foo(...T) -> ``` - -> Callable type with one variadic named parameter. -> ```typescript -> foo(...T $name) -> ``` - -> Callable type with one variadic output named parameter. -> ```typescript -> foo(...T &$name) -> ``` - - - - > Callable type with one variadic parameter. > ```typescript > foo(T...) @@ -193,9 +175,9 @@ _before the type_ or _before the parameter name._ -> The ellipses (`...`) must come before or after the type. +> The ellipsis (`...`) must be placed after the parameter's type. > ```typescript -> foo(...T...) +> foo(...T) > ``` > > An error similar to the one below should occur @@ -204,61 +186,25 @@ _before the type_ or _before the parameter name._ > ``` > {style="warning"} -> Variadic parameter cannot be optional. +> The ampersand (`&`) must be placed before the ellipsis (`...`). > ```typescript -> foo(T ...$name=) +> foo(T ...&$name) > ``` -> +> > An error similar to the one below should occur > ``` -> ParseException: Cannot have variadic param with a default +> ParseException: Syntax error, unexpected "&" > ``` > {style="warning"} - - - - -## Attributes - - - -Each callable parameter allows you to define list of additional attributes. -An attribute is additional metadata for a parameter. - - - - -> Simple attribute with one argument for each callable parameter. -> ```typescript -> Example\Functor(#[type(int8)] int $a): void -> ``` - -> Multiple attributes in one group. -> ```typescript -> Example\Functor(#[type(int8), const] int&): void -> ``` - -> Multiple attribute groups. -> ```typescript -> Example\OnCreate( -> #[deprecated] -> #[inline] -> (callback(T): void) $callback=, -> ): void -> ``` - - - - -> Only valid identifiers are allowed. +> Variadic parameter cannot be optional. > ```typescript -> Example\Functor(#[42] int $a): void +> foo(T ...$name=) > ``` -> +> > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "42" +> ParseException: Cannot have variadic param with a default > ``` > {style="warning"} diff --git a/docs/Writerside/topics/language/comparison.md b/docs/Writerside/topics/language/comparison.md index d0840f7..d91a45e 100644 --- a/docs/Writerside/topics/language/comparison.md +++ b/docs/Writerside/topics/language/comparison.md @@ -248,28 +248,6 @@ Below is a list of simple, logical and other common types. - - - - Template argument attributes - - - ExampleCollection<#[assert(not<"0">)] array-key> - - - - - - - - - Not Supported - - - - Not Supported - - @@ -1538,30 +1516,6 @@ Below is a list of grammar of shaped types. - - - - Shape field attributes - - - object { - #[inline, assert(not<"">)] - name: string - } - - - - - - - - - - PHPStan parser only (since 2.3) - - - - diff --git a/docs/Writerside/topics/language/generic-types.md b/docs/Writerside/topics/language/generic-types.md index 8905c84..baef0bc 100644 --- a/docs/Writerside/topics/language/generic-types.md +++ b/docs/Writerside/topics/language/generic-types.md @@ -156,46 +156,3 @@ In addition to modern list declarations such as `list` or - - -## Attributes - - - -Each template argument allows you to define list of additional attributes. -An attribute is additional metadata for an argument. - - - - -> Simple attribute with one argument for each template argument. -> ```typescript -> HashMap<#[name("key")] T, #[name("value")] U> -> ``` - -> Multiple attributes in one group. -> ```typescript -> HashMap<#[name("key"), out] T> -> ``` - -> Multiple attribute groups. -> ```typescript -> HashMap<#[name("key")] #[out] T> -> ``` - - - - -> Only valid identifiers are allowed. -> ```typescript -> Collection<#[42] User> -> ``` -> -> An error similar to the one below should occur -> ``` -> ParseException: Syntax error, unexpected "42" -> ``` -> {style="warning"} - - - diff --git a/docs/Writerside/topics/language/shape-types.md b/docs/Writerside/topics/language/shape-types.md index 772f062..15d2f25 100644 --- a/docs/Writerside/topics/language/shape-types.md +++ b/docs/Writerside/topics/language/shape-types.md @@ -196,62 +196,3 @@ array{ } ``` - - -## Attributes - - - -Each shape field allows you to define list of additional attributes. -An attribute is additional metadata for a field. - - - - -> Simple attribute with one argument for each shape field. -> ```typescript -> App\Domain\User{ -> #[name("user_name")] -> userName: non-empty-string, -> #[skip_when_empty] -> friends: list, -> ... -> } -> ``` - -> Multiple attributes in one group. -> ```typescript -> App\Domain\User{ -> #[complexity(100), skip_when_empty] -> friends: list, -> } -> ``` - -> Multiple attribute groups. -> ```typescript -> array{ -> #[serialize("onSerialize")] -> #[deserialize("onDeserialize")] -> test?: App\Domain\User, -> } -> ``` - - - - -> Only valid identifiers are allowed. -> ```typescript -> Collection{ -> #[42] -> test?: App\Domain\User, -> } -> ``` -> -> An error similar to the one below should occur -> ``` -> ParseException: Syntax error, unexpected "42" -> ``` -> {style="warning"} - - - diff --git a/docs/Writerside/topics/parser/features.md b/docs/Writerside/topics/parser/features.md index 96c2510..4ea4ca3 100644 --- a/docs/Writerside/topics/parser/features.md +++ b/docs/Writerside/topics/parser/features.md @@ -68,7 +68,6 @@ $features = new TypeParserFeatures() | `unions` | `T\|U` | `Union types not allowed` | | `intersections` | `T&U` | `Intersection types not allowed` | | `conditions` | `T is U ? A : B` | `Conditional expressions not allowed` | -| `attributes` | `#[attr]` on fields/args/params | see below | ### Literals @@ -232,48 +231,3 @@ $parser->parse('T is U ? 23 : 42'); ``` Conditional expressions not allowed in "T is U ? 23 : 42" at column 1 ``` - -### Attributes - -Unlike the other flags, `attributes` is checked independently at every -place attributes may occur — currently on -[shape fields](shape-types.md#attributes), on -[template arguments](generic-types.md#attributes), and on -[callable parameters](callable-types.md#attributes) — each reporting its own -message. - -```php -$parser = new TypeParser(new TypeParserFeatures( - attributes: false, -)); - -$parser->parse(<<<'PHP' - array{ - #[name("new_name"), skip_when_empty] - oldName: int, - } - PHP); -``` - -``` -Shape field attributes not allowed in "array{\n #[name("new_name"), -…" (33+) on line 2 at column 5 -``` - -```php -$parser->parse('Collection<#[a] T>'); -``` - -``` -Template argument attributes not allowed in "Collection<#[a] T>" -at column 12 -``` - -```php -$parser->parse('callable(#[a] int $x): void'); -``` - -``` -Callable parameter attributes not allowed in "callable(#[a] int $x): void" -at column 10 -``` From 08732e5d625ccbca566b3dc3b5338a1fc1a45fbf Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:25:29 +0300 Subject: [PATCH 035/119] Add wrapping AST type comments with examples --- libs/types/src/WrappingTypeNode.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/types/src/WrappingTypeNode.php b/libs/types/src/WrappingTypeNode.php index 0e5a44a..ce58364 100644 --- a/libs/types/src/WrappingTypeNode.php +++ b/libs/types/src/WrappingTypeNode.php @@ -5,6 +5,14 @@ namespace TypeLang\Type; /** + * A type (monad) built around another one. + * + * ``` + * ?Some\Any + * ^ the wrapping type + * ^^^^^^^^ the type it wraps + * ``` + * * @template T of TypeNode = TypeNode */ abstract class WrappingTypeNode extends TypeNode From c0e1623ace58ee6f4b2eada3c9389c9b97195992 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:25:45 +0300 Subject: [PATCH 036/119] Add logical AST types comments with examples --- libs/types/src/IntersectionTypeNode.php | 6 ++ libs/types/src/LogicalTypeNode.php | 107 ++++++++++++++++++------ libs/types/src/UnionTypeNode.php | 6 ++ 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/libs/types/src/IntersectionTypeNode.php b/libs/types/src/IntersectionTypeNode.php index d1f0255..58f7aab 100644 --- a/libs/types/src/IntersectionTypeNode.php +++ b/libs/types/src/IntersectionTypeNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type; /** + * All of several types at once. + * + * ``` + * Some\Any & Stringable + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends LogicalTypeNode diff --git a/libs/types/src/LogicalTypeNode.php b/libs/types/src/LogicalTypeNode.php index 525db56..e58a895 100644 --- a/libs/types/src/LogicalTypeNode.php +++ b/libs/types/src/LogicalTypeNode.php @@ -5,16 +5,24 @@ namespace TypeLang\Type; /** + * Several types joined by an operator, like a union or an intersection. + * + * A statement of the same kind is flattened into its parent, so an + * `A|(B|C)` holds the three of them side by side rather than a nested + * statement. + * * @template T of TypeNode = TypeNode * * @template-implements \IteratorAggregate + * @template-implements \ArrayAccess, T> */ abstract class LogicalTypeNode extends TypeNode implements \IteratorAggregate, + \ArrayAccess, \Countable { /** - * @var non-empty-list + * @var list{T, T, ...} */ public array $statements; @@ -22,7 +30,7 @@ abstract class LogicalTypeNode extends TypeNode implements * A logical statement accepts an arbitrary number of types, so there is no * place left for an offset argument: use the {@see $offset} property. * - * @param iterable $statements + * @param iterable $statements * @param int<0, max> $offset * @throws \LogicException in case of less than two statements are passed */ @@ -33,7 +41,7 @@ public function __construct( $statements = self::unwrap($statements); if (\count($statements) < 2) { - throw new \LogicException('A logical statement must contain at least 2 elements'); + throw new \InvalidArgumentException('A logical statement must contain at least 2 elements'); } // @phpstan-ignore-next-line : List of types contains at least 2 elements @@ -68,39 +76,88 @@ private static function unwrap(iterable $statements): array return $result; } - public function getIterator(): \Traversable + public function offsetExists(mixed $offset): bool { - return new \ArrayIterator($this->statements); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetExists($offset) must be an int<0, max>', static::class), + ); + } + + return isset($this->statements[$offset]); } - /** - * @return int<2, max> a logical statement must contain at least 2 elements - */ - public function count(): int + public function offsetGet(mixed $offset): ?Node { - /** @var int<2, max> */ - return \count($this->statements); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetGet($offset) must be an int<0, max>', static::class), + ); + } + + return $this->statements[$offset] ?? null; } - /** - * @return array{non-empty-list, int<0, max>} - */ - public function __serialize(): array + public function offsetSet(mixed $offset, mixed $value): void + { + if (!$value instanceof Node) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet(..., $value) must be instance of %s', static::class, Node::class), + ); + } + + if ($offset === null) { + $this->statements[] = $value; + + return; + } + + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet($offset, ...) must be an int<0, max>|null', static::class), + ); + } + + // @phpstan-ignore-next-line + $this->statements[$offset] = $value; + + if (!\array_is_list($this->statements)) { + $this->statements = \array_values($this->statements); + } + } + + public function offsetUnset(mixed $offset): void + { + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetUnset($offset) must be an int<0, max>', static::class), + ); + } + + // @phpstan-ignore-next-line : Temporary allow stmt removing + unset($this->statements[$offset]); + + if (\count($this->statements) < 2) { + throw new \InvalidArgumentException('A logical statement must contain at least 2 elements'); + } + + if (!\array_is_list($this->statements)) { + // @phpstan-ignore-next-line : An array size already has been checked above + $this->statements = \array_values($this->statements); + } + } + + public function getIterator(): \Traversable { - return [$this->statements, $this->offset]; + return new \ArrayIterator($this->statements); } /** - * @param array{0?: non-empty-list, 1?: int<0, max>} $data - * @throws \UnexpectedValueException + * @return int<2, max> a logical statement must contain at least 2 elements */ - public function __unserialize(array $data): void + public function count(): int { - $this->statements = $data[0] ?? throw new \UnexpectedValueException(\sprintf( - 'Unable to unserialize %s statements', - static::class, - )); - - $this->offset = $data[1] ?? 0; + /** @var int<2, max> */ + return \count($this->statements); } } diff --git a/libs/types/src/UnionTypeNode.php b/libs/types/src/UnionTypeNode.php index 2a2deba..5bf3307 100644 --- a/libs/types/src/UnionTypeNode.php +++ b/libs/types/src/UnionTypeNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type; /** + * Any one of several types. + * + * ``` + * int|string|null + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends LogicalTypeNode From 59b60e0928e250e012d65d9e3ff181cbc7542927 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:26:19 +0300 Subject: [PATCH 037/119] Add name + identifier comments and improve Name constructor --- libs/types/src/Identifier.php | 10 ++++++ libs/types/src/Name.php | 61 ++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/libs/types/src/Identifier.php b/libs/types/src/Identifier.php index 2545d43..838ce36 100644 --- a/libs/types/src/Identifier.php +++ b/libs/types/src/Identifier.php @@ -5,6 +5,16 @@ namespace TypeLang\Type; /** + * A single word a name is made of. + * + * ``` + * Some\Any + * ^^^^ ^^^ // two identifiers + * + * non-empty-string + * ^^^^^^^^^^^^^^^^ // a single identifier: A dash belongs to a word + * ``` + * * @phpstan-consistent-constructor */ final class Identifier extends Node implements \Stringable diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index 01cb6a3..5bbf11c 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -5,6 +5,17 @@ namespace TypeLang\Type; /** + * A name, that is, the identifiers ({@see Identifier}) it is written of and the + * namespace separators between them. + * + * ``` + * Some\Any + * ^^^^ ^^^ a relative name of two parts + * + * \Some\Any + * ^ a fully qualified name begins in a separator + * ``` + * * @phpstan-consistent-constructor * * @template-implements \IteratorAggregate @@ -19,11 +30,21 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String public const IS_FULLY_QUALIFIED_DEFAULT_VALUE = false; /** + * The identifiers a name is written of, in order. Never empty: A name of + * no parts is no name. + * * @var non-empty-list */ public readonly array $parts; + /** + * The first of the {@see $parts}, which is the one an alias is resolved by. + */ public readonly Identifier $first; + + /** + * The last of the {@see $parts}, which is the short name of a class. + */ public readonly Identifier $last; /** @@ -34,9 +55,32 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String */ public function __construct( iterable $parts, + /** + * Whether a name is written with the leading separator that says it + * is to be read from the root and not from wherever it stands. + * + * ``` + * Some\Any // false + * \Some\Any // true + * ``` + */ public readonly bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, int $offset = 0, ) { + $this->parts = self::formatNameParts($parts); + + $this->first = $this->parts[0]; + $this->last = $this->parts[\count($this->parts) - 1]; + + parent::__construct($offset); + } + + /** + * @param iterable $parts + * @return non-empty-list + */ + private static function formatNameParts(iterable $parts): array + { $parts = match (true) { $parts instanceof \Traversable => \iterator_to_array($parts, false), \array_is_list($parts) => $parts, @@ -47,22 +91,7 @@ public function __construct( throw new \InvalidArgumentException('Name parts count can not be empty'); } - $first = \reset($parts); - $last = \end($parts); - - if (!$first instanceof Identifier || !$last instanceof Identifier) { - throw new \InvalidArgumentException(\sprintf( - 'Name parts must contain only %s instances', - Identifier::class, - )); - } - - $this->parts = $parts; - - $this->first = $first; - $this->last = $last; - - parent::__construct($offset); + return $parts; } /** From c2513ea977f78ecb30b54f242e115ee034dc27f2 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:26:57 +0300 Subject: [PATCH 038/119] Add support of non-empty node list --- libs/types/src/NodeList.php | 90 +++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/libs/types/src/NodeList.php b/libs/types/src/NodeList.php index ef867d7..ccfb4cb 100644 --- a/libs/types/src/NodeList.php +++ b/libs/types/src/NodeList.php @@ -5,7 +5,11 @@ namespace TypeLang\Type; /** + * An ordered list of nodes of one kind, like the arguments of a generic + * or the fields of a shape. + * * @template TNode of Node = Node + * @template TNonEmpty of bool = false * * @template-implements \IteratorAggregate * @template-implements \ArrayAccess, TNode> @@ -16,7 +20,9 @@ abstract class NodeList extends Node implements \Countable { /** - * @var list + * The nodes of a list, in the order they are written in. + * + * @var (TNonEmpty is true ? non-empty-list : list) */ public array $items = []; @@ -27,16 +33,40 @@ abstract class NodeList extends Node implements public function __construct(iterable $items = [], int $offset = 0) { $this->items = match (true) { - // A list is already shaped the way it is stored, so it is taken - // as it is rather than copied - \is_array($items) && \array_is_list($items) => $items, - \is_array($items) => \array_values($items), - default => \iterator_to_array($items, false), + $items instanceof \Traversable => \iterator_to_array($items, false), + \array_is_list($items) => $items, + default => \array_values($items), }; parent::__construct($offset); } + /** + * Gets the first node of a list or {@see null} in case of the list is empty. + * + * @return (TNonEmpty is true ? TNode : TNode|null) + */ + public function first(): ?Node + { + return $this->items[0] ?? null; + } + + /** + * Gets the last node of a list or {@see null} in case of the list is empty. + * + * @return (TNonEmpty is true ? TNode : TNode|null) + */ + public function last(): ?Node + { + $key = \array_key_last($this->items); + + if ($key === null) { + return null; + } + + return $this->items[$key]; + } + /** * Returns the ordinal number (position) of an element {@see TNode} in * a node list, starting with index 0. @@ -61,7 +91,9 @@ public function findIndex(Node $node): ?int public function offsetExists(mixed $offset): bool { if (!\is_int($offset) || $offset < 0) { - throw new \InvalidArgumentException('A NodeList index must be an int<0, max>'); + throw new \InvalidArgumentException( + \sprintf('A %s::offsetExists($offset) must be an int<0, max>', static::class), + ); } return isset($this->items[$offset]); @@ -70,7 +102,9 @@ public function offsetExists(mixed $offset): bool public function offsetGet(mixed $offset): ?Node { if (!\is_int($offset) || $offset < 0) { - throw new \InvalidArgumentException('A NodeList index must be an int<0, max>'); + throw new \InvalidArgumentException( + \sprintf('A %s::offsetGet($offset) must be an int<0, max>', static::class), + ); } return $this->items[$offset] ?? null; @@ -79,7 +113,9 @@ public function offsetGet(mixed $offset): ?Node public function offsetSet(mixed $offset, mixed $value): void { if (!$value instanceof Node) { - throw new \InvalidArgumentException('A NodeList value must be instance of Node'); + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet(..., $value) must be instance of %s', static::class, Node::class), + ); } if ($offset === null) { @@ -89,7 +125,9 @@ public function offsetSet(mixed $offset, mixed $value): void } if (!\is_int($offset) || $offset < 0) { - throw new \InvalidArgumentException('A NodeList index must be an int<0, max>|null'); + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet($offset, ...) must be an int<0, max>|null', static::class), + ); } // @phpstan-ignore-next-line @@ -103,7 +141,9 @@ public function offsetSet(mixed $offset, mixed $value): void public function offsetUnset(mixed $offset): void { if (!\is_int($offset) || $offset < 0) { - throw new \InvalidArgumentException('A NodeList index must be an int<0, max>|null'); + throw new \InvalidArgumentException( + \sprintf('A %s::offsetUnset($offset) must be an int<0, max>', static::class), + ); } // @phpstan-ignore-next-line @@ -120,36 +160,10 @@ public function getIterator(): \Traversable } /** - * @return int<0, max> + * @return (TNonEmpty is true ? int<1, max> : int<0, max>) */ public function count(): int { return \count($this->items); } - - /** - * Gets the first node of a list or {@see null} in case of the list is empty. - * - * @return TNode|null - */ - public function first(): ?Node - { - return $this->items[0] ?? null; - } - - /** - * Gets the last node of a list or {@see null} in case of the list is empty. - * - * @return TNode|null - */ - public function last(): ?Node - { - $key = \array_key_last($this->items); - - if ($key === null) { - return null; - } - - return $this->items[$key]; - } } From 7ab4386d2aaa7504443d5836c1184acac95bbce7 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:27:26 +0300 Subject: [PATCH 039/119] Add wrapping AST types comments --- libs/types/src/NullableTypeNode.php | 6 ++++++ libs/types/src/TypeOffsetAccessNode.php | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/libs/types/src/NullableTypeNode.php b/libs/types/src/NullableTypeNode.php index 5db00b3..25e2d8a 100644 --- a/libs/types/src/NullableTypeNode.php +++ b/libs/types/src/NullableTypeNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type; /** + * A type a `null` is allowed besides, written with a leading `?`. + * + * ``` + * ?Some\Any + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends WrappingTypeNode diff --git a/libs/types/src/TypeOffsetAccessNode.php b/libs/types/src/TypeOffsetAccessNode.php index 61b2bbc..89f7bc0 100644 --- a/libs/types/src/TypeOffsetAccessNode.php +++ b/libs/types/src/TypeOffsetAccessNode.php @@ -5,6 +5,14 @@ namespace TypeLang\Type; /** + * The type standing at an offset of another type. + * + * ``` + * Some\Any[int] + * ^^^^^^^^ the type an offset is taken of + * ^^^ the offset itself + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends WrappingTypeNode @@ -17,6 +25,13 @@ final class TypeOffsetAccessNode extends WrappingTypeNode */ public function __construct( TypeNode $type, + /** + * The offset itself, that is, whatever stands inside the brackets. + * + * Note: An `$access` name instead of `$offset` is used to avoid + * conflicts with the physical byte offset of the type in the + * source code. + */ public readonly TypeNode $access, int $offset = 0, ) { From 8653c0235890f470f35e30bb08223b32ed9da316 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:28:30 +0300 Subject: [PATCH 040/119] Add scalar nodes and comments --- libs/types/src/Literal/BoolLiteralNode.php | 10 ++++++++-- libs/types/src/Literal/FloatLiteralNode.php | 11 +++++++++-- libs/types/src/Literal/IntLiteralNode.php | 14 ++++++++++++-- libs/types/src/Literal/LiteralNode.php | 17 +++++++++++++++++ libs/types/src/Literal/LiteralNodeInterface.php | 2 ++ libs/types/src/Literal/NullLiteralNode.php | 6 ++++++ libs/types/src/Literal/ScalarNode.php | 12 ++++++++++++ libs/types/src/Literal/StringLiteralNode.php | 10 ++++++++-- 8 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 libs/types/src/Literal/ScalarNode.php diff --git a/libs/types/src/Literal/BoolLiteralNode.php b/libs/types/src/Literal/BoolLiteralNode.php index 09f1ded..4a6ee94 100644 --- a/libs/types/src/Literal/BoolLiteralNode.php +++ b/libs/types/src/Literal/BoolLiteralNode.php @@ -5,11 +5,17 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A `true` or a `false`, in any case they are written in. + * + * ``` + * TruE // value: true, raw: "TruE" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class BoolLiteralNode extends LiteralNode +final class BoolLiteralNode extends ScalarNode { /** * @param int<0, max> $offset diff --git a/libs/types/src/Literal/FloatLiteralNode.php b/libs/types/src/Literal/FloatLiteralNode.php index 7b8580a..45e6a3a 100644 --- a/libs/types/src/Literal/FloatLiteralNode.php +++ b/libs/types/src/Literal/FloatLiteralNode.php @@ -5,11 +5,18 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A floating point number. + * + * ``` + * -1.5e+3 // value: -1500.0, raw: "-1.5e+3" + * .42 // value: 0.42, raw: ".42" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class FloatLiteralNode extends LiteralNode +final class FloatLiteralNode extends ScalarNode { /** * @param int<0, max> $offset diff --git a/libs/types/src/Literal/IntLiteralNode.php b/libs/types/src/Literal/IntLiteralNode.php index 54f1c0e..c8e9aaf 100644 --- a/libs/types/src/Literal/IntLiteralNode.php +++ b/libs/types/src/Literal/IntLiteralNode.php @@ -5,13 +5,23 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A whole number, written in any of the four radixes. + * + * ``` + * 0xFE_DE // value: 65246, raw: "0xFE_DE", decimal: "65246" + * 042 // value: 34, raw: "042", decimal: "34" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class IntLiteralNode extends LiteralNode +final class IntLiteralNode extends ScalarNode { /** + * The value written out in base 10, so that a number too large for the + * platform's `int` is still readable in full. + * * @var numeric-string */ public readonly string $decimal; diff --git a/libs/types/src/Literal/LiteralNode.php b/libs/types/src/Literal/LiteralNode.php index 1e43593..4d8cbc6 100644 --- a/libs/types/src/Literal/LiteralNode.php +++ b/libs/types/src/Literal/LiteralNode.php @@ -7,6 +7,17 @@ use TypeLang\Type\TypeNode; /** + * A value standing where a type is expected, kept both as PHP sees it + * and as it is written. + * + * The two are not the same thing: A value is what PHP would compute, while + * a raw is the very text it was computed from, quotes, radix and all. + * + * ``` + * 0x1F // value: 31, raw: "0x1F" + * '\x41' // value: "\x41", raw: "'\x41'" + * ``` + * * @template TValue of mixed = mixed * * @template-implements LiteralNodeInterface @@ -21,9 +32,15 @@ abstract class LiteralNode extends TypeNode implements LiteralNodeInterface */ public function __construct( /** + * The value as PHP sees it. + * * @var TValue */ public readonly mixed $value, + /** + * The very text the value was read from, so that a statement can be + * written back the way it came. + */ public readonly string $raw, int $offset = 0, ) { diff --git a/libs/types/src/Literal/LiteralNodeInterface.php b/libs/types/src/Literal/LiteralNodeInterface.php index fc68feb..3300cae 100644 --- a/libs/types/src/Literal/LiteralNodeInterface.php +++ b/libs/types/src/Literal/LiteralNodeInterface.php @@ -5,6 +5,8 @@ namespace TypeLang\Type\Literal; /** + * A value standing where a type is expected. + * * @template-covariant TValue of mixed = mixed * * @property-read TValue $value Gets a PHP representation of the literal value. diff --git a/libs/types/src/Literal/NullLiteralNode.php b/libs/types/src/Literal/NullLiteralNode.php index fb5d7e1..2fe8480 100644 --- a/libs/types/src/Literal/NullLiteralNode.php +++ b/libs/types/src/Literal/NullLiteralNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type\Literal; /** + * A `null`, in any case it is written in. + * + * ``` + * NulL // value: null, raw: "NulL" + * ``` + * * @template-extends LiteralNode */ final class NullLiteralNode extends LiteralNode diff --git a/libs/types/src/Literal/ScalarNode.php b/libs/types/src/Literal/ScalarNode.php new file mode 100644 index 0000000..11b4d4e --- /dev/null +++ b/libs/types/src/Literal/ScalarNode.php @@ -0,0 +1,12 @@ + + */ +abstract class ScalarNode extends LiteralNode {} diff --git a/libs/types/src/Literal/StringLiteralNode.php b/libs/types/src/Literal/StringLiteralNode.php index eb211e3..6628965 100644 --- a/libs/types/src/Literal/StringLiteralNode.php +++ b/libs/types/src/Literal/StringLiteralNode.php @@ -5,11 +5,17 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A string, kept both decoded and as it is written. + * + * ``` + * "\x41\x42" // value: "AB", raw: "\"\x41\x42\"" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class StringLiteralNode extends LiteralNode +final class StringLiteralNode extends ScalarNode { /** * @param int<0, max> $offset From af3f8d49b3585ba9403eb46c5d9d2147592ce994 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:28:42 +0300 Subject: [PATCH 041/119] Add variable literal node --- libs/types/src/Literal/VariableLiteralNode.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/types/src/Literal/VariableLiteralNode.php b/libs/types/src/Literal/VariableLiteralNode.php index 4219f6c..686110b 100644 --- a/libs/types/src/Literal/VariableLiteralNode.php +++ b/libs/types/src/Literal/VariableLiteralNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type\Literal; /** + * A variable, standing in a condition or naming a callable parameter. + * + * ``` + * $value // value: "value", raw: "$value" + * ``` + * * @template-extends LiteralNode * * @phpstan-consistent-constructor From d1a0320ed9e0172be94bf7d9ecd86299bda6ba3d Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:46:10 +0300 Subject: [PATCH 042/119] Reorganize shape fields --- libs/types/src/Shape/ClassConstFieldNode.php | 39 -------------- .../src/Shape/ClassConstMaskFieldNode.php | 39 -------------- libs/types/src/Shape/ComplexFieldNode.php | 48 +++++++++++++++++ libs/types/src/Shape/ConstMaskFieldNode.php | 36 ------------- libs/types/src/Shape/ExplicitFieldNode.php | 19 +++++-- libs/types/src/Shape/FieldNode.php | 30 +++++++---- libs/types/src/Shape/FieldsListNode.php | 16 ++++++ libs/types/src/Shape/ImplicitFieldNode.php | 9 ++++ libs/types/src/Shape/NamedFieldNode.php | 12 ++++- libs/types/src/Shape/NumericFieldNode.php | 36 ------------- libs/types/src/Shape/ScalarFieldNode.php | 53 +++++++++++++++++++ .../src/Shape/SimpleFieldNodeInterface.php | 31 +++++++++++ libs/types/src/Shape/StringNamedFieldNode.php | 36 ------------- 13 files changed, 201 insertions(+), 203 deletions(-) delete mode 100644 libs/types/src/Shape/ClassConstFieldNode.php delete mode 100644 libs/types/src/Shape/ClassConstMaskFieldNode.php create mode 100644 libs/types/src/Shape/ComplexFieldNode.php delete mode 100644 libs/types/src/Shape/ConstMaskFieldNode.php delete mode 100644 libs/types/src/Shape/NumericFieldNode.php create mode 100644 libs/types/src/Shape/ScalarFieldNode.php create mode 100644 libs/types/src/Shape/SimpleFieldNodeInterface.php delete mode 100644 libs/types/src/Shape/StringNamedFieldNode.php diff --git a/libs/types/src/Shape/ClassConstFieldNode.php b/libs/types/src/Shape/ClassConstFieldNode.php deleted file mode 100644 index 01e3442..0000000 --- a/libs/types/src/Shape/ClassConstFieldNode.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -final class ClassConstFieldNode extends ExplicitFieldNode -{ - /** - * @param int<0, max> $offset - */ - public function __construct( - ClassConstNode $key, - TypeNode $type, - bool $isOptional = false, - int $offset = 0, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - offset: $offset, - ); - } - - public function getIndex(): string - { - return \vsprintf('%s::%s', [ - $this->key->class->toString(), - $this->key->constant->toString(), - ]); - } -} diff --git a/libs/types/src/Shape/ClassConstMaskFieldNode.php b/libs/types/src/Shape/ClassConstMaskFieldNode.php deleted file mode 100644 index 87fafb3..0000000 --- a/libs/types/src/Shape/ClassConstMaskFieldNode.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -final class ClassConstMaskFieldNode extends ExplicitFieldNode -{ - /** - * @param int<0, max> $offset - */ - public function __construct( - ClassConstMaskNode $key, - TypeNode $type, - bool $isOptional = false, - int $offset = 0, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - offset: $offset, - ); - } - - public function getIndex(): string - { - return \vsprintf('%s::%s*', [ - $this->key->class->toString(), - $this->key->constant?->toString(), - ]); - } -} diff --git a/libs/types/src/Shape/ComplexFieldNode.php b/libs/types/src/Shape/ComplexFieldNode.php new file mode 100644 index 0000000..d722a75 --- /dev/null +++ b/libs/types/src/Shape/ComplexFieldNode.php @@ -0,0 +1,48 @@ + + */ +final class ComplexFieldNode extends ExplicitFieldNode +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + TypeNode $key, + TypeNode $type, + bool $isOptional = false, + int $offset = 0, + ) { + parent::__construct( + key: $key, + type: $type, + isOptional: $isOptional, + offset: $offset, + ); + } +} diff --git a/libs/types/src/Shape/ConstMaskFieldNode.php b/libs/types/src/Shape/ConstMaskFieldNode.php deleted file mode 100644 index b8b95fb..0000000 --- a/libs/types/src/Shape/ConstMaskFieldNode.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ -final class ConstMaskFieldNode extends ExplicitFieldNode -{ - /** - * @param int<0, max> $offset - */ - public function __construct( - ConstMaskNode $key, - TypeNode $type, - bool $isOptional = false, - int $offset = 0, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - offset: $offset, - ); - } - - public function getIndex(): string - { - return $this->key->name . '*'; - } -} diff --git a/libs/types/src/Shape/ExplicitFieldNode.php b/libs/types/src/Shape/ExplicitFieldNode.php index 2e2cb41..3530ad2 100644 --- a/libs/types/src/Shape/ExplicitFieldNode.php +++ b/libs/types/src/Shape/ExplicitFieldNode.php @@ -7,6 +7,16 @@ use TypeLang\Type\TypeNode; /** + * A field of a shape written with a key of its own. + * + * What the key may be is what tells the children of this class apart: A + * {@see NamedFieldNode} carries a bare word, a {@see ScalarFieldNode} a scalar + * value and a {@see ComplexFieldNode} a reference that has to be read. + * + * A key that comes down to a string of its own is marked by the + * {@see SimpleFieldNodeInterface}, which is what a shape tells its keys + * apart by. + * * @template TKey of mixed */ abstract class ExplicitFieldNode extends FieldNode @@ -16,6 +26,10 @@ abstract class ExplicitFieldNode extends FieldNode */ public function __construct( /** + * The key a field is written with. Its kind is what the children of + * this class differ in, so a child narrows it down to a node of its + * own. + * * @var TKey */ public mixed $key, @@ -29,9 +43,4 @@ public function __construct( offset: $offset, ); } - - /** - * Gets a pretty-printed string representation of the key - */ - abstract public function getIndex(): string; } diff --git a/libs/types/src/Shape/FieldNode.php b/libs/types/src/Shape/FieldNode.php index 0791007..12a09d4 100644 --- a/libs/types/src/Shape/FieldNode.php +++ b/libs/types/src/Shape/FieldNode.php @@ -7,6 +7,16 @@ use TypeLang\Type\Node; use TypeLang\Type\TypeNode; +/** + * A single field of a shape. + * + * A shape is written either way throughout, never both at once. + * + * ``` + * array{name: string, id: int} // fields written with a key + * array{string, int} // fields written with none + * ``` + */ abstract class FieldNode extends Node { /** @@ -14,20 +24,18 @@ abstract class FieldNode extends Node */ public function __construct( public TypeNode $type, + /** + * Whether a field may be absent, written as a `?` in front of + * the colon. + * + * ``` + * array{name: string} // false + * array{name?: string} // true + * ``` + */ public bool $isOptional = false, int $offset = 0, ) { parent::__construct($offset); } - - /** - * Returns {@see true} in case of the field is an instance of the - * passed class. - * - * @param class-string $class - */ - public function is(string $class): bool - { - return $this instanceof $class; - } } diff --git a/libs/types/src/Shape/FieldsListNode.php b/libs/types/src/Shape/FieldsListNode.php index 34814f8..e2390a2 100644 --- a/libs/types/src/Shape/FieldsListNode.php +++ b/libs/types/src/Shape/FieldsListNode.php @@ -7,6 +7,12 @@ use TypeLang\Type\NodeList; /** + * The fields of a shape, in the order they are written in. + * + * An empty list is a shape written with no fields at all, which is a thing of + * its own: An `array` carries no list, while an `array{}` carries an empty one + * (see {@see \TypeLang\Type\NamedTypeNode::$fields}). + * * @template-extends NodeList */ final class FieldsListNode extends NodeList @@ -17,6 +23,16 @@ final class FieldsListNode extends NodeList */ public function __construct( array $list = [], + /** + * Whether the shape is written with the fields it lists and no other. + * An unsealed one ends in a `...`, which says that whatever else it + * carries was left unsaid. + * + * ``` + * array{a: int} // sealed + * array{a: int, ...} // unsealed + * ``` + */ public bool $isSealed = true, int $offset = 0, ) { diff --git a/libs/types/src/Shape/ImplicitFieldNode.php b/libs/types/src/Shape/ImplicitFieldNode.php index 043e7a6..385face 100644 --- a/libs/types/src/Shape/ImplicitFieldNode.php +++ b/libs/types/src/Shape/ImplicitFieldNode.php @@ -4,4 +4,13 @@ namespace TypeLang\Type\Shape; +/** + * A field of a shape written with no key at all, the way an element of + * a plain list is. + * + * ``` + * array{int, string} + * // ^^^ ^^^^^^ two implicit fields + * ``` + */ final class ImplicitFieldNode extends FieldNode {} diff --git a/libs/types/src/Shape/NamedFieldNode.php b/libs/types/src/Shape/NamedFieldNode.php index bf2e2cb..17a7ed1 100644 --- a/libs/types/src/Shape/NamedFieldNode.php +++ b/libs/types/src/Shape/NamedFieldNode.php @@ -8,9 +8,19 @@ use TypeLang\Type\TypeNode; /** + * A field of a shape keyed by a bare word. + * + * ``` + * array{name: string} + * // ^^^^ the key + * ``` + * + * A `true`, a `false` and a `null` are bare words as well: In the place of + * a key they name nothing but themselves. + * * @template-extends ExplicitFieldNode */ -final class NamedFieldNode extends ExplicitFieldNode +final class NamedFieldNode extends ExplicitFieldNode implements SimpleFieldNodeInterface { /** * @param int<0, max> $offset diff --git a/libs/types/src/Shape/NumericFieldNode.php b/libs/types/src/Shape/NumericFieldNode.php deleted file mode 100644 index ed5cb8f..0000000 --- a/libs/types/src/Shape/NumericFieldNode.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ -final class NumericFieldNode extends ExplicitFieldNode -{ - /** - * @param int<0, max> $offset - */ - public function __construct( - IntLiteralNode $key, - TypeNode $type, - bool $isOptional = false, - int $offset = 0, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - offset: $offset, - ); - } - - public function getIndex(): string - { - return (string) $this->key->value; - } -} diff --git a/libs/types/src/Shape/ScalarFieldNode.php b/libs/types/src/Shape/ScalarFieldNode.php new file mode 100644 index 0000000..fba56fb --- /dev/null +++ b/libs/types/src/Shape/ScalarFieldNode.php @@ -0,0 +1,53 @@ + + */ +final class ScalarFieldNode extends ExplicitFieldNode implements SimpleFieldNodeInterface +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + IntLiteralNode|StringLiteralNode $key, + TypeNode $type, + bool $isOptional = false, + int $offset = 0, + ) { + parent::__construct( + key: $key, + type: $type, + isOptional: $isOptional, + offset: $offset, + ); + } + + public function getIndex(): string + { + /** @var non-empty-string */ + return (string) $this->key->value; + } +} diff --git a/libs/types/src/Shape/SimpleFieldNodeInterface.php b/libs/types/src/Shape/SimpleFieldNodeInterface.php new file mode 100644 index 0000000..74ff2d8 --- /dev/null +++ b/libs/types/src/Shape/SimpleFieldNodeInterface.php @@ -0,0 +1,31 @@ + - */ -final class StringNamedFieldNode extends ExplicitFieldNode -{ - /** - * @param int<0, max> $offset - */ - public function __construct( - StringLiteralNode $key, - TypeNode $type, - bool $isOptional = false, - int $offset = 0, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - offset: $offset, - ); - } - - public function getIndex(): string - { - return $this->key->value; - } -} From 4eebfde5410b393b96171b6b1d899ec233222c58 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:47:44 +0300 Subject: [PATCH 043/119] Add comments with examples to CallableParameterListNode --- libs/types/src/Callable/CallableParameterListNode.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/types/src/Callable/CallableParameterListNode.php b/libs/types/src/Callable/CallableParameterListNode.php index 88783ea..1706ae8 100644 --- a/libs/types/src/Callable/CallableParameterListNode.php +++ b/libs/types/src/Callable/CallableParameterListNode.php @@ -7,6 +7,14 @@ use TypeLang\Type\NodeList; /** + * Parameters of a callable, in the order they are written in. + * + * ``` + * callable(int, string ...$rest) + * // ^^^ ^^^^^^^^^^^^^^^ + * // two parameters + * ``` + * * @template-extends NodeList */ final class CallableParameterListNode extends NodeList {} From 8b3b428081bd69bc1bde852809b47efc18cb5855 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:50:27 +0300 Subject: [PATCH 044/119] Split `variable` and `this` nodes --- .../types/src/Literal/VariableLiteralNode.php | 28 ------------------- libs/types/src/ThisNode.php | 18 ++++++++++++ 2 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 libs/types/src/Literal/VariableLiteralNode.php create mode 100644 libs/types/src/ThisNode.php diff --git a/libs/types/src/Literal/VariableLiteralNode.php b/libs/types/src/Literal/VariableLiteralNode.php deleted file mode 100644 index 686110b..0000000 --- a/libs/types/src/Literal/VariableLiteralNode.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * @phpstan-consistent-constructor - */ -final class VariableLiteralNode extends LiteralNode -{ - /** - * @param non-empty-string $value - * @param int<0, max> $offset - */ - public function __construct(string $value, int $offset = 0) - { - parent::__construct($value, '$' . $value, $offset); - } -} diff --git a/libs/types/src/ThisNode.php b/libs/types/src/ThisNode.php new file mode 100644 index 0000000..00f0640 --- /dev/null +++ b/libs/types/src/ThisNode.php @@ -0,0 +1,18 @@ + Date: Mon, 7 Sep 2026 21:59:16 +0300 Subject: [PATCH 045/119] Add comments to the types list AST node --- libs/types/src/TypesListNode.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libs/types/src/TypesListNode.php b/libs/types/src/TypesListNode.php index 013ea17..721b3a3 100644 --- a/libs/types/src/TypesListNode.php +++ b/libs/types/src/TypesListNode.php @@ -5,6 +5,16 @@ namespace TypeLang\Type; /** + * A list of the wrapped type, written in the legacy `[]` notation. + * + * ``` + * Some\Any[] + * ^^^^^^^^ the type the list is made of + * + * Some\Any[][] + * ^^^^^^^^^^ a list the outer list is made of + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends WrappingTypeNode From 61d94b4882665d9b230a10fee07c70a7593fad3e Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:59:33 +0300 Subject: [PATCH 046/119] Add comments to the type AST node --- libs/types/src/TypeNode.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/types/src/TypeNode.php b/libs/types/src/TypeNode.php index 5285943..145ca6e 100644 --- a/libs/types/src/TypeNode.php +++ b/libs/types/src/TypeNode.php @@ -4,4 +4,7 @@ namespace TypeLang\Type; +/** + * A node standing where a type is expected. + */ abstract class TypeNode extends Node {} From 348e663b6da82ab94b5407f08eafa2b1b9972251 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 21:59:53 +0300 Subject: [PATCH 047/119] Add example comment to the ternary AST node --- libs/types/src/TernaryExpressionNode.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libs/types/src/TernaryExpressionNode.php b/libs/types/src/TernaryExpressionNode.php index 5f5fd64..bb6a400 100644 --- a/libs/types/src/TernaryExpressionNode.php +++ b/libs/types/src/TernaryExpressionNode.php @@ -6,6 +6,16 @@ use TypeLang\Type\Condition\Condition; +/** + * A type chosen by a condition. + * + * ``` + * ($value is int ? string : bool) + * ^^^^^^^^^^^^^ the condition + * ^^^^^^ the type it holds for + * ^^^^ the type it does not + * ``` + */ final class TernaryExpressionNode extends TypeNode { /** From 6a4b7e3e55b4bdce28b1c9ea4ff624c5f5fd5ab5 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:00:13 +0300 Subject: [PATCH 048/119] Add example comment to the named type AST node --- libs/types/src/NamedTypeNode.php | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/libs/types/src/NamedTypeNode.php b/libs/types/src/NamedTypeNode.php index b445c4a..ab4e25f 100644 --- a/libs/types/src/NamedTypeNode.php +++ b/libs/types/src/NamedTypeNode.php @@ -7,6 +7,24 @@ use TypeLang\Type\Shape\FieldsListNode; use TypeLang\Type\Template\TemplateArgumentListNode; +/** + * A type referenced by name, along with whatever the name is followed by. + * + * ``` + * Some\Any + * ^^^^^^^^ the name + * ^^^^^^^^^^^^^ the template arguments + * + * array{name: string} + * ^^^^^ the name + * ^^^^^^^^^^^^^^ the shape fields + * + * object{name: string, ...} + * ^^^^^^ the name + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the shape fields + * ^^^^^^^^^^^^^^^ the template arguments + * ``` + */ final class NamedTypeNode extends TypeNode { /** @@ -14,7 +32,29 @@ final class NamedTypeNode extends TypeNode */ public function __construct( public Name $name, + /** + * Template arguments of a type, or {@see null} in case of the name is + * followed by none. + * + * The list is never empty: An `Some<>` is no type. + * + * ``` + * Some // null + * Some // a list of 1 argument + * ``` + */ public ?TemplateArgumentListNode $arguments = null, + /** + * Shape fields of a type, or {@see null} in case of the name is + * followed by none. An empty list is not the same as a {@see null}: + * A shape may well be written with no fields at all. + * + * ``` + * array // null: no shape + * array{} // an empty list: a shape of no fields + * array{a: int} // a list of 1 field + * ``` + */ public ?FieldsListNode $fields = null, int $offset = 0, ) { From 0a69b1800544ffd309c4c8d7d637ac227dc29266 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:00:45 +0300 Subject: [PATCH 049/119] Add variable AST node with comments and usage examples --- libs/types/src/VariableNode.php | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 libs/types/src/VariableNode.php diff --git a/libs/types/src/VariableNode.php b/libs/types/src/VariableNode.php new file mode 100644 index 0000000..11fb7e8 --- /dev/null +++ b/libs/types/src/VariableNode.php @@ -0,0 +1,36 @@ + $offset + */ + public function __construct( + /** + * The name of the variable, written without the `$` it begins with. + */ + public Identifier $name, + int $offset = 0, + ) { + parent::__construct($offset); + } +} From 9986f9aa1f441c8ae32b76c46f90529187f82138 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:01:14 +0300 Subject: [PATCH 050/119] Add mask node and wildcard AST node classes --- libs/types/src/MaskNode.php | 121 ++++++++++++++++++++++++++++++++ libs/types/src/WildcardNode.php | 40 +++++++++++ 2 files changed, 161 insertions(+) create mode 100644 libs/types/src/MaskNode.php create mode 100644 libs/types/src/WildcardNode.php diff --git a/libs/types/src/MaskNode.php b/libs/types/src/MaskNode.php new file mode 100644 index 0000000..d58d816 --- /dev/null +++ b/libs/types/src/MaskNode.php @@ -0,0 +1,121 @@ + + */ +final class MaskNode extends NodeList implements \Stringable +{ + /** + * @param iterable $items + * @param int<0, max> $offset + */ + public function __construct(iterable $items, int $offset = 0) + { + parent::__construct($items, $offset); + + if (\count($this->items) < 1) { + throw new \InvalidArgumentException('A mask must have at least one item'); + } + } + + #[\Override] + public function offsetUnset(mixed $offset): void + { + parent::offsetUnset($offset); + + if (\count($this->items) < 1) { + throw new \UnderflowException('A mask must have at least one item'); + } + } + + /** + * Gets the literal segments of a mask, in the order they are written in, + * leaving every wildcard out. + * + * ``` + * // Some\Any::BAR*BAZ*SOME + * $mask->getSegments(); // [BAR, BAZ, SOME] + * + * // Some\Any::* + * $mask->getSegments(); // [] + * ``` + * + * @return list + */ + public function getSegments(): array + { + $result = []; + + foreach ($this->items as $item) { + if ($item instanceof Identifier) { + $result[] = $item; + } + } + + return $result; + } + + /** + * Gets the literal segments of a mask as strings. + * + * @return list + */ + public function getSegmentsAsStrings(): array + { + $result = []; + + foreach ($this->items as $item) { + if ($item instanceof Identifier) { + $result[] = $item->value; + } + } + + return $result; + } + + /** + * Returns a mask as the string it is written as. + * + * @return non-empty-string + */ + public function toString(): string + { + $result = ''; + + foreach ($this->items as $item) { + $result .= $item->toString(); + } + + /** @var non-empty-string */ + return $result; + } + + /** + * @return non-empty-string + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/libs/types/src/WildcardNode.php b/libs/types/src/WildcardNode.php new file mode 100644 index 0000000..0ca40cc --- /dev/null +++ b/libs/types/src/WildcardNode.php @@ -0,0 +1,40 @@ + + * // ^ an unspecified template argument + * + * Some\Any::CONST_* + * // ^ an unspecified part of a constant name + * ``` + */ +final class WildcardNode extends TypeNode implements \Stringable +{ + /** + * @var non-empty-string + */ + public const CHAR = '*'; + + /** + * @return non-empty-string + */ + public function toString(): string + { + return self::CHAR; + } + + /** + * @return non-empty-string + */ + public function __toString(): string + { + return self::CHAR; + } +} From 00a57ad96c2c904a4a62bf1b594eb1ca0b4c727e Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:01:41 +0300 Subject: [PATCH 051/119] Rewrite constants and const mask nodes --- libs/types/src/ClassConstMaskNode.php | 11 +++++++- libs/types/src/ClassConstNode.php | 11 ++++++++ libs/types/src/ConstMaskNode.php | 40 ++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/libs/types/src/ClassConstMaskNode.php b/libs/types/src/ClassConstMaskNode.php index 9548c6a..50261ce 100644 --- a/libs/types/src/ClassConstMaskNode.php +++ b/libs/types/src/ClassConstMaskNode.php @@ -4,6 +4,15 @@ namespace TypeLang\Type; +/** + * A class constant referenced by a name written in part. + * + * ``` + * Some\Any::CONST_* + * ^^^^^^^^ the class the constant belongs to + * ^^^^^^^ the mask its name is written as + * ``` + */ final class ClassConstMaskNode extends TypeNode { /** @@ -11,7 +20,7 @@ final class ClassConstMaskNode extends TypeNode */ public function __construct( public Name $class, - public ?Identifier $constant = null, + public MaskNode $mask = new MaskNode([new WildcardNode()]), int $offset = 0, ) { parent::__construct($offset); diff --git a/libs/types/src/ClassConstNode.php b/libs/types/src/ClassConstNode.php index 6b4e5d7..04e98a1 100644 --- a/libs/types/src/ClassConstNode.php +++ b/libs/types/src/ClassConstNode.php @@ -4,6 +4,17 @@ namespace TypeLang\Type; +/** + * A constant of a class, referenced by its whole name. + * + * ``` + * Some\Any::CONST_NAME + * ^^^^^^^^ the class the constant belongs to + * ^^^^^^^^^^ the name of the constant + * ``` + * + * A name written in part is a {@see ClassConstMaskNode} instead. + */ final class ClassConstNode extends TypeNode { /** diff --git a/libs/types/src/ConstMaskNode.php b/libs/types/src/ConstMaskNode.php index b26f964..51da2d1 100644 --- a/libs/types/src/ConstMaskNode.php +++ b/libs/types/src/ConstMaskNode.php @@ -4,13 +4,51 @@ namespace TypeLang\Type; +/** + * A global constant referenced by a name written in part. + * + * ``` + * Some\Any\SOME_* + * ^^^^^^^^ the namespace the constant belongs to + * ^^^^^^ the mask its name is written as + * + * *_SOME + * ^^^^^^ a mask alone: The name may be left unsaid from its very beginning + * ``` + */ final class ConstMaskNode extends TypeNode { /** * @param int<0, max> $offset */ public function __construct( - public Name $name, + /** + * The name of the constant, written in part. + */ + public MaskNode $mask, + /** + * The namespace the constant belongs to, or {@see null} in case of it + * is written with none. Always a relative name: The leading separator + * a fully qualified reference begins in is the + * {@see $isFullyQualified} below. + * + * ``` + * SOME_* // null + * Some\Any\SOME_* // "Some\Any" + * ``` + */ + public ?Name $namespace = null, + /** + * Whether the reference is written with the leading separator that + * says it is to be read from the root namespace. + * + * ``` + * Some\SOME_* // false + * \Some\SOME_* // true + * \SOME_* // true, with no namespace at all + * ``` + */ + public bool $isFullyQualified = Name::IS_FULLY_QUALIFIED_DEFAULT_VALUE, int $offset = 0, ) { parent::__construct($offset); From 721b777e095d8ebe7884c47912d4e02681479687 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:20:43 +0300 Subject: [PATCH 052/119] Fix callable parameter var reference and add comment with example --- .../src/Callable/CallableParameterNode.php | 42 +++++++++++++- .../src/Template/TemplateBoundEdgeNode.php | 33 +++++++++++ .../Template/TemplateParameterListNode.php | 20 +++++++ .../src/Template/TemplateParameterNode.php | 58 +++++++++++++++++++ 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 libs/types/src/Template/TemplateBoundEdgeNode.php create mode 100644 libs/types/src/Template/TemplateParameterListNode.php create mode 100644 libs/types/src/Template/TemplateParameterNode.php diff --git a/libs/types/src/Callable/CallableParameterNode.php b/libs/types/src/Callable/CallableParameterNode.php index 0c5f6f5..d147876 100644 --- a/libs/types/src/Callable/CallableParameterNode.php +++ b/libs/types/src/Callable/CallableParameterNode.php @@ -4,20 +4,58 @@ namespace TypeLang\Type\Callable; -use TypeLang\Type\Literal\VariableLiteralNode; use TypeLang\Type\Node; use TypeLang\Type\TypeNode; +use TypeLang\Type\VariableNode; +/** + * A single parameter of a callable. + * + * ``` + * callable(int &$byRef, string ...$rest, bool $flag = ) + * // ^^^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ + * // by reference variadic optional + * ``` + */ final class CallableParameterNode extends Node { /** * @param int<0, max> $offset */ public function __construct( + /** + * Type of parameter. A statement always writes one, so a + * {@see null} only ever reaches a tree that is built by hand, where + * a name alone is enough to tell one parameter from another. + */ public ?TypeNode $type = null, - public ?VariableLiteralNode $name = null, + /** + * Name of a parameter, or {@see null} in case of it is written + * without one. + * + * ``` + * callable(int) // null + * callable(int $value) // a name + * ``` + */ + public ?VariableNode $name = null, + /** + * Whether a parameter is taken by reference. + * + * It is written as an `&` behind the type, like the one of + * a `callable(int &$byRef)`. + */ public bool $isOutput = false, + /** + * Whether a parameter takes every argument left, written as a `...`: + * A `callable(string ...$rest)`. + */ public bool $isVariadic = false, + /** + * Whether a parameter may be left out, written as a trailing `=`: + * A `callable(bool $flag =)`. A variadic one is optional already, so + * the two are never both set. + */ public bool $isOptional = false, int $offset = 0, ) { diff --git a/libs/types/src/Template/TemplateBoundEdgeNode.php b/libs/types/src/Template/TemplateBoundEdgeNode.php new file mode 100644 index 0000000..b647e14 --- /dev/null +++ b/libs/types/src/Template/TemplateBoundEdgeNode.php @@ -0,0 +1,33 @@ +(T): T + * // ^^^^^^^ the upper bound + * // ^^^^^^^^^ the lower bound + * ``` + */ +final class TemplateBoundEdgeNode extends Node +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + public Identifier $operator, + public TypeNode $type, + int $offset = 0, + ) { + parent::__construct($offset); + } +} diff --git a/libs/types/src/Template/TemplateParameterListNode.php b/libs/types/src/Template/TemplateParameterListNode.php new file mode 100644 index 0000000..cc72249 --- /dev/null +++ b/libs/types/src/Template/TemplateParameterListNode.php @@ -0,0 +1,20 @@ +(T): U + * // ^ ^^^^^^^^^ + * // two parameters + * ``` + * + * @template-extends NodeList + */ +final class TemplateParameterListNode extends NodeList {} diff --git a/libs/types/src/Template/TemplateParameterNode.php b/libs/types/src/Template/TemplateParameterNode.php new file mode 100644 index 0000000..5b04fee --- /dev/null +++ b/libs/types/src/Template/TemplateParameterNode.php @@ -0,0 +1,58 @@ +(T): U + * // ^ a name and nothing else + * // ^^^^^^^^^ the upper bound + * // ^^^^^^^^^ the lower bound + * // ^^^^^ the default + * ``` + */ +final class TemplateParameterNode extends Node +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + public Identifier $name, + /** + * The end the parameter is bounded at from above, written with an + * `of` or an `as`. + * + * The argument is to be a subtype of it. + */ + public ?TemplateBoundEdgeNode $upper = null, + /** + * The end the parameter is bounded at from below, written with + * a `super`. + * + * The argument is to be a supertype of it. + */ + public ?TemplateBoundEdgeNode $lower = null, + /** + * The type the parameter takes when no argument is passed, written + * with an `=`. + * + * It bounds nothing. A bound says what an argument may be, while + * a default says what it becomes when left unsaid. + */ + public ?TypeNode $default = null, + int $offset = 0, + ) { + parent::__construct($offset); + } +} From cd1ea667aeb5c23a61c2c0c3cea70d86efabc85a Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:30:57 +0300 Subject: [PATCH 053/119] Drop gt, gte, lt and lte conditions --- libs/types/src/Condition/GreaterThanConditionNode.php | 7 ------- .../src/Condition/GreaterThanOrEqualConditionNode.php | 7 ------- libs/types/src/Condition/LessThanConditionNode.php | 7 ------- libs/types/src/Condition/LessThanOrEqualConditionNode.php | 7 ------- 4 files changed, 28 deletions(-) delete mode 100644 libs/types/src/Condition/GreaterThanConditionNode.php delete mode 100644 libs/types/src/Condition/GreaterThanOrEqualConditionNode.php delete mode 100644 libs/types/src/Condition/LessThanConditionNode.php delete mode 100644 libs/types/src/Condition/LessThanOrEqualConditionNode.php diff --git a/libs/types/src/Condition/GreaterThanConditionNode.php b/libs/types/src/Condition/GreaterThanConditionNode.php deleted file mode 100644 index 5826ae6..0000000 --- a/libs/types/src/Condition/GreaterThanConditionNode.php +++ /dev/null @@ -1,7 +0,0 @@ - Date: Mon, 7 Sep 2026 22:31:25 +0300 Subject: [PATCH 054/119] Improve comparison nodes --- libs/types/src/Condition/Condition.php | 15 +++++++++++++-- libs/types/src/Condition/EqualConditionNode.php | 7 +++++++ .../types/src/Condition/NotEqualConditionNode.php | 7 +++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/libs/types/src/Condition/Condition.php b/libs/types/src/Condition/Condition.php index 55f726e..948835b 100644 --- a/libs/types/src/Condition/Condition.php +++ b/libs/types/src/Condition/Condition.php @@ -6,15 +6,26 @@ use TypeLang\Type\Node; use TypeLang\Type\TypeNode; +use TypeLang\Type\VariableNode; +/** + * The question a ternary type asks, that is, the two sides of it and, + * in the class itself, the operator between them. + * + * ``` + * ($value is int ? string : bool) + * //^^^^^^ ^^ ^^^ + * //subject target + * ``` + */ abstract class Condition extends Node { /** * @param int<0, max> $offset */ public function __construct( - public TypeNode $subject, - public TypeNode $target, + public TypeNode|VariableNode $subject, + public TypeNode|VariableNode $target, int $offset = 0, ) { parent::__construct($offset); diff --git a/libs/types/src/Condition/EqualConditionNode.php b/libs/types/src/Condition/EqualConditionNode.php index 850de39..ac9a7a9 100644 --- a/libs/types/src/Condition/EqualConditionNode.php +++ b/libs/types/src/Condition/EqualConditionNode.php @@ -4,4 +4,11 @@ namespace TypeLang\Type\Condition; +/** + * Whether the subject is the target. + * + * ``` + * ($value is int ? string : bool) + * ``` + */ final class EqualConditionNode extends Condition {} diff --git a/libs/types/src/Condition/NotEqualConditionNode.php b/libs/types/src/Condition/NotEqualConditionNode.php index 7c9241d..ba26420 100644 --- a/libs/types/src/Condition/NotEqualConditionNode.php +++ b/libs/types/src/Condition/NotEqualConditionNode.php @@ -4,4 +4,11 @@ namespace TypeLang\Type\Condition; +/** + * Whether the subject is anything but the target. + * + * ``` + * ($value is not int ? string : bool) + * ``` + */ final class NotEqualConditionNode extends Condition {} From ca68cb38b643c3d30a959b1dee0cd10e07beea57 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:31:56 +0300 Subject: [PATCH 055/119] Remove unnecessary comments from literal nodes --- libs/types/src/Literal/LiteralNode.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/libs/types/src/Literal/LiteralNode.php b/libs/types/src/Literal/LiteralNode.php index 4d8cbc6..8238d6e 100644 --- a/libs/types/src/Literal/LiteralNode.php +++ b/libs/types/src/Literal/LiteralNode.php @@ -10,7 +10,7 @@ * A value standing where a type is expected, kept both as PHP sees it * and as it is written. * - * The two are not the same thing: A value is what PHP would compute, while + * The two are not the same thing. A value is what PHP would compute, while * a raw is the very text it was computed from, quotes, radix and all. * * ``` @@ -32,15 +32,9 @@ abstract class LiteralNode extends TypeNode implements LiteralNodeInterface */ public function __construct( /** - * The value as PHP sees it. - * * @var TValue */ public readonly mixed $value, - /** - * The very text the value was read from, so that a statement can be - * written back the way it came. - */ public readonly string $raw, int $offset = 0, ) { From f678c988ff0553112a9b572a5a45b3e04d225928 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:32:14 +0300 Subject: [PATCH 056/119] Improve shape field comments --- libs/types/src/Shape/ComplexFieldNode.php | 6 ++++-- libs/types/src/Shape/ExplicitFieldNode.php | 11 ++++++----- libs/types/src/Shape/FieldsListNode.php | 8 +++++--- libs/types/src/Shape/NamedFieldNode.php | 2 +- libs/types/src/Shape/ScalarFieldNode.php | 6 ++++-- libs/types/src/Shape/SimpleFieldNodeInterface.php | 15 +++++++++------ 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/libs/types/src/Shape/ComplexFieldNode.php b/libs/types/src/Shape/ComplexFieldNode.php index d722a75..72229d7 100644 --- a/libs/types/src/Shape/ComplexFieldNode.php +++ b/libs/types/src/Shape/ComplexFieldNode.php @@ -22,8 +22,10 @@ * ``` * * Unlike a {@see SimpleFieldNodeInterface}, such a key comes down to no - * string of its own: Whether two references name the same constant depends on - * what the names are resolved against, which a written statement does not say. + * string of its own. + * + * Whether two references name the same constant depends on what the names + * are resolved against, and a written statement does not say that. * * @template-extends ExplicitFieldNode */ diff --git a/libs/types/src/Shape/ExplicitFieldNode.php b/libs/types/src/Shape/ExplicitFieldNode.php index 3530ad2..db1f22c 100644 --- a/libs/types/src/Shape/ExplicitFieldNode.php +++ b/libs/types/src/Shape/ExplicitFieldNode.php @@ -9,13 +9,14 @@ /** * A field of a shape written with a key of its own. * - * What the key may be is what tells the children of this class apart: A - * {@see NamedFieldNode} carries a bare word, a {@see ScalarFieldNode} a scalar - * value and a {@see ComplexFieldNode} a reference that has to be read. + * What the key may be is what tells the children of this class apart. + * A {@see NamedFieldNode} carries a bare word, a {@see ScalarFieldNode} + * a scalar value and a {@see ComplexFieldNode} a reference that has to + * be read. * * A key that comes down to a string of its own is marked by the - * {@see SimpleFieldNodeInterface}, which is what a shape tells its keys - * apart by. + * {@see SimpleFieldNodeInterface}. That string is what a shape tells its + * keys apart by. * * @template TKey of mixed */ diff --git a/libs/types/src/Shape/FieldsListNode.php b/libs/types/src/Shape/FieldsListNode.php index e2390a2..fc54e08 100644 --- a/libs/types/src/Shape/FieldsListNode.php +++ b/libs/types/src/Shape/FieldsListNode.php @@ -9,9 +9,11 @@ /** * The fields of a shape, in the order they are written in. * - * An empty list is a shape written with no fields at all, which is a thing of - * its own: An `array` carries no list, while an `array{}` carries an empty one - * (see {@see \TypeLang\Type\NamedTypeNode::$fields}). + * An empty list is a shape written with no fields at all, which is a thing + * of its own. + * + * An `array` carries no list, while an `array{}` carries an empty one. + * See the {@see \TypeLang\Type\NamedTypeNode::$fields}. * * @template-extends NodeList */ diff --git a/libs/types/src/Shape/NamedFieldNode.php b/libs/types/src/Shape/NamedFieldNode.php index 17a7ed1..caba21b 100644 --- a/libs/types/src/Shape/NamedFieldNode.php +++ b/libs/types/src/Shape/NamedFieldNode.php @@ -15,7 +15,7 @@ * // ^^^^ the key * ``` * - * A `true`, a `false` and a `null` are bare words as well: In the place of + * A `true`, a `false` and a `null` are bare words as well. In the place of * a key they name nothing but themselves. * * @template-extends ExplicitFieldNode diff --git a/libs/types/src/Shape/ScalarFieldNode.php b/libs/types/src/Shape/ScalarFieldNode.php index fba56fb..645aa01 100644 --- a/libs/types/src/Shape/ScalarFieldNode.php +++ b/libs/types/src/Shape/ScalarFieldNode.php @@ -20,8 +20,10 @@ * ``` * * A key is a number or a string and nothing else, the way a key of an array - * is. A `true` and a `null` are keys of a {@see NamedFieldNode} instead: In - * the place of a key they are read as the words they are written with, not as + * is. + * + * A `true` and a `null` are keys of a {@see NamedFieldNode} instead. In the + * place of a key they are read as the words they are written with, not as * the values they name elsewhere. * * @template-extends ExplicitFieldNode diff --git a/libs/types/src/Shape/SimpleFieldNodeInterface.php b/libs/types/src/Shape/SimpleFieldNodeInterface.php index 74ff2d8..3984ed9 100644 --- a/libs/types/src/Shape/SimpleFieldNodeInterface.php +++ b/libs/types/src/Shape/SimpleFieldNodeInterface.php @@ -5,14 +5,17 @@ namespace TypeLang\Type\Shape; /** - * A field the key of which is a simple thing: A word or a scalar value, and - * nothing that has to be read to be told from another. + * A field the key of which is a simple thing. + * + * A word or a scalar value is such a thing, while anything that has to be + * read to be told from another is not. * * Such a key comes down to a string, so two of them are told apart by that - * string alone - which is what a shape needs to refuse a key it already - * carries. A key that is no simple thing (see {@see ComplexFieldNode}) offers - * no such string, since whether two references denote the same constant is - * beyond what a written statement says. + * string alone. That is what a shape needs to refuse a key it already + * carries. + * + * A key that is no simple thing offers no such string. See the + * {@see ComplexFieldNode}. */ interface SimpleFieldNodeInterface { From 11e269442c83c73c001fbf2c640e200c3f51016f Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:33:00 +0300 Subject: [PATCH 057/119] Add template argument list count checks (+ phpstan types) --- .../src/Template/TemplateArgumentListNode.php | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/libs/types/src/Template/TemplateArgumentListNode.php b/libs/types/src/Template/TemplateArgumentListNode.php index 248757a..778896a 100644 --- a/libs/types/src/Template/TemplateArgumentListNode.php +++ b/libs/types/src/Template/TemplateArgumentListNode.php @@ -7,6 +7,41 @@ use TypeLang\Type\NodeList; /** - * @template-extends NodeList + * Template arguments a type is referenced with, in the order they are + * written in. + * + * Note that an empty template argument list CANNOT be created, and this is + * an undefined behavior. + * + * ``` + * Some\Any + * // ^^^ ^^^^^^ two arguments + * ``` + * + * @template-extends NodeList */ -final class TemplateArgumentListNode extends NodeList {} +final class TemplateArgumentListNode extends NodeList +{ + /** + * @param iterable $items + * @param int<0, max> $offset + */ + public function __construct(iterable $items, int $offset = 0) + { + parent::__construct($items, $offset); + + if (\count($this->items) < 1) { + throw new \InvalidArgumentException('Template arguments must have at least one item'); + } + } + + #[\Override] + public function offsetUnset(mixed $offset): void + { + parent::offsetUnset($offset); + + if (\count($this->items) < 1) { + throw new \UnderflowException('Template arguments must have at least one item'); + } + } +} From 6d50ea8df2c3960c74af6ad5e98faf535cae0bac Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 22:33:35 +0300 Subject: [PATCH 058/119] Add AST template argument comments --- .../src/Template/TemplateArgumentNode.php | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/libs/types/src/Template/TemplateArgumentNode.php b/libs/types/src/Template/TemplateArgumentNode.php index 4e06b30..18c7cb3 100644 --- a/libs/types/src/Template/TemplateArgumentNode.php +++ b/libs/types/src/Template/TemplateArgumentNode.php @@ -8,20 +8,37 @@ use TypeLang\Type\Node; use TypeLang\Type\TypeNode; +/** + * A single template argument, along with the hint it may carry. + * + * ``` + * Some\Any + * // ^^^ simple argument + * // ^^^^^^^^^ a hinted argument + * ``` + */ final class TemplateArgumentNode extends Node { - public ?Identifier $hint; - /** * @param int<0, max> $offset */ public function __construct( public TypeNode $value, - ?Identifier $hint = null, + /** + * Word standing in front of the argument, or {@see null} in case of it + * carries none. + * + * What the word means is left to whoever reads the tree. A `covariant` + * or an `out` is an ordinary identifier as far as the grammar goes. + * + * ``` + * Some\Any + * // ^^^^^^^^^ the hint + * ``` + */ + public ?Identifier $hint = null, int $offset = 0, ) { - $this->hint = $hint; - parent::__construct($offset); } } From b3c32ba7bd0fdeff8ab6faf79897ca6cad021cd2 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:02:05 +0300 Subject: [PATCH 059/119] Simplify type offset + this AST nodes comments --- libs/types/src/ThisNode.php | 7 ++++--- libs/types/src/TypeOffsetAccessNode.php | 8 +++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/libs/types/src/ThisNode.php b/libs/types/src/ThisNode.php index 00f0640..e185079 100644 --- a/libs/types/src/ThisNode.php +++ b/libs/types/src/ThisNode.php @@ -11,8 +11,9 @@ * callable(): $this * ``` * - * It is written the way a variable is, but it is a type: It names the object - * itself rather than a place a value is kept in. Any other variable is - * a {@see VariableNode} and no type at all. + * It is written the way a variable is, but it is a type. It names the object + * itself rather than a place a value is kept in. + * + * Any other variable is a {@see VariableNode} and no type at all. */ final class ThisNode extends TypeNode {} diff --git a/libs/types/src/TypeOffsetAccessNode.php b/libs/types/src/TypeOffsetAccessNode.php index 89f7bc0..fd27d79 100644 --- a/libs/types/src/TypeOffsetAccessNode.php +++ b/libs/types/src/TypeOffsetAccessNode.php @@ -26,11 +26,9 @@ final class TypeOffsetAccessNode extends WrappingTypeNode public function __construct( TypeNode $type, /** - * The offset itself, that is, whatever stands inside the brackets. - * - * Note: An `$access` name instead of `$offset` is used to avoid - * conflicts with the physical byte offset of the type in the - * source code. + * The property is named an `$access` rather than an `$offset` so that + * it is not taken for the place in the source text every node + * carries. */ public readonly TypeNode $access, int $offset = 0, From 871e69b864d18ac9e57ae507bb8f5e97ac5bcaf7 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:02:23 +0300 Subject: [PATCH 060/119] Remove useless comments from node list --- libs/types/src/NodeList.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/types/src/NodeList.php b/libs/types/src/NodeList.php index ccfb4cb..bf5e73d 100644 --- a/libs/types/src/NodeList.php +++ b/libs/types/src/NodeList.php @@ -20,8 +20,6 @@ abstract class NodeList extends Node implements \Countable { /** - * The nodes of a list, in the order they are written in. - * * @var (TNonEmpty is true ? non-empty-list : list) */ public array $items = []; From d2571f0e104592e90660e7ed74de32f69040006d Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:02:49 +0300 Subject: [PATCH 061/119] Fix typos in named type AST node comments --- libs/types/src/NamedTypeNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/types/src/NamedTypeNode.php b/libs/types/src/NamedTypeNode.php index ab4e25f..8a07015 100644 --- a/libs/types/src/NamedTypeNode.php +++ b/libs/types/src/NamedTypeNode.php @@ -36,7 +36,7 @@ public function __construct( * Template arguments of a type, or {@see null} in case of the name is * followed by none. * - * The list is never empty: An `Some<>` is no type. + * The list is never empty, since an `Some<>` is no type. * * ``` * Some // null From c839fd69930549795cbb458b99b0111225e4c4e0 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:03:55 +0300 Subject: [PATCH 062/119] Simplify Name::$parts AST node comment --- libs/types/src/Name.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index 5bbf11c..ba7af87 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -30,8 +30,7 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String public const IS_FULLY_QUALIFIED_DEFAULT_VALUE = false; /** - * The identifiers a name is written of, in order. Never empty: A name of - * no parts is no name. + * The identifiers a name is written of. * * @var non-empty-list */ From a743cc77aac9a95c78d4aeaf69eb2208965eedff Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:08:40 +0300 Subject: [PATCH 063/119] Improve MaskNode AST node comment --- libs/types/src/MaskNode.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/types/src/MaskNode.php b/libs/types/src/MaskNode.php index d58d816..e85a6a6 100644 --- a/libs/types/src/MaskNode.php +++ b/libs/types/src/MaskNode.php @@ -5,9 +5,10 @@ namespace TypeLang\Type; /** - * The name of a constant written in part: An ordered list of the literal - * segments it is made of, with a {@see WildcardNode} standing wherever the - * name is left unsaid. + * The name of a constant written in part. + * + * The segments the name is made of are kept in the order they are written + * in, with a {@see WildcardNode} standing wherever the name is left unsaid. * * Note that an empty mask node list CANNOT be created, and this is * an undefined behavior. @@ -19,7 +20,7 @@ * // the segments and the wildcards between them * * Some\Any::* - * // ^ a single wildcard and no segments at all + * // ^ a single wildcard * ``` * * @template-extends NodeList @@ -77,8 +78,6 @@ public function getSegments(): array } /** - * Gets the literal segments of a mask as strings. - * * @return list */ public function getSegmentsAsStrings(): array From cb690e33af5ab6403e9beda3535d6112e7a51d9c Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:35:14 +0300 Subject: [PATCH 064/119] Fix Identifier comment typo --- libs/types/src/Identifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/types/src/Identifier.php b/libs/types/src/Identifier.php index 838ce36..212095e 100644 --- a/libs/types/src/Identifier.php +++ b/libs/types/src/Identifier.php @@ -12,7 +12,7 @@ * ^^^^ ^^^ // two identifiers * * non-empty-string - * ^^^^^^^^^^^^^^^^ // a single identifier: A dash belongs to a word + * ^^^^^^^^^^^^^^^^ // a single identifier * ``` * * @phpstan-consistent-constructor From f675f8bed949dfb2339e5e339ddfbc1f13e7c828 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:35:47 +0300 Subject: [PATCH 065/119] Improve ConstMaskNode AST node --- libs/types/src/ConstMaskNode.php | 60 +++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/libs/types/src/ConstMaskNode.php b/libs/types/src/ConstMaskNode.php index 51da2d1..74a44fd 100644 --- a/libs/types/src/ConstMaskNode.php +++ b/libs/types/src/ConstMaskNode.php @@ -13,7 +13,7 @@ * ^^^^^^ the mask its name is written as * * *_SOME - * ^^^^^^ a mask alone: The name may be left unsaid from its very beginning + * ^^^^^^ a mask with no namespace at all * ``` */ final class ConstMaskNode extends TypeNode @@ -27,30 +27,48 @@ public function __construct( */ public MaskNode $mask, /** - * The namespace the constant belongs to, or {@see null} in case of it - * is written with none. Always a relative name: The leading separator - * a fully qualified reference begins in is the - * {@see $isFullyQualified} below. + * The namespace the constant belongs to, the leading separator of which + * says whether the reference is fully qualified. * - * ``` - * SOME_* // null - * Some\Any\SOME_* // "Some\Any" - * ``` + * A {@see bool} stands for that separator alone, in case of the + * constant is written with no namespace. */ - public ?Name $namespace = null, - /** - * Whether the reference is written with the leading separator that - * says it is to be read from the root namespace. - * - * ``` - * Some\SOME_* // false - * \Some\SOME_* // true - * \SOME_* // true, with no namespace at all - * ``` - */ - public bool $isFullyQualified = Name::IS_FULLY_QUALIFIED_DEFAULT_VALUE, + public Name|bool $namespaceOrFullyQualified = Name::IS_FULLY_QUALIFIED_DEFAULT_VALUE, int $offset = 0, ) { parent::__construct($offset); } + + /** + * Gets whether the reference is to be read from the root namespace. + */ + public function isFullyQualified(): bool + { + $context = $this->namespaceOrFullyQualified; + + if ($context instanceof Name) { + return $context->isFullyQualified; + } + + return $context; + } + + /** + * A helper method to set whether the reference is to be read from + * the root namespace. + */ + public function setFullyQualified(bool $isFullyQualified = true): void + { + $context = $this->namespaceOrFullyQualified; + + if ($context instanceof Name) { + $this->namespaceOrFullyQualified = $isFullyQualified + ? $context->toFullQualified() + : $context->toUnqualified(); + + return; + } + + $this->namespaceOrFullyQualified = $isFullyQualified; + } } From 9b92edd6c449db25766879af1fb155d294cfdbb2 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:36:16 +0300 Subject: [PATCH 066/119] Simplify variable node AST node comment --- libs/types/src/VariableNode.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libs/types/src/VariableNode.php b/libs/types/src/VariableNode.php index 11fb7e8..94ce8ed 100644 --- a/libs/types/src/VariableNode.php +++ b/libs/types/src/VariableNode.php @@ -14,10 +14,6 @@ * ($value is int ? string : bool) * //^^^^^ stands as the subject of a condition * ``` - * - * A variable is no type: It names a place a value is kept in, and the type - * of what is kept there is what a statement is about. The one variable that - * is a type of its own is the `$this`, and that one is a {@see ThisNode}. */ final class VariableNode extends Node { @@ -26,7 +22,7 @@ final class VariableNode extends Node */ public function __construct( /** - * The name of the variable, written without the `$` it begins with. + * The name of the variable, written without the leading `$`. */ public Identifier $name, int $offset = 0, From f1bf21984c6b114e6354355a3163655af0bbb3d7 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:36:47 +0300 Subject: [PATCH 067/119] Add template parameters to the callable type node --- libs/types/src/CallableTypeNode.php | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/libs/types/src/CallableTypeNode.php b/libs/types/src/CallableTypeNode.php index 753ff6e..ac31b2d 100644 --- a/libs/types/src/CallableTypeNode.php +++ b/libs/types/src/CallableTypeNode.php @@ -5,7 +5,19 @@ namespace TypeLang\Type; use TypeLang\Type\Callable\CallableParameterListNode; +use TypeLang\Type\Template\TemplateParameterListNode; +/** + * Something callable, written as a name with a parameter list behind it. + * + * ``` + * Closure(T, string ...$rest): T + * ^^^^^^^ the name + * ^^^^^^^^^^^ the template parameters + * ^^^^^^^^^^^^^^^^^^^^ the parameters + * ^ the return type + * ``` + */ final class CallableTypeNode extends TypeNode { /** @@ -13,8 +25,39 @@ final class CallableTypeNode extends TypeNode */ public function __construct( public Name $name, + /** + * Parameters a callable accepts. Unlike the ones below, the list is + * never a {@see null}: A callable is written with its parentheses, + * and an empty list is what stands inside an empty pair of them. + * + * ``` + * callable() // an empty list + * callable(int) // a list of 1 parameter + * ``` + */ public CallableParameterListNode $parameters = new CallableParameterListNode(), + /** + * A return type of callable, or {@see null} in case of it is left + * unsaid. + * + * ``` + * callable() // null + * callable(): int // a type + * ``` + */ public ?TypeNode $type = null, + /** + * Template parameters a callable declares, or {@see null} in case of + * it declares none. + * + * The list is never empty, since a `callable<>()` is no type. + * + * ``` + * callable(T): T // null + * callable(T): T // a list of 1 parameter + * ``` + */ + public ?TemplateParameterListNode $templates = null, int $offset = 0, ) { parent::__construct($offset); From 75fe01ac17525bb205906e319b709d4807eec7fa Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:37:01 +0300 Subject: [PATCH 068/119] Fix tests --- .../Callable/CallableParameterNodeTest.php | 7 +- libs/types/tests/ClassConstMaskNodeTest.php | 36 ++-- .../GreaterThanConditionNodeTest.php | 36 ---- .../GreaterThanOrEqualConditionNodeTest.php | 36 ---- .../Condition/LessThanConditionNodeTest.php | 36 ---- .../LessThanOrEqualConditionNodeTest.php | 36 ---- libs/types/tests/ConstMaskNodeTest.php | 90 +++++++++- libs/types/tests/Literal/LiteralNodeTest.php | 11 -- .../tests/Literal/VariableLiteralNodeTest.php | 53 ------ libs/types/tests/LogicalTypeNodeTest.php | 31 ---- libs/types/tests/MaskNodeTest.php | 95 ++++++++++ libs/types/tests/NameTest.php | 2 +- libs/types/tests/NamedTypeNodeTest.php | 5 +- libs/types/tests/NodeTest.php | 113 +++++------- .../tests/Shape/ClassConstFieldNodeTest.php | 84 --------- .../Shape/ClassConstMaskFieldNodeTest.php | 79 -------- .../tests/Shape/ConstMaskFieldNodeTest.php | 78 -------- .../tests/Shape/ExplicitFieldNodeTest.php | 170 +++++++++--------- .../tests/Shape/NumericFieldNodeTest.php | 78 -------- .../tests/Shape/StringNamedFieldNodeTest.php | 69 ------- .../TemplateArgumentsListNodeTest.php | 21 ++- libs/types/tests/VariableNodeTest.php | 67 +++++++ libs/types/tests/WildcardNodeTest.php | 37 ++++ 23 files changed, 452 insertions(+), 818 deletions(-) delete mode 100644 libs/types/tests/Condition/GreaterThanConditionNodeTest.php delete mode 100644 libs/types/tests/Condition/GreaterThanOrEqualConditionNodeTest.php delete mode 100644 libs/types/tests/Condition/LessThanConditionNodeTest.php delete mode 100644 libs/types/tests/Condition/LessThanOrEqualConditionNodeTest.php delete mode 100644 libs/types/tests/Literal/VariableLiteralNodeTest.php create mode 100644 libs/types/tests/MaskNodeTest.php delete mode 100644 libs/types/tests/Shape/ClassConstFieldNodeTest.php delete mode 100644 libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php delete mode 100644 libs/types/tests/Shape/ConstMaskFieldNodeTest.php delete mode 100644 libs/types/tests/Shape/NumericFieldNodeTest.php delete mode 100644 libs/types/tests/Shape/StringNamedFieldNodeTest.php create mode 100644 libs/types/tests/VariableNodeTest.php create mode 100644 libs/types/tests/WildcardNodeTest.php diff --git a/libs/types/tests/Callable/CallableParameterNodeTest.php b/libs/types/tests/Callable/CallableParameterNodeTest.php index 6ad79a0..7e94da0 100644 --- a/libs/types/tests/Callable/CallableParameterNodeTest.php +++ b/libs/types/tests/Callable/CallableParameterNodeTest.php @@ -6,7 +6,8 @@ use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\Callable\CallableParameterNode; -use TypeLang\Type\Literal\VariableLiteralNode; +use TypeLang\Type\Identifier; +use TypeLang\Type\VariableNode; use TypeLang\Type\Name; use TypeLang\Type\NamedTypeNode; use TypeLang\Type\Node; @@ -30,7 +31,7 @@ public function constructorWithTypeOnly(): void #[Test] public function constructorWithNameOnly(): void { - $name = new VariableLiteralNode('param'); + $name = new VariableNode(new Identifier('param')); $node = new CallableParameterNode(name: $name); self::assertNull($node->type); @@ -41,7 +42,7 @@ public function constructorWithNameOnly(): void public function constructorWithTypeAndName(): void { $type = new NamedTypeNode(Name::createFromString('int')); - $name = new VariableLiteralNode('count'); + $name = new VariableNode(new Identifier('count')); $node = new CallableParameterNode($type, $name); self::assertSame($type, $node->type); diff --git a/libs/types/tests/ClassConstMaskNodeTest.php b/libs/types/tests/ClassConstMaskNodeTest.php index 33268cc..bef275f 100644 --- a/libs/types/tests/ClassConstMaskNodeTest.php +++ b/libs/types/tests/ClassConstMaskNodeTest.php @@ -7,44 +7,42 @@ use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\ClassConstMaskNode; use TypeLang\Type\Identifier; +use TypeLang\Type\MaskNode; use TypeLang\Type\Name; +use TypeLang\Type\WildcardNode; final class ClassConstMaskNodeTest extends TestCase { #[Test] - public function constructorStoresClassWithConstant(): void + public function constructorStoresClassWithMask(): void { $class = Name::createFromString('MyClass'); - $const = new Identifier('STATUS_'); - $node = new ClassConstMaskNode($class, $const); + $mask = new MaskNode([new Identifier('STATUS_'), new WildcardNode()]); + $node = new ClassConstMaskNode($class, $mask); self::assertSame($class, $node->class); - self::assertSame($const, $node->constant); + self::assertSame($mask, $node->mask); } #[Test] - public function constantDefaultsToNull(): void + public function aMaskOfNothingButAWildcardCarriesNoSegments(): void { - $class = Name::createFromString('MyClass'); - $node = new ClassConstMaskNode($class); - - self::assertNull($node->constant); - } + $node = new ClassConstMaskNode( + Name::createFromString('MyEnum'), + new MaskNode([new WildcardNode()]), + ); - #[Test] - public function constructorAcceptsNullConstant(): void - { - $class = Name::createFromString('MyEnum'); - $node = new ClassConstMaskNode($class, null); - - self::assertSame($class, $node->class); - self::assertNull($node->constant); + self::assertSame([], $node->mask->getSegments()); + self::assertSame('*', $node->mask->toString()); } #[Test] public function defaultOffsetIsZero(): void { - $node = new ClassConstMaskNode(Name::createFromString('Foo')); + $node = new ClassConstMaskNode( + Name::createFromString('Foo'), + new MaskNode([new WildcardNode()]), + ); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Condition/GreaterThanConditionNodeTest.php b/libs/types/tests/Condition/GreaterThanConditionNodeTest.php deleted file mode 100644 index c3931fa..0000000 --- a/libs/types/tests/Condition/GreaterThanConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new GreaterThanConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Condition/GreaterThanOrEqualConditionNodeTest.php b/libs/types/tests/Condition/GreaterThanOrEqualConditionNodeTest.php deleted file mode 100644 index 619ba02..0000000 --- a/libs/types/tests/Condition/GreaterThanOrEqualConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new GreaterThanOrEqualConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Condition/LessThanConditionNodeTest.php b/libs/types/tests/Condition/LessThanConditionNodeTest.php deleted file mode 100644 index 81f23ca..0000000 --- a/libs/types/tests/Condition/LessThanConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new LessThanConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Condition/LessThanOrEqualConditionNodeTest.php b/libs/types/tests/Condition/LessThanOrEqualConditionNodeTest.php deleted file mode 100644 index 642eb36..0000000 --- a/libs/types/tests/Condition/LessThanOrEqualConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new LessThanOrEqualConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/ConstMaskNodeTest.php b/libs/types/tests/ConstMaskNodeTest.php index f64ae92..cdd48e8 100644 --- a/libs/types/tests/ConstMaskNodeTest.php +++ b/libs/types/tests/ConstMaskNodeTest.php @@ -6,24 +6,104 @@ use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\ConstMaskNode; +use TypeLang\Type\Identifier; +use TypeLang\Type\MaskNode; use TypeLang\Type\Name; +use TypeLang\Type\WildcardNode; final class ConstMaskNodeTest extends TestCase { #[Test] - public function constructorStoresName(): void + public function constructorStoresTheMask(): void { - $name = Name::createFromString('SOME_CONST'); - $node = new ConstMaskNode($name); + $mask = new MaskNode([new Identifier('SOME_CONST'), new WildcardNode()]); + $node = new ConstMaskNode($mask); - self::assertSame($name, $node->name); + self::assertSame($mask, $node->mask); + } + + #[Test] + public function namespaceIsNullByDefault(): void + { + $node = new ConstMaskNode(new MaskNode([new WildcardNode(), new Identifier('_SOME')])); + + self::assertNull($node->namespace); + self::assertFalse($node->isFullyQualified); + } + + #[Test] + public function constructorStoresTheNamespace(): void + { + $namespace = Name::createFromString('Some\Any'); + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + $namespace, + ); + + self::assertSame($namespace, $node->namespace); + } + + /** + * The leading separator belongs to the node, so a namespace written with + * one is kept as a relative name and lifted into the flag. + */ + #[Test] + public function aFullyQualifiedNamespaceFillsTheFlag(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + Name::createFromString('\Some\Any'), + ); + + self::assertTrue($node->isFullyQualified); + self::assertNotNull($node->namespace); + self::assertFalse($node->namespace->isFullyQualified); + self::assertSame('Some\Any', $node->namespace->toString()); + } + + #[Test] + public function aRelativeNamespaceLeavesTheFlagUnset(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + Name::createFromString('Some\Any'), + ); + + self::assertFalse($node->isFullyQualified); + } + + #[Test] + public function constructorStoresTheFullyQualifiedFlag(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + true, + ); + + self::assertTrue($node->isFullyQualified); } #[Test] public function defaultOffsetIsZero(): void { - $node = new ConstMaskNode(Name::createFromString('FOO')); + $node = new ConstMaskNode(new MaskNode([new Identifier('FOO'), new WildcardNode()])); self::assertSame(0, $node->offset); } + + #[Test] + public function aMaskIsWrittenTheWayItIsRead(): void + { + $node = new ConstMaskNode( + new MaskNode([ + new Identifier('SOME'), + new WildcardNode(), + new Identifier('ANY'), + new WildcardNode(), + ]), + ); + + self::assertSame('SOME*ANY*', $node->mask->toString()); + self::assertSame(['SOME', 'ANY'], $node->mask->getSegmentsAsStrings()); + } } diff --git a/libs/types/tests/Literal/LiteralNodeTest.php b/libs/types/tests/Literal/LiteralNodeTest.php index d7dbf11..350294f 100644 --- a/libs/types/tests/Literal/LiteralNodeTest.php +++ b/libs/types/tests/Literal/LiteralNodeTest.php @@ -13,7 +13,6 @@ use TypeLang\Type\Literal\LiteralNodeInterface; use TypeLang\Type\Literal\NullLiteralNode; use TypeLang\Type\Literal\StringLiteralNode; -use TypeLang\Type\Literal\VariableLiteralNode; use TypeLang\Type\Tests\TestCase; use TypeLang\Type\TypeNode; @@ -30,7 +29,6 @@ public static function provideLiterals(): iterable yield 'int' => [new IntLiteralNode(42, '42', '42'), 42, '42']; yield 'null' => [new NullLiteralNode(), null, 'null']; yield 'string' => [new StringLiteralNode('example', '"example"'), 'example', '"example"']; - yield 'variable' => [new VariableLiteralNode('example'), 'example', '$example']; } #[Test] @@ -113,15 +111,6 @@ public function floatLiteralDerivesRawFromValue(): void self::assertSame('0.5', $node->raw); } - #[Test] - public function variableLiteralValueDoesNotContainDollarSign(): void - { - $node = new VariableLiteralNode('example'); - - self::assertSame('example', $node->value); - self::assertSame('$example', $node->raw); - } - #[Test] public function literalValueAndRawArePartOfTheNodeState(): void { diff --git a/libs/types/tests/Literal/VariableLiteralNodeTest.php b/libs/types/tests/Literal/VariableLiteralNodeTest.php deleted file mode 100644 index 706ba27..0000000 --- a/libs/types/tests/Literal/VariableLiteralNodeTest.php +++ /dev/null @@ -1,53 +0,0 @@ -value); - } - - #[Test] - public function rawValueIsPrefixedByDollarSign(): void - { - $node = new VariableLiteralNode('foo'); - - self::assertSame('$foo', $node->raw); - } - - #[Test] - public function toStringReturnsRaw(): void - { - $node = new VariableLiteralNode('bar'); - - self::assertSame('$bar', (string) $node); - } - - #[Test] - public function singleCharacterVariableIsAllowed(): void - { - $node = new VariableLiteralNode('a'); - - self::assertSame('a', $node->value); - self::assertSame('$a', $node->raw); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new VariableLiteralNode('x'); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/LogicalTypeNodeTest.php b/libs/types/tests/LogicalTypeNodeTest.php index fee9751..9577ccf 100644 --- a/libs/types/tests/LogicalTypeNodeTest.php +++ b/libs/types/tests/LogicalTypeNodeTest.php @@ -244,37 +244,6 @@ public function serializationRoundtripPreservesStatementsAndOffset(string $class self::assertSame('B', $restored->statements[1]->name->toString()); } - #[Test] - #[DataProvider('provideLogicalTypes')] - public function serializePayloadContainsStatementsAndOffset(string $class): void - { - $node = new $class([$this->type('A'), $this->type('B')], 7); - - self::assertSame([$node->statements, 7], $node->__serialize()); - } - - #[Test] - #[DataProvider('provideLogicalTypes')] - public function unserializeThrowsWhenStatementsAreMissing(string $class): void - { - $node = new $class([$this->type('A'), $this->type('B')]); - - $this->expectException(\UnexpectedValueException::class); - - $node->__unserialize([]); - } - - #[Test] - #[DataProvider('provideLogicalTypes')] - public function unserializeDefaultsTheOffsetToZero(string $class): void - { - $node = new $class([$this->type('A'), $this->type('B')], 42); - - $node->__unserialize([$node->statements]); - - self::assertSame(0, $node->offset); - } - #[Test] #[DataProvider('provideLogicalTypes')] public function theOffsetIsPassedThroughTheConstructor(string $class): void diff --git a/libs/types/tests/MaskNodeTest.php b/libs/types/tests/MaskNodeTest.php new file mode 100644 index 0000000..4a2b22e --- /dev/null +++ b/libs/types/tests/MaskNodeTest.php @@ -0,0 +1,95 @@ + $items each of which is either a literal + * segment or the "*" of a wildcard + */ + private static function mask(array $items): MaskNode + { + $parts = []; + + foreach ($items as $item) { + $parts[] = $item === '*' ? new WildcardNode() : new Identifier($item); + } + + return new MaskNode($parts); + } + + /** + * @return iterable, non-empty-string, list}> + */ + public static function provideMasks(): iterable + { + yield 'a wildcard alone' => [['*'], '*', []]; + yield 'a trailing wildcard' => [['BAR_', '*'], 'BAR_*', ['BAR_']]; + yield 'a leading wildcard' => [['*', '_BAR'], '*_BAR', ['_BAR']]; + yield 'a wildcard in between' => [['A', '*', 'B'], 'A*B', ['A', 'B']]; + yield 'several wildcards' => [ + ['BAR', '*', 'BAZ', '*', 'SOME'], + 'BAR*BAZ*SOME', + ['BAR', 'BAZ', 'SOME'], + ]; + yield 'wildcards all around' => [['*', 'A', '*'], '*A*', ['A']]; + } + + /** + * @param list $items + * @param non-empty-string $expected + * @param list $segments + */ + #[Test] + #[DataProvider('provideMasks')] + public function aMaskIsWrittenTheWayItIsRead(array $items, string $expected, array $segments): void + { + self::assertSame($expected, self::mask($items)->toString()); + self::assertSame($expected, (string) self::mask($items)); + } + + /** + * @param list $items + * @param non-empty-string $expected + * @param list $segments + */ + #[Test] + #[DataProvider('provideMasks')] + public function segmentsAreTheLiteralPartsAlone(array $items, string $expected, array $segments): void + { + $mask = self::mask($items); + + self::assertSame($segments, $mask->getSegmentsAsStrings()); + + $identifiers = []; + + foreach ($mask->getSegments() as $identifier) { + $identifiers[] = $identifier->value; + } + + self::assertSame($segments, $identifiers); + } + + #[Test] + public function everyPartOfAMaskIsKeptInTheOrderItIsWrittenIn(): void + { + $mask = self::mask(['A', '*', 'B']); + + self::assertCount(3, $mask); + self::assertInstanceOf(Identifier::class, $mask->items[0]); + self::assertInstanceOf(WildcardNode::class, $mask->items[1]); + self::assertInstanceOf(Identifier::class, $mask->items[2]); + } +} diff --git a/libs/types/tests/NameTest.php b/libs/types/tests/NameTest.php index 216ac4d..cfa5659 100644 --- a/libs/types/tests/NameTest.php +++ b/libs/types/tests/NameTest.php @@ -692,7 +692,7 @@ private static function payload(mixed ...$data): string #[Test] public function constructorThrowsOnNonIdentifierParts(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(\TypeError::class); /** @phpstan-ignore-next-line */ new Name(['Foo']); diff --git a/libs/types/tests/NamedTypeNodeTest.php b/libs/types/tests/NamedTypeNodeTest.php index 30931a0..c3cc454 100644 --- a/libs/types/tests/NamedTypeNodeTest.php +++ b/libs/types/tests/NamedTypeNodeTest.php @@ -9,6 +9,7 @@ use TypeLang\Type\NamedTypeNode; use TypeLang\Type\Shape\FieldsListNode; use TypeLang\Type\Template\TemplateArgumentListNode; +use TypeLang\Type\Template\TemplateArgumentNode; final class NamedTypeNodeTest extends TestCase { @@ -40,7 +41,9 @@ public function fieldsDefaultToNull(): void #[Test] public function constructorStoresTemplateArguments(): void { - $args = new TemplateArgumentListNode(); + $args = new TemplateArgumentListNode([ + new TemplateArgumentNode(new NamedTypeNode(Name::createFromString('int'))), + ]); $node = new NamedTypeNode(Name::createFromString('array'), $args); self::assertSame($args, $node->arguments); diff --git a/libs/types/tests/NodeTest.php b/libs/types/tests/NodeTest.php index 6ed8a5d..362b9ae 100644 --- a/libs/types/tests/NodeTest.php +++ b/libs/types/tests/NodeTest.php @@ -12,10 +12,6 @@ use TypeLang\Type\ClassConstMaskNode; use TypeLang\Type\ClassConstNode; use TypeLang\Type\Condition\EqualConditionNode; -use TypeLang\Type\Condition\GreaterThanConditionNode; -use TypeLang\Type\Condition\GreaterThanOrEqualConditionNode; -use TypeLang\Type\Condition\LessThanConditionNode; -use TypeLang\Type\Condition\LessThanOrEqualConditionNode; use TypeLang\Type\Condition\NotEqualConditionNode; use TypeLang\Type\ConstMaskNode; use TypeLang\Type\Identifier; @@ -25,27 +21,30 @@ use TypeLang\Type\Literal\IntLiteralNode; use TypeLang\Type\Literal\NullLiteralNode; use TypeLang\Type\Literal\StringLiteralNode; -use TypeLang\Type\Literal\VariableLiteralNode; +use TypeLang\Type\ThisNode; +use TypeLang\Type\VariableNode; +use TypeLang\Type\MaskNode; use TypeLang\Type\Name; use TypeLang\Type\NamedTypeNode; use TypeLang\Type\Node; use TypeLang\Type\NodeInterface; use TypeLang\Type\NullableTypeNode; -use TypeLang\Type\Shape\ClassConstFieldNode; -use TypeLang\Type\Shape\ClassConstMaskFieldNode; -use TypeLang\Type\Shape\ConstMaskFieldNode; +use TypeLang\Type\Shape\ComplexFieldNode; use TypeLang\Type\Shape\FieldsListNode; use TypeLang\Type\Shape\ImplicitFieldNode; use TypeLang\Type\Shape\NamedFieldNode; -use TypeLang\Type\Shape\NumericFieldNode; -use TypeLang\Type\Shape\StringNamedFieldNode; +use TypeLang\Type\Shape\ScalarFieldNode; use TypeLang\Type\Template\TemplateArgumentListNode; use TypeLang\Type\Template\TemplateArgumentNode; +use TypeLang\Type\Template\TemplateBoundEdgeNode; +use TypeLang\Type\Template\TemplateParameterListNode; +use TypeLang\Type\Template\TemplateParameterNode; use TypeLang\Type\TernaryExpressionNode; use TypeLang\Type\TypeNode; use TypeLang\Type\TypeOffsetAccessNode; use TypeLang\Type\TypesListNode; use TypeLang\Type\UnionTypeNode; +use TypeLang\Type\WildcardNode; final class NodeTest extends TestCase { @@ -67,9 +66,10 @@ public static function provideNodes(): iterable yield ClassConstNode::class => [ new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), ]; - yield ConstMaskNode::class => [new ConstMaskNode(Name::createFromString('CONST'))]; + yield ConstMaskNode::class => [new ConstMaskNode(new MaskNode([new Identifier('CONST'), new WildcardNode()]))]; yield Identifier::class => [new Identifier('Example')]; yield IntersectionTypeNode::class => [new IntersectionTypeNode([self::type('A'), self::type('B')])]; + yield MaskNode::class => [new MaskNode([new WildcardNode()])]; yield Name::class => [Name::createFromString('Example')]; yield NamedTypeNode::class => [self::type()]; yield NullableTypeNode::class => [new NullableTypeNode(self::type())]; @@ -83,19 +83,12 @@ public static function provideNodes(): iterable yield TypeOffsetAccessNode::class => [new TypeOffsetAccessNode(self::type(), self::type('Offset'))]; yield TypesListNode::class => [new TypesListNode(self::type())]; yield UnionTypeNode::class => [new UnionTypeNode([self::type('A'), self::type('B')])]; + yield WildcardNode::class => [new WildcardNode()]; yield CallableParameterListNode::class => [new CallableParameterListNode()]; yield CallableParameterNode::class => [new CallableParameterNode(self::type())]; yield EqualConditionNode::class => [new EqualConditionNode(self::type('A'), self::type('B'))]; - yield GreaterThanConditionNode::class => [new GreaterThanConditionNode(self::type('A'), self::type('B'))]; - yield GreaterThanOrEqualConditionNode::class => [ - new GreaterThanOrEqualConditionNode(self::type('A'), self::type('B')), - ]; - yield LessThanConditionNode::class => [new LessThanConditionNode(self::type('A'), self::type('B'))]; - yield LessThanOrEqualConditionNode::class => [ - new LessThanOrEqualConditionNode(self::type('A'), self::type('B')), - ]; yield NotEqualConditionNode::class => [new NotEqualConditionNode(self::type('A'), self::type('B'))]; yield BoolLiteralNode::class => [new BoolLiteralNode(true)]; @@ -103,30 +96,27 @@ public static function provideNodes(): iterable yield IntLiteralNode::class => [new IntLiteralNode(42)]; yield NullLiteralNode::class => [new NullLiteralNode()]; yield StringLiteralNode::class => [new StringLiteralNode('example')]; - yield VariableLiteralNode::class => [new VariableLiteralNode('example')]; + yield ThisNode::class => [new ThisNode()]; + yield VariableNode::class => [new VariableNode(new Identifier('example'))]; - yield ClassConstFieldNode::class => [ - new ClassConstFieldNode( + yield ComplexFieldNode::class => [ + new ComplexFieldNode( new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), self::type(), ), ]; - yield ClassConstMaskFieldNode::class => [ - new ClassConstMaskFieldNode(new ClassConstMaskNode(Name::createFromString('Example')), self::type()), - ]; - yield ConstMaskFieldNode::class => [ - new ConstMaskFieldNode(new ConstMaskNode(Name::createFromString('CONST')), self::type()), - ]; yield FieldsListNode::class => [new FieldsListNode()]; yield ImplicitFieldNode::class => [new ImplicitFieldNode(self::type())]; yield NamedFieldNode::class => [new NamedFieldNode(new Identifier('key'), self::type())]; - yield NumericFieldNode::class => [new NumericFieldNode(new IntLiteralNode(0), self::type())]; - yield StringNamedFieldNode::class => [ - new StringNamedFieldNode(new StringLiteralNode('key'), self::type()), - ]; + yield ScalarFieldNode::class => [new ScalarFieldNode(new IntLiteralNode(0), self::type())]; - yield TemplateArgumentListNode::class => [new TemplateArgumentListNode()]; + yield TemplateArgumentListNode::class => [ + new TemplateArgumentListNode([new TemplateArgumentNode(self::type())]), + ]; yield TemplateArgumentNode::class => [new TemplateArgumentNode(self::type())]; + yield TemplateBoundEdgeNode::class => [new TemplateBoundEdgeNode(new Identifier('of'), self::type())]; + yield TemplateParameterListNode::class => [new TemplateParameterListNode()]; + yield TemplateParameterNode::class => [new TemplateParameterNode(new Identifier('T'))]; } #[Test] @@ -271,8 +261,11 @@ public static function provideNodesWithOffset(): iterable yield ClassConstNode::class => [ new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST'), 42), ]; - yield ConstMaskNode::class => [new ConstMaskNode(Name::createFromString('CONST'), 42)]; + yield ConstMaskNode::class => [ + new ConstMaskNode(new MaskNode([new Identifier('CONST'), new WildcardNode()]), offset: 42), + ]; yield Identifier::class => [new Identifier('Example', 42)]; + yield MaskNode::class => [new MaskNode([new WildcardNode()], 42)]; yield Name::class => [Name::createFromString('Example', 42)]; yield NamedTypeNode::class => [new NamedTypeNode(Name::createFromString('Example'), offset: 42)]; yield NullableTypeNode::class => [new NullableTypeNode(self::type(), 42)]; @@ -288,23 +281,12 @@ public static function provideNodesWithOffset(): iterable new TypeOffsetAccessNode(self::type(), self::type('Offset'), 42), ]; yield TypesListNode::class => [new TypesListNode(self::type(), 42)]; + yield WildcardNode::class => [new WildcardNode(42)]; yield CallableParameterListNode::class => [new CallableParameterListNode([], 42)]; yield CallableParameterNode::class => [new CallableParameterNode(self::type(), offset: 42)]; yield EqualConditionNode::class => [new EqualConditionNode(self::type('A'), self::type('B'), 42)]; - yield GreaterThanConditionNode::class => [ - new GreaterThanConditionNode(self::type('A'), self::type('B'), 42), - ]; - yield GreaterThanOrEqualConditionNode::class => [ - new GreaterThanOrEqualConditionNode(self::type('A'), self::type('B'), 42), - ]; - yield LessThanConditionNode::class => [ - new LessThanConditionNode(self::type('A'), self::type('B'), 42), - ]; - yield LessThanOrEqualConditionNode::class => [ - new LessThanOrEqualConditionNode(self::type('A'), self::type('B'), 42), - ]; yield NotEqualConditionNode::class => [ new NotEqualConditionNode(self::type('A'), self::type('B'), 42), ]; @@ -314,43 +296,36 @@ public static function provideNodesWithOffset(): iterable yield IntLiteralNode::class => [new IntLiteralNode(42, offset: 42)]; yield NullLiteralNode::class => [new NullLiteralNode(offset: 42)]; yield StringLiteralNode::class => [new StringLiteralNode('example', offset: 42)]; - yield VariableLiteralNode::class => [new VariableLiteralNode('example', 42)]; + yield ThisNode::class => [new ThisNode(42)]; + yield VariableNode::class => [new VariableNode(new Identifier('example'), 42)]; - yield ClassConstFieldNode::class => [ - new ClassConstFieldNode( + yield ComplexFieldNode::class => [ + new ComplexFieldNode( new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), self::type(), offset: 42, ), ]; - yield ClassConstMaskFieldNode::class => [ - new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('Example')), - self::type(), - offset: 42, - ), - ]; - yield ConstMaskFieldNode::class => [ - new ConstMaskFieldNode( - new ConstMaskNode(Name::createFromString('CONST')), - self::type(), - offset: 42, - ), - ]; yield FieldsListNode::class => [new FieldsListNode(offset: 42)]; yield ImplicitFieldNode::class => [new ImplicitFieldNode(self::type(), offset: 42)]; yield NamedFieldNode::class => [ new NamedFieldNode(new Identifier('key'), self::type(), offset: 42), ]; - yield NumericFieldNode::class => [ - new NumericFieldNode(new IntLiteralNode(0), self::type(), offset: 42), - ]; - yield StringNamedFieldNode::class => [ - new StringNamedFieldNode(new StringLiteralNode('key'), self::type(), offset: 42), + yield ScalarFieldNode::class => [ + new ScalarFieldNode(new IntLiteralNode(0), self::type(), offset: 42), ]; - yield TemplateArgumentListNode::class => [new TemplateArgumentListNode([], 42)]; + yield TemplateArgumentListNode::class => [ + new TemplateArgumentListNode([new TemplateArgumentNode(self::type())], 42), + ]; yield TemplateArgumentNode::class => [new TemplateArgumentNode(self::type(), offset: 42)]; + yield TemplateBoundEdgeNode::class => [ + new TemplateBoundEdgeNode(new Identifier('of'), self::type(), 42), + ]; + yield TemplateParameterListNode::class => [new TemplateParameterListNode([], 42)]; + yield TemplateParameterNode::class => [ + new TemplateParameterNode(new Identifier('T'), offset: 42), + ]; yield UnionTypeNode::class => [new UnionTypeNode([self::type('A'), self::type('B')], 42)]; yield IntersectionTypeNode::class => [ diff --git a/libs/types/tests/Shape/ClassConstFieldNodeTest.php b/libs/types/tests/Shape/ClassConstFieldNodeTest.php deleted file mode 100644 index 9630959..0000000 --- a/libs/types/tests/Shape/ClassConstFieldNodeTest.php +++ /dev/null @@ -1,84 +0,0 @@ -makeKey('MyClass', 'STATUS'); - $type = new NamedTypeNode(Name::createFromString('int')); - $node = new ClassConstFieldNode($key, $type); - - self::assertSame($key, $node->key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexFormatsAsClassDoubleColonConst(): void - { - $key = $this->makeKey('MyEnum', 'ACTIVE'); - $node = new ClassConstFieldNode($key, new NamedTypeNode(Name::createFromString('string'))); - - self::assertSame('MyEnum::ACTIVE', $node->getIndex()); - } - - #[Test] - public function indexWithQualifiedClassName(): void - { - $key = $this->makeKey('Vendor\Package\Status', 'OK'); - $node = new ClassConstFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('Vendor\Package\Status::OK', $node->getIndex()); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new ClassConstFieldNode( - $this->makeKey('Foo', 'BAR'), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new ClassConstFieldNode( - $this->makeKey('Foo', 'BAR'), - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new ClassConstFieldNode( - $this->makeKey('A', 'B'), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php b/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php deleted file mode 100644 index 9aa194b..0000000 --- a/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php +++ /dev/null @@ -1,79 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexWithConstantSuffix(): void - { - $key = new ClassConstMaskNode(Name::createFromString('MyEnum'), new Identifier('STATUS')); - $node = new ClassConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('MyEnum::STATUS*', $node->getIndex()); - } - - #[Test] - public function indexWithNullConstant(): void - { - $key = new ClassConstMaskNode(Name::createFromString('MyEnum')); - $node = new ClassConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('MyEnum::*', $node->getIndex()); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('Foo')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('Foo')), - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('A')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/ConstMaskFieldNodeTest.php b/libs/types/tests/Shape/ConstMaskFieldNodeTest.php deleted file mode 100644 index 2df1c38..0000000 --- a/libs/types/tests/Shape/ConstMaskFieldNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexReturnsConstMaskNodeAsString(): void - { - $key = new ConstMaskNode(Name::createFromString('MY_CONST')); - $node = new ConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('MY_CONST*', $node->getIndex()); - } - - #[Test] - public function indexWithQualifiedName(): void - { - $key = new ConstMaskNode(Name::createFromString('Vendor\Pkg\STATUS')); - $node = new ConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('Vendor\Pkg\STATUS*', $node->getIndex()); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new ConstMaskFieldNode( - new ConstMaskNode(Name::createFromString('FOO')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new ConstMaskFieldNode( - new ConstMaskNode(Name::createFromString('FOO')), - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new ConstMaskFieldNode( - new ConstMaskNode(Name::createFromString('X')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/ExplicitFieldNodeTest.php b/libs/types/tests/Shape/ExplicitFieldNodeTest.php index 140bd4d..6d272ab 100644 --- a/libs/types/tests/Shape/ExplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ExplicitFieldNodeTest.php @@ -12,19 +12,19 @@ use TypeLang\Type\Identifier; use TypeLang\Type\Literal\IntLiteralNode; use TypeLang\Type\Literal\StringLiteralNode; +use TypeLang\Type\MaskNode; use TypeLang\Type\Name; use TypeLang\Type\NamedTypeNode; -use TypeLang\Type\Shape\ClassConstFieldNode; -use TypeLang\Type\Shape\ClassConstMaskFieldNode; -use TypeLang\Type\Shape\ConstMaskFieldNode; +use TypeLang\Type\Shape\ComplexFieldNode; use TypeLang\Type\Shape\ExplicitFieldNode; use TypeLang\Type\Shape\FieldNode; use TypeLang\Type\Shape\ImplicitFieldNode; use TypeLang\Type\Shape\NamedFieldNode; -use TypeLang\Type\Shape\NumericFieldNode; -use TypeLang\Type\Shape\StringNamedFieldNode; +use TypeLang\Type\Shape\ScalarFieldNode; +use TypeLang\Type\Shape\SimpleFieldNodeInterface; use TypeLang\Type\Tests\TestCase; use TypeLang\Type\TypeNode; +use TypeLang\Type\WildcardNode; final class ExplicitFieldNodeTest extends TestCase { @@ -34,52 +34,113 @@ private static function type(string $name = 'Example'): NamedTypeNode } /** - * @return iterable + * @return iterable */ public static function provideExplicitFields(): iterable + { + yield 'named' => [new NamedFieldNode(new Identifier('key'), self::type())]; + yield 'scalar' => [new ScalarFieldNode(new IntLiteralNode(42), self::type())]; + yield 'complex' => [ + new ComplexFieldNode( + new ClassConstNode(Name::createFromString('Vendor\Status'), new Identifier('OK')), + self::type(), + ), + ]; + } + + /** + * Only a key that comes down to a string of its own carries an index. + * + * @return iterable + */ + public static function provideIndexedFields(): iterable { yield 'named' => [ new NamedFieldNode(new Identifier('key'), self::type()), 'key', ]; - yield 'string named' => [ - new StringNamedFieldNode(new StringLiteralNode('some key'), self::type()), + yield 'named by a keyword' => [ + new NamedFieldNode(new Identifier('true'), self::type()), + 'true', + ]; + + yield 'string' => [ + new ScalarFieldNode(new StringLiteralNode('some key'), self::type()), 'some key', ]; - yield 'numeric' => [ - new NumericFieldNode(new IntLiteralNode(42), self::type()), + yield 'string carrying a sequence' => [ + new ScalarFieldNode(new StringLiteralNode("a\nb"), self::type()), + "a\nb", + ]; + + yield 'number' => [ + new ScalarFieldNode(new IntLiteralNode(42), self::type()), '42', ]; + yield 'zero' => [ + new ScalarFieldNode(new IntLiteralNode(0), self::type()), + '0', + ]; + + yield 'negative number' => [ + new ScalarFieldNode(new IntLiteralNode(-1), self::type()), + '-1', + ]; + } + + + #[Test] + #[DataProvider('provideIndexedFields')] + public function indexMethodReturnsTheKeyAsAString(SimpleFieldNodeInterface $field, string $index): void + { + self::assertSame($index, $field->getIndex()); + } + + /** + * A key that has to be read to be understood offers no index: Whether two + * of them name the same constant is not written down. + * + * @return iterable + */ + public static function provideComplexFields(): iterable + { yield 'class const' => [ - new ClassConstFieldNode( + new ComplexFieldNode( new ClassConstNode(Name::createFromString('Vendor\Status'), new Identifier('OK')), self::type(), ), - 'Vendor\Status::OK', ]; yield 'class const mask' => [ - new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('Vendor\Status'), new Identifier('IS_')), + new ComplexFieldNode( + new ClassConstMaskNode( + Name::createFromString('Vendor\Status'), + new MaskNode([new Identifier('IS_'), new WildcardNode()]), + ), self::type(), ), - 'Vendor\Status::IS_*', ]; yield 'const mask' => [ - new ConstMaskFieldNode(new ConstMaskNode(Name::createFromString('Vendor\STATUS_')), self::type()), - 'Vendor\STATUS_*', + new ComplexFieldNode( + new ConstMaskNode( + new MaskNode([new Identifier('STATUS_'), new WildcardNode()]), + Name::createFromString('Vendor'), + ), + self::type(), + ), ]; } #[Test] - #[DataProvider('provideExplicitFields')] - public function indexMethodReturnsPrettyPrintedKey(ExplicitFieldNode $field, string $index): void + #[DataProvider('provideComplexFields')] + public function complexFieldCarriesNoIndex(ComplexFieldNode $field): void { - self::assertSame($index, $field->getIndex()); + self::assertNotInstanceOf(SimpleFieldNodeInterface::class, $field); + self::assertInstanceOf(TypeNode::class, $field->key); } #[Test] @@ -92,21 +153,18 @@ public function indexIsRecalculatedAfterKeyMutation(): void $field->key = new Identifier('other'); self::assertSame('other', $field->getIndex(), 'The index must be derived from the current key'); - self::assertSame('other', $field->getIndex()); } #[Test] #[DataProvider('provideExplicitFields')] - public function explicitFieldIsRequiredByDefault(ExplicitFieldNode $field, string $index): void + public function explicitFieldIsRequiredByDefault(ExplicitFieldNode $field): void { self::assertFalse($field->isOptional); } #[Test] #[DataProvider('provideExplicitFields')] - #[Test] - #[DataProvider('provideExplicitFields')] - public function explicitFieldIsAFieldNode(ExplicitFieldNode $field, string $index): void + public function explicitFieldIsAFieldNode(ExplicitFieldNode $field): void { self::assertInstanceOf(FieldNode::class, $field); self::assertInstanceOf(TypeNode::class, $field->type); @@ -120,7 +178,6 @@ public function optionalFieldStoresTheFlag(): void self::assertTrue($field->isOptional); } - #[Test] #[Test] public function implicitFieldHasNoKey(): void { @@ -130,41 +187,6 @@ public function implicitFieldHasNoKey(): void self::assertNotInstanceOf(ExplicitFieldNode::class, $field); } - #[Test] - public function classConstMaskFieldWithoutConstantIsStringified(): void - { - $field = new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('Vendor\Status')), - self::type(), - ); - - self::assertSame('Vendor\Status::*', $field->getIndex()); - } - - #[Test] - public function numericFieldIndexOfZero(): void - { - $field = new NumericFieldNode(new IntLiteralNode(0), self::type()); - - self::assertSame('0', $field->getIndex()); - } - - #[Test] - public function numericFieldIndexOfNegativeKey(): void - { - $field = new NumericFieldNode(new IntLiteralNode(-1), self::type()); - - self::assertSame('-1', $field->getIndex()); - } - - #[Test] - public function stringNamedFieldIndexIsTheDecodedValue(): void - { - $field = new StringNamedFieldNode(new StringLiteralNode("a\nb"), self::type()); - - self::assertSame("a\nb", $field->getIndex()); - } - #[Test] public function fieldTypeIsMutable(): void { @@ -173,30 +195,4 @@ public function fieldTypeIsMutable(): void self::assertSame('B', $field->type->name->toString()); } - - #[Test] - #[DataProvider('provideExplicitFields')] - public function isReturnsTrueForOwnClass(ExplicitFieldNode $field, string $index): void - { - self::assertTrue($field->is($field::class)); - self::assertTrue($field->is(ExplicitFieldNode::class)); - self::assertTrue($field->is(FieldNode::class)); - } - - #[Test] - #[DataProvider('provideExplicitFields')] - public function isReturnsFalseForAnotherClass(ExplicitFieldNode $field, string $index): void - { - self::assertFalse($field->is(ImplicitFieldNode::class)); - } - - #[Test] - public function implicitFieldIsNotAnExplicitOne(): void - { - $field = new ImplicitFieldNode(self::type()); - - self::assertTrue($field->is(ImplicitFieldNode::class)); - self::assertTrue($field->is(FieldNode::class)); - self::assertFalse($field->is(ExplicitFieldNode::class)); - } } diff --git a/libs/types/tests/Shape/NumericFieldNodeTest.php b/libs/types/tests/Shape/NumericFieldNodeTest.php deleted file mode 100644 index f7a5d46..0000000 --- a/libs/types/tests/Shape/NumericFieldNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexReturnsStringifiedKeyValue(): void - { - $key = new IntLiteralNode(42); - $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('42', $node->getIndex()); - } - - #[Test] - public function indexForZeroKey(): void - { - $key = new IntLiteralNode(0); - $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('0', $node->getIndex()); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new NumericFieldNode( - new IntLiteralNode(1), - new NamedTypeNode(Name::createFromString('string')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new NumericFieldNode( - new IntLiteralNode(1), - new NamedTypeNode(Name::createFromString('string')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new NumericFieldNode( - new IntLiteralNode(0), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/StringNamedFieldNodeTest.php b/libs/types/tests/Shape/StringNamedFieldNodeTest.php deleted file mode 100644 index 3aa01ce..0000000 --- a/libs/types/tests/Shape/StringNamedFieldNodeTest.php +++ /dev/null @@ -1,69 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexReturnsKeyValue(): void - { - $key = new StringLiteralNode('field-name'); - $node = new StringNamedFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('field-name', $node->getIndex()); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new StringNamedFieldNode( - new StringLiteralNode('key'), - new NamedTypeNode(Name::createFromString('string')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new StringNamedFieldNode( - new StringLiteralNode('key'), - new NamedTypeNode(Name::createFromString('string')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new StringNamedFieldNode( - new StringLiteralNode('k'), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Template/TemplateArgumentsListNodeTest.php b/libs/types/tests/Template/TemplateArgumentsListNodeTest.php index ec230f4..5139b19 100644 --- a/libs/types/tests/Template/TemplateArgumentsListNodeTest.php +++ b/libs/types/tests/Template/TemplateArgumentsListNodeTest.php @@ -19,13 +19,22 @@ private function makeArg(string $type): TemplateArgumentNode } #[Test] - public function emptyListByDefault(): void + public function emptyListIsNotAllowed(): void { - $list = new TemplateArgumentListNode(); + $this->expectException(\InvalidArgumentException::class); - self::assertCount(0, $list); - self::assertNull($list->first()); - self::assertNull($list->last()); + new TemplateArgumentListNode([]); + } + + #[Test] + public function singleArgumentIsBothTheFirstAndTheLast(): void + { + $argument = $this->makeArg('string'); + $list = new TemplateArgumentListNode([$argument]); + + self::assertCount(1, $list); + self::assertSame($argument, $list->first()); + self::assertSame($argument, $list->last()); } #[Test] @@ -103,7 +112,7 @@ public function iteratorYieldsArguments(): void #[Test] public function defaultOffsetIsZero(): void { - $list = new TemplateArgumentListNode(); + $list = new TemplateArgumentListNode([$this->makeArg('int')]); self::assertSame(0, $list->offset); } diff --git a/libs/types/tests/VariableNodeTest.php b/libs/types/tests/VariableNodeTest.php new file mode 100644 index 0000000..9940fc8 --- /dev/null +++ b/libs/types/tests/VariableNodeTest.php @@ -0,0 +1,67 @@ +name->toString()); + } + + #[Test] + public function singleCharacterNameIsAllowed(): void + { + $node = new VariableNode(new Identifier('a')); + + self::assertSame('a', $node->name->toString()); + } + + #[Test] + public function defaultOffsetIsZero(): void + { + $node = new VariableNode(new Identifier('x')); + + self::assertSame(0, $node->offset); + } + + /** + * A variable names a place a value is kept in, so it is no type of + * its own. + */ + #[Test] + public function variableIsNotAType(): void + { + $node = new VariableNode(new Identifier('foo')); + + self::assertInstanceOf(Node::class, $node); + self::assertNotInstanceOf(TypeNode::class, $node); + } + + /** + * The one variable that is a type of its own is the `$this`. + */ + #[Test] + public function thisIsAType(): void + { + self::assertInstanceOf(TypeNode::class, new ThisNode()); + } + + #[Test] + public function thisDefaultOffsetIsZero(): void + { + self::assertSame(0, (new ThisNode())->offset); + } +} diff --git a/libs/types/tests/WildcardNodeTest.php b/libs/types/tests/WildcardNodeTest.php new file mode 100644 index 0000000..d1b60a7 --- /dev/null +++ b/libs/types/tests/WildcardNodeTest.php @@ -0,0 +1,37 @@ +toString()); + self::assertSame('*', (string) $node); + self::assertSame('*', WildcardNode::CHAR); + } + + #[Test] + public function defaultOffsetIsZero(): void + { + self::assertSame(0, (new WildcardNode())->offset); + } +} From 1ca8d0dd87e1ad75db0a05a3206030e0925d0b85 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:37:30 +0300 Subject: [PATCH 069/119] Actualize printer using the new AST structures --- libs/printer/src/NativeTypePrinter.php | 16 ++- libs/printer/src/PrettyTypePrinter.php | 180 ++++++++++++++++++------- 2 files changed, 144 insertions(+), 52 deletions(-) diff --git a/libs/printer/src/NativeTypePrinter.php b/libs/printer/src/NativeTypePrinter.php index 724a641..72e2534 100644 --- a/libs/printer/src/NativeTypePrinter.php +++ b/libs/printer/src/NativeTypePrinter.php @@ -15,12 +15,13 @@ use TypeLang\Type\Literal\LiteralNode; use TypeLang\Type\Literal\NullLiteralNode; use TypeLang\Type\Literal\StringLiteralNode; -use TypeLang\Type\Literal\VariableLiteralNode; use TypeLang\Type\NamedTypeNode; use TypeLang\Type\TernaryExpressionNode; +use TypeLang\Type\ThisNode; use TypeLang\Type\TypeOffsetAccessNode; use TypeLang\Type\TypesListNode; use TypeLang\Type\UnionTypeNode; +use TypeLang\Type\WildcardNode; class NativeTypePrinter extends PrettyTypePrinter { @@ -121,6 +122,18 @@ protected function printConstMaskNode(ConstMaskNode $node): string return 'mixed'; } + #[\Override] + protected function printWildcardNode(WildcardNode $node): string + { + return 'mixed'; + } + + #[\Override] + protected function printThisNode(ThisNode $node): string + { + return 'self'; + } + #[\Override] protected function printClassConstNode(ClassConstNode $node): string { @@ -222,7 +235,6 @@ protected function printLiteralNode(LiteralNode $node): string $node instanceof IntLiteralNode => 'int', $node instanceof NullLiteralNode => 'null', $node instanceof StringLiteralNode => 'string', - $node instanceof VariableLiteralNode => $node->value === 'this' ? 'self' : 'mixed', default => \get_debug_type($node->value), }; } diff --git a/libs/printer/src/PrettyTypePrinter.php b/libs/printer/src/PrettyTypePrinter.php index 1f5288c..caf9322 100644 --- a/libs/printer/src/PrettyTypePrinter.php +++ b/libs/printer/src/PrettyTypePrinter.php @@ -11,10 +11,6 @@ use TypeLang\Type\ClassConstNode; use TypeLang\Type\Condition\Condition; use TypeLang\Type\Condition\EqualConditionNode; -use TypeLang\Type\Condition\GreaterThanConditionNode; -use TypeLang\Type\Condition\GreaterThanOrEqualConditionNode; -use TypeLang\Type\Condition\LessThanConditionNode; -use TypeLang\Type\Condition\LessThanOrEqualConditionNode; use TypeLang\Type\Condition\NotEqualConditionNode; use TypeLang\Type\ConstMaskNode; use TypeLang\Type\IntersectionTypeNode; @@ -23,21 +19,24 @@ use TypeLang\Type\NamedTypeNode; use TypeLang\Type\Node; use TypeLang\Type\NullableTypeNode; -use TypeLang\Type\Shape\ClassConstFieldNode; -use TypeLang\Type\Shape\ClassConstMaskFieldNode; -use TypeLang\Type\Shape\ConstMaskFieldNode; +use TypeLang\Type\Shape\ComplexFieldNode; use TypeLang\Type\Shape\FieldNode; use TypeLang\Type\Shape\FieldsListNode; use TypeLang\Type\Shape\NamedFieldNode; -use TypeLang\Type\Shape\NumericFieldNode; -use TypeLang\Type\Shape\StringNamedFieldNode; +use TypeLang\Type\Shape\ScalarFieldNode; use TypeLang\Type\Template\TemplateArgumentListNode; use TypeLang\Type\Template\TemplateArgumentNode; +use TypeLang\Type\Template\TemplateBoundEdgeNode; +use TypeLang\Type\Template\TemplateParameterListNode; +use TypeLang\Type\Template\TemplateParameterNode; use TypeLang\Type\TernaryExpressionNode; +use TypeLang\Type\ThisNode; use TypeLang\Type\TypeNode; use TypeLang\Type\TypeOffsetAccessNode; use TypeLang\Type\TypesListNode; use TypeLang\Type\UnionTypeNode; +use TypeLang\Type\VariableNode; +use TypeLang\Type\WildcardNode; class PrettyTypePrinter extends TypePrinter { @@ -131,6 +130,8 @@ protected function make(TypeNode $stmt): string $stmt instanceof TernaryExpressionNode => $this->printTernaryType($stmt), $stmt instanceof TypesListNode => $this->printTypeListNode($stmt), $stmt instanceof TypeOffsetAccessNode => $this->printTypeOffsetAccessNode($stmt), + $stmt instanceof WildcardNode => $this->printWildcardNode($stmt), + $stmt instanceof ThisNode => $this->printThisNode($stmt), default => throw NonPrintableNodeException::becauseInvalidNodeGiven($stmt), }; } @@ -232,53 +233,39 @@ protected function printShapeFieldNode(FieldNode $field): string return $this->make($field->type); } + /** + * @throws NonPrintableNodeException + */ protected function printShapeFieldName(FieldNode $field): string { return match (true) { - $field instanceof StringNamedFieldNode => $this->printStringShapeFieldName($field), - $field instanceof NumericFieldNode => $this->printNumericShapeFieldName($field), $field instanceof NamedFieldNode => $this->printNamedShapeFieldName($field), - $field instanceof ConstMaskFieldNode => $this->printConstMaskShapeFieldName($field), - $field instanceof ClassConstMaskFieldNode => $this->printClassConstMaskShapeFieldName($field), - $field instanceof ClassConstFieldNode => $this->printClassConstShapeFieldName($field), + $field instanceof ScalarFieldNode => $this->printScalarShapeFieldName($field), + $field instanceof ComplexFieldNode => $this->printComplexShapeFieldName($field), default => $this->printUnknownShapeFieldName($field), }; } - protected function printStringShapeFieldName(StringNamedFieldNode $field): string - { - return $field->key->raw; - } - - protected function printNumericShapeFieldName(NumericFieldNode $field): string - { - return $field->key->raw; - } - protected function printNamedShapeFieldName(NamedFieldNode $field): string { return $field->key->toString(); } - protected function printConstMaskShapeFieldName(ConstMaskFieldNode $field): string - { - return $field->key->name->toString() . '*'; - } - - protected function printClassConstShapeFieldName(ClassConstFieldNode $field): string + /** + * A scalar key is written back the way it was written, so that a "0x2A" + * does not come out as a "42". + */ + protected function printScalarShapeFieldName(ScalarFieldNode $field): string { - return \sprintf('%s::%s', $field->key->class, $field->key->constant); + return $field->key->raw; } - protected function printClassConstMaskShapeFieldName(ClassConstMaskFieldNode $field): string + /** + * @throws NonPrintableNodeException + */ + protected function printComplexShapeFieldName(ComplexFieldNode $field): string { - $constant = $field->key->constant; - - if ($constant === null) { - return \sprintf('%s::*', $field->key->class); - } - - return \sprintf('%s::%s*', $field->key->class, $constant); + return $this->make($field->key); } protected function printUnknownShapeFieldName(FieldNode $field): string @@ -318,6 +305,54 @@ protected function printTemplateArgumentNode(TemplateArgumentNode $argument): st return $result; } + /** + * @param TemplateParameterListNode|TemplateParameterListNode $parameters + * @return non-empty-string + * @throws NonPrintableNodeException + */ + protected function printTemplateParametersNode(TemplateParameterListNode $parameters): string + { + $result = []; + + foreach ($parameters as $parameter) { + $result[] = $this->printTemplateParameterNode($parameter); + } + + return \sprintf('<%s>', \implode(', ', $result)); + } + + /** + * @return non-empty-string + * @throws NonPrintableNodeException + */ + protected function printTemplateParameterNode(TemplateParameterNode $parameter): string + { + $result = $parameter->name->toString(); + + if ($parameter->upper !== null) { + $result .= ' ' . $this->printTemplateBoundEdgeNode($parameter->upper); + } + + if ($parameter->lower !== null) { + $result .= ' ' . $this->printTemplateBoundEdgeNode($parameter->lower); + } + + if ($parameter->default !== null) { + $result .= ' = ' . $this->make($parameter->default); + } + + return $result; + } + + /** + * @return non-empty-string + * @throws NonPrintableNodeException + */ + protected function printTemplateBoundEdgeNode(TemplateBoundEdgeNode $edge): string + { + return \sprintf('%s %s', $edge->operator->toString(), $this->make($edge->type)); + } + /** * @return non-empty-string */ @@ -336,7 +371,7 @@ protected function printClassConstMaskNode(ClassConstMaskNode $node): string { return \vsprintf('%s::%s', [ $node->class->toString(), - (string) $node->constant?->toString() . '*', + $node->mask->toString(), ]); } @@ -345,7 +380,41 @@ protected function printClassConstMaskNode(ClassConstMaskNode $node): string */ protected function printConstMaskNode(ConstMaskNode $node): string { - return $node->name->toString() . '*'; + $result = $node->mask->toString(); + + if ($node->namespace !== null) { + $result = $node->namespace->toUnqualifiedString() . '\\' . $result; + } + + if ($node->isFullyQualified) { + return '\\' . $result; + } + + return $result; + } + + /** + * @return non-empty-string + */ + protected function printThisNode(ThisNode $node): string + { + return '$this'; + } + + /** + * @return non-empty-string + */ + protected function printVariableNode(VariableNode $node): string + { + return '$' . $node->name->toString(); + } + + /** + * @return non-empty-string + */ + protected function printWildcardNode(WildcardNode $node): string + { + return $node->toString(); } /** @@ -356,6 +425,11 @@ protected function printCallableTypeNode(CallableTypeNode $node): string { $result = $node->name->toString(); + // Add template parameters + if ($node->templates !== null) { + $result .= $this->printTemplateParametersNode($node->templates); + } + $arguments = []; foreach ($node->parameters as $argument) { @@ -406,8 +480,7 @@ protected function printCallableArgumentNode(CallableParameterNode $node): strin } if ($node->name !== null) { - // @phpstan-ignore-next-line : VariableLiteralNode is a subtype of LiteralNode - $result .= $this->printLiteralNode($node->name); + $result .= $this->printVariableNode($node->name); } if ($node->isOptional) { @@ -552,27 +625,34 @@ protected function printNullableType(NullableTypeNode $node): string protected function printTernaryType(TernaryExpressionNode $node): string { return \vsprintf('(%s %s %s ? %s : %s)', [ - $this->make($node->condition->subject), + $this->printConditionOperand($node->condition->subject), $this->printCondition($node->condition), - $this->make($node->condition->target), + $this->printConditionOperand($node->condition->target), $this->make($node->then), $this->make($node->else), ]); } /** - * @return non-empty-string * @throws NonPrintableNodeException */ + protected function printConditionOperand(TypeNode|VariableNode $node): string + { + if ($node instanceof VariableNode) { + return $this->printVariableNode($node); + } + + return $this->make($node); + } + + /** + * @return non-empty-string + */ protected function printCondition(Condition $node): string { return match (true) { $node instanceof EqualConditionNode => 'is', $node instanceof NotEqualConditionNode => 'is not', - $node instanceof GreaterThanOrEqualConditionNode => '>=', - $node instanceof LessThanOrEqualConditionNode => '<=', - $node instanceof GreaterThanConditionNode => '>', - $node instanceof LessThanConditionNode => '<', default => throw NonPrintableNodeException::becauseInvalidNodeGiven($node), }; } From b41308b8405b59f9ee375d3b46a0d571bc3468d8 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:37:34 +0300 Subject: [PATCH 070/119] Fix tests --- libs/printer/tests/NativeTypePrinterTest.php | 5 ++ libs/printer/tests/PrettyTypePrinterTest.php | 52 ++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/libs/printer/tests/NativeTypePrinterTest.php b/libs/printer/tests/NativeTypePrinterTest.php index 31cdd24..b21e334 100644 --- a/libs/printer/tests/NativeTypePrinterTest.php +++ b/libs/printer/tests/NativeTypePrinterTest.php @@ -119,6 +119,11 @@ public function testPrintConstantMaskAsMixed(): void self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('FOO_*'))); } + public function testPrintWildcardAsMixed(): void + { + self::assertSame('mixed', (new NativeTypePrinter())->print(new \TypeLang\Type\WildcardNode())); + } + public function testPrintTypeOffsetAccessAsMixed(): void { self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('Foo[Bar]'))); diff --git a/libs/printer/tests/PrettyTypePrinterTest.php b/libs/printer/tests/PrettyTypePrinterTest.php index 4861e28..10fcee6 100644 --- a/libs/printer/tests/PrettyTypePrinterTest.php +++ b/libs/printer/tests/PrettyTypePrinterTest.php @@ -256,6 +256,54 @@ public function testPrintConstantMask(): void self::assertSame('FOO_*', $this->printer()->print(self::parse('FOO_*'))); } + /** + * @return iterable + */ + public static function maskProvider(): iterable + { + yield 'a wildcard alone' => ['Foo::*']; + yield 'a trailing wildcard' => ['Foo::BAR_*']; + yield 'a leading wildcard' => ['Foo::*_BAR']; + yield 'a wildcard in between' => ['Foo::A*B']; + yield 'several wildcards' => ['Foo::BAR*BAZ*SOME']; + yield 'a global mask' => ['JSON_*_FLAG']; + yield 'a namespaced global mask' => ['Some\Any\JSON_*']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('maskProvider')] + public function testPrintMaskOfEverySegmentItIsWrittenOf(string $type): void + { + self::assertSame($type, $this->printer()->print(self::parse($type))); + } + + /** + * @return iterable + */ + public static function wildcardAndTemplateProvider(): iterable + { + yield 'a wildcard argument' => ['Collection<*>']; + yield 'a wildcard beside a type' => ['HashMap']; + yield 'a hinted wildcard' => ['Collection']; + yield 'a template parameter' => ['callable(T): T']; + yield 'a bounded template parameter' => ['callable(T): T']; + yield 'every kind of bound' => ['Closure(T, U): V']; + yield 'several bounds on one parameter' => ['callable(T): void']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('wildcardAndTemplateProvider')] + public function testPrintWildcardsAndTemplateParameters(string $type): void + { + self::assertSame($type, $this->printer()->print(self::parse($type))); + } + public function testPrintTypesList(): void { self::assertSame('int[]', $this->printer()->print(self::parse('int[]'))); @@ -304,10 +352,6 @@ public static function ternaryConditionProvider(): iterable { yield 'equal' => ['($x is int ? string : bool)', '($x is int ? string : bool)']; yield 'not equal' => ['($x is not int ? string : bool)', '($x is not int ? string : bool)']; - yield 'greater than' => ['($x > 5 ? string : bool)', '($x > 5 ? string : bool)']; - yield 'greater than or equal' => ['($x >= 5 ? string : bool)', '($x >= 5 ? string : bool)']; - yield 'less than' => ['($x < 5 ? string : bool)', '($x < 5 ? string : bool)']; - yield 'less than or equal' => ['($x <= 5 ? string : bool)', '($x <= 5 ? string : bool)']; } public function testPrintUsesCustomNewLineAndIndention(): void From c424f704c94e58cb7ad2d7b18baabf2ae286facd Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:37:53 +0300 Subject: [PATCH 071/119] Remove phpstan/phpdoc-parser from phpdoc dev-dependencies --- libs/phpdoc/composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/phpdoc/composer.json b/libs/phpdoc/composer.json index 35f4349..76c4fd4 100644 --- a/libs/phpdoc/composer.json +++ b/libs/phpdoc/composer.json @@ -18,7 +18,6 @@ } }, "require-dev": { - "phpstan/phpdoc-parser": "^2.3", "phpstan/phpstan": "^2.2", "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, From 4a77bf31e075639be200f8381f7abd8d275b0852 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:38:21 +0300 Subject: [PATCH 072/119] Move benchmarks to the root package --- .../PHPStanStringUnescaperBench.php | 23 ++ .../StringDecoding/StringDecodingBench.php | 41 +++ .../TypeLangStringDecoderBench.php | 23 ++ bench/PhpDoc/Tools/DocBlockParserBench.php | 290 ++++++++++++++++++ .../PhpDoc/Tools}/PHPStanParserBench.php | 13 +- .../Tools}/PhpDocumentorParserBench.php | 13 +- .../PhpDoc/Tools}/TypeLangParserBench.php | 13 +- composer.json | 13 +- .../tests/Bench/DocBlockParserBench.php | 28 -- phpbench.json | 82 +++-- 10 files changed, 467 insertions(+), 72 deletions(-) create mode 100644 bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php create mode 100644 bench/Parser/StringDecoding/StringDecodingBench.php create mode 100644 bench/Parser/StringDecoding/TypeLangStringDecoderBench.php create mode 100644 bench/PhpDoc/Tools/DocBlockParserBench.php rename {libs/phpdoc/tests/Bench => bench/PhpDoc/Tools}/PHPStanParserBench.php (69%) rename {libs/phpdoc/tests/Bench => bench/PhpDoc/Tools}/PhpDocumentorParserBench.php (51%) rename {libs/phpdoc/tests/Bench => bench/PhpDoc/Tools}/TypeLangParserBench.php (50%) delete mode 100644 libs/phpdoc/tests/Bench/DocBlockParserBench.php diff --git a/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php b/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php new file mode 100644 index 0000000..1deb87f --- /dev/null +++ b/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php @@ -0,0 +1,23 @@ + + */ + private const REPEATS = 16; + + /** + * @return iterable + */ + public static function stringsDataProvider(): iterable + { + yield 'plain' => ['string' => '"example"', 'double' => true]; + yield 'plain long' => ['string' => '"' . \str_repeat(self::LOREM, 8) . '"', 'double' => true]; + yield 'quote' => ['string' => '"a\"b"', 'double' => true]; + yield 'few specials' => ['string' => '"line\nand\ttab"', 'double' => true]; + yield 'many specials' => ['string' => '"' . \str_repeat('a\nb\tc\r', self::REPEATS) . '"', 'double' => true]; + yield 'many backslashes' => ['string' => '"' . \str_repeat('a\\\\b', self::REPEATS) . '"', 'double' => true]; + yield 'one hexadecimal' => ['string' => '"a\x41b"', 'double' => true]; + yield 'numeric mix' => ['string' => '"\x41\101\u{1F600}\x42"', 'double' => true]; + yield 'many numeric' => ['string' => '"' . \str_repeat('\x41\101\u{48}', self::REPEATS) . '"', 'double' => true]; + yield 'single quoted plain' => ['string' => "'" . \str_repeat(self::LOREM, 8) . "'", 'double' => false]; + yield 'single quoted escaped' => ['string' => "'" . \str_repeat('a\\\\b\\\'c', self::REPEATS) . "'", 'double' => false]; + } + + /** + * @param array{string: non-empty-string, double: bool} $params + */ + abstract public function benchDecodeString(array $params): void; +} diff --git a/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php b/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php new file mode 100644 index 0000000..05592bc --- /dev/null +++ b/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php @@ -0,0 +1,23 @@ + + */ + private const REPEATS = 16; + + /** + * @return iterable + */ + public static function docBlocksDataProvider(): iterable + { + yield 'empty' => ['docblock' => '/** */']; + + yield 'inline description' => ['docblock' => '/** An example of the inline description. */']; + + yield 'inline tag' => ['docblock' => '/** @return void */']; + + yield 'description' => ['docblock' => <<<'DOC' + /** + * The `@link` tag can be used to define a relation, or link, between + * the element, or part of the long description when used inline, to a URI. + */ + DOC]; + + yield 'description long' => ['docblock' => self::createDescription(self::REPEATS)]; + + yield 'scalar types' => ['docblock' => <<<'DOC' + /** + * @param int $offset + * @param string $name + * @param bool $strict + * @param float $ratio + * @param mixed $context + * @return void + */ + DOC]; + + yield 'union types' => ['docblock' => <<<'DOC' + /** + * @param int|string $key + * @param string|null $name + * @param ?iterable $items + * @param bool|int|float|string|null $scalar + * @return array|false + */ + DOC]; + + yield 'intersection types' => ['docblock' => <<<'DOC' + /** + * @param \Traversable&\Countable $items + * @param (\Stringable&\JsonSerializable)|null $value + * @param \IteratorAggregate&\ArrayAccess&\Countable $collection + * @return \Traversable&\Countable + */ + DOC]; + + yield 'named types' => ['docblock' => <<<'DOC' + /** + * @param \TypeLang\PhpDoc\DocBlockParserInterface $parser + * @param DocBlock\Tag\TagInterface $tag + * @param self $self + * @param static $static + * @return $this + */ + DOC]; + + yield 'generic types' => ['docblock' => <<<'DOC' + /** + * @param array $list + * @param iterable $items + * @param \Traversable>> $nested + * @param class-string<\Throwable> $exception + * @return \Generator> + */ + DOC]; + + yield 'shape types' => ['docblock' => <<<'DOC' + /** + * @param array{int, string, bool} $tuple + * @param array{name: string, age?: int<0, max>} $struct + * @param array{ + * id: positive-int, + * meta: array{ + * created: \DateTimeInterface, + * tags: list, + * ... + * } + * } $nested + * @param object{name: string, value?: mixed} $object + * @return array{} + */ + DOC]; + + yield 'callable types' => ['docblock' => <<<'DOC' + /** + * @param callable $any + * @param callable(): void $empty + * @param callable(int, string): bool $simple + * @param \Closure(non-empty-string, mixed...): (int|string) $variadic + * @param callable(callable(int): string): callable(string): int $nested + * @return \Closure(): \Generator + */ + DOC]; + + yield 'literal types' => ['docblock' => <<<'DOC' + /** + * @param 'read'|'write'|'append' $mode + * @param 1|2|3|42 $version + * @param 0.1|-0.5 $ratio + * @param true|false $flag + * @param "double \"quoted\" literal" $quoted + * @return 'ok' + */ + DOC]; + + yield 'const types' => ['docblock' => <<<'DOC' + /** + * @param \PDO::FETCH_* $mode + * @param self::STATUS_ACTIVE|self::STATUS_INACTIVE $status + * @param \PHP_INT_MAX $max + * @param int<0, \PHP_INT_MAX> $range + * @return static::DEFAULT_VALUE + */ + DOC]; + + yield 'conditional types' => ['docblock' => <<<'DOC' + /** + * @template T + * @param T $value + * @return (T is int ? string : (T is string ? int : bool)) + */ + DOC]; + + yield 'template tags' => ['docblock' => <<<'DOC' + /** + * @template TKey of array-key + * @template TValue of object + * @template-covariant TResult + * @extends \IteratorAggregate + * @implements \ArrayAccess + * @param TKey $key + * @param TValue $value + * @return static + */ + DOC]; + + yield 'inline tags' => ['docblock' => <<<'DOC' + /** + * {@inheritDoc} + * + * See the {@see \TypeLang\PhpDoc\DocBlockParser::parse()} method and + * the {@link https://www.ietf.org/rfc/rfc2396.txt RFC2396} document. + * + * @return void + */ + DOC]; + + yield 'metadata tags' => ['docblock' => <<<'DOC' + /** + * @author Nesmeyanov Kirill + * @copyright 2024 TypeLang + * @license MIT + * @since 1.0 + * @deprecated since 2.0, use something else instead + * @internal + * @api + * @final + * @todo Remove this method + */ + DOC]; + + yield 'unknown tags' => ['docblock' => <<<'DOC' + /** + * @some-vendor-tag with an arbitrary payload + * @another_tag(with, parens) + * @x-custom {"json": "like", "payload": [1, 2, 3]} + * @return void + */ + DOC]; + + yield 'real world' => ['docblock' => <<<'DOC' + /** + * The `@link` tag can be used to define a relation, or link, between + * the element, or part of the long description when used inline, to a URI. + * + * ``` + * "@link" [ | ] [] + * ``` + * + * @link https://www.ietf.org/rfc/rfc2396.txt RFC2396 + * @return iterable }> + * }> + */ + DOC]; + + yield 'many params' => ['docblock' => self::createTags(self::REPEATS, [ + '@param int<0, max> $offset%d', + '@param non-empty-string $name%d', + '@param list $items%d', + ])]; + + yield 'many throws' => ['docblock' => self::createTags(self::REPEATS, [ + '@throws \RuntimeException in case of a %d error', + '@throws \InvalidArgumentException in case of an invalid #%d argument', + ])]; + + yield 'many mixed tags' => ['docblock' => self::createTags(self::REPEATS, [ + '@param \Closure(int, string...): (bool|null) $callback%d', + '@param array{a: int, b: array{c: string, d?: list}} $shape%d', + '@var iterable> $iterable%d', + '@throws \LogicException on the %d failure', + '@see \TypeLang\PhpDoc\DocBlockParser::parse() the #%d reference', + '@deprecated since %d.0', + ])]; + + yield 'huge' => ['docblock' => self::createHuge()]; + } + + /** + * @param int<1, max> $repeats + * @return non-empty-string + */ + private static function createDescription(int $repeats): string + { + $lines = ['/**']; + + for ($i = 0; $i < $repeats; ++$i) { + $lines[] = ' * Lorem ipsum dolor sit amet, consectetur adipiscing elit,'; + $lines[] = ' * sed do eiusmod tempor incididunt ut labore et dolore magna.'; + $lines[] = ' *'; + } + + $lines[] = ' */'; + + return \implode("\n", $lines); + } + + /** + * @param int<1, max> $repeats + * @param non-empty-list $templates sprintf templates of the tag lines + * @return non-empty-string + */ + private static function createTags(int $repeats, array $templates): string + { + $lines = ['/**']; + + for ($i = 0; $i < $repeats; ++$i) { + foreach ($templates as $template) { + $lines[] = ' * ' . \sprintf($template, $i); + } + } + + $lines[] = ' */'; + + return \implode("\n", $lines); + } + + /** + * @return non-empty-string + */ + private static function createHuge(): string + { + // Removes the trailing " */" line terminator of the description block. + $description = \substr(self::createDescription(self::REPEATS), 0, -3); + + // Removes the leading "/**" line opener of the tags block. + $tags = \substr(self::createTags(self::REPEATS, [ + '@param \Closure(int, string...): (bool|null) $callback%d', + '@param array{a: int, b: array{c: string, d?: list}} $shape%d', + '@throws \LogicException on the %d failure', + ]), 4); + + return $description . $tags; + } + + /** + * @param array{docblock: non-empty-string} $params + */ + abstract public function benchParseDocBlock(array $params): void; +} diff --git a/libs/phpdoc/tests/Bench/PHPStanParserBench.php b/bench/PhpDoc/Tools/PHPStanParserBench.php similarity index 69% rename from libs/phpdoc/tests/Bench/PHPStanParserBench.php rename to bench/PhpDoc/Tools/PHPStanParserBench.php index 2e265ca..7494d0c 100644 --- a/libs/phpdoc/tests/Bench/PHPStanParserBench.php +++ b/bench/PhpDoc/Tools/PHPStanParserBench.php @@ -2,10 +2,12 @@ declare(strict_types=1); -namespace TypeLang\PhpDoc\Tests\Bench; +namespace TypeLang\Bench\PhpDoc\Tools; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; use PhpBench\Attributes\RetryThreshold; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; @@ -16,8 +18,8 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\ParserConfig; -#[Revs(20), Warmup(5), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] -final readonly class PHPStanParserBench extends DocBlockParserBench +#[Groups(['phpstan']), Revs(500), Warmup(50), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] +final class PHPStanParserBench extends DocBlockParserBench { private Lexer $lexer; private PhpDocParser $parser; @@ -37,9 +39,10 @@ public function prepare(): void $this->parser = new PhpDocParser($config, $typeParser, $constExprParser); } - public function benchParseDocBlock(): void + #[ParamProviders('docBlocksDataProvider')] + public function benchParseDocBlock(array $params): void { - $iterator = new TokenIterator($this->lexer->tokenize(self::DOC_BLOCK_SAMPLE)); + $iterator = new TokenIterator($this->lexer->tokenize($params['docblock'])); $this->parser->parse($iterator); } diff --git a/libs/phpdoc/tests/Bench/PhpDocumentorParserBench.php b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php similarity index 51% rename from libs/phpdoc/tests/Bench/PhpDocumentorParserBench.php rename to bench/PhpDoc/Tools/PhpDocumentorParserBench.php index b5210b3..8a9fe5a 100644 --- a/libs/phpdoc/tests/Bench/PhpDocumentorParserBench.php +++ b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php @@ -2,18 +2,20 @@ declare(strict_types=1); -namespace TypeLang\PhpDoc\Tests\Bench; +namespace TypeLang\Bench\PhpDoc\Tools; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; use PhpBench\Attributes\RetryThreshold; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; -#[Revs(20), Warmup(5), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] -final readonly class PhpDocumentorParserBench extends DocBlockParserBench +#[Groups(['phpdocumentor']), Revs(500), Warmup(50), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] +final class PhpDocumentorParserBench extends DocBlockParserBench { private DocBlockFactoryInterface $parser; @@ -22,8 +24,9 @@ public function prepare(): void $this->parser = DocBlockFactory::createInstance(); } - public function benchParseDocBlock(): void + #[ParamProviders('docBlocksDataProvider')] + public function benchParseDocBlock(array $params): void { - $this->parser->create(self::DOC_BLOCK_SAMPLE); + $this->parser->create($params['docblock']); } } diff --git a/libs/phpdoc/tests/Bench/TypeLangParserBench.php b/bench/PhpDoc/Tools/TypeLangParserBench.php similarity index 50% rename from libs/phpdoc/tests/Bench/TypeLangParserBench.php rename to bench/PhpDoc/Tools/TypeLangParserBench.php index e2628e5..97069cc 100644 --- a/libs/phpdoc/tests/Bench/TypeLangParserBench.php +++ b/bench/PhpDoc/Tools/TypeLangParserBench.php @@ -2,18 +2,20 @@ declare(strict_types=1); -namespace TypeLang\PhpDoc\Tests\Bench; +namespace TypeLang\Bench\PhpDoc\Tools; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; use PhpBench\Attributes\RetryThreshold; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; use TypeLang\PhpDoc\DocBlockParser; use TypeLang\PhpDoc\DocBlockParserInterface; -#[Revs(20), Warmup(5), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] -final readonly class TypeLangParserBench extends DocBlockParserBench +#[Groups(['typelang', 'baseline']), Revs(500), Warmup(50), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] +final class TypeLangParserBench extends DocBlockParserBench { private DocBlockParserInterface $parser; @@ -22,8 +24,9 @@ public function prepare(): void $this->parser = new DocBlockParser(); } - public function benchParseDocBlock(): void + #[ParamProviders('docBlocksDataProvider')] + public function benchParseDocBlock(array $params): void { - $this->parser->parse(self::DOC_BLOCK_SAMPLE); + $this->parser->parse($params['docblock']); } } diff --git a/composer.json b/composer.json index d75f0da..3b74f8f 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,12 @@ }, "autoload-dev": { "psr-4": { - "TypeLang\\Parser\\Tests\\": "libs/parser/tests", - "TypeLang\\Printer\\Tests\\": "libs/printer/tests", - "TypeLang\\PhpDoc\\Tests\\": "libs/phpdoc/tests", - "TypeLang\\Reader\\Tests\\": "libs/reader/tests", - "TypeLang\\Type\\Tests\\": "libs/types/tests" + "TypeLang\\Bench\\": "bench", + "TypeLang\\Parser\\Tests\\": "libs/parser/tests", + "TypeLang\\Printer\\Tests\\": "libs/printer/tests", + "TypeLang\\PhpDoc\\Tests\\": "libs/phpdoc/tests", + "TypeLang\\Reader\\Tests\\": "libs/reader/tests", + "TypeLang\\Type\\Tests\\": "libs/types/tests" } }, "extra": { @@ -56,7 +57,7 @@ "phpcs:check": ["phpcs", "cs", "psr", "per"] }, "scripts": { - "bench": "phpbench run libs/phpdoc/tests --report=default", + "bench": "phpbench run ./bench --report=compare", "test:unit": "phpunit --testdox", "linter:check": "phpstan analyse --configuration phpstan.neon --memory-limit 256M", "linter:baseline": "@linter:check --generate-baseline", diff --git a/libs/phpdoc/tests/Bench/DocBlockParserBench.php b/libs/phpdoc/tests/Bench/DocBlockParserBench.php deleted file mode 100644 index da74224..0000000 --- a/libs/phpdoc/tests/Bench/DocBlockParserBench.php +++ /dev/null @@ -1,28 +0,0 @@ - | ] [] - * ``` - * - * @link https://www.ietf.org/rfc/rfc2396.txt RFC2396 - * @return iterable }> - * }> - */ - DOC; - - abstract public function benchParseDocBlock(): void; -} diff --git a/phpbench.json b/phpbench.json index 4e6f73c..0890f63 100644 --- a/phpbench.json +++ b/phpbench.json @@ -1,14 +1,22 @@ { "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", "runner.bootstrap": "vendor/autoload.php", - "runner.path": "tests", + "runner.path": "bench", "runner.progress": "plain", "runner.file_pattern": "*Bench.php", "runner.php_config": { "opcache.enable": 1, + "zend.assertions": -1, "opcache.enable_cli": 1, + "opcache.jit": "tracing", "opcache.jit_buffer_size": "128M", - "xdebug.mode": "off" + "opcache.jit_hot_loop": 1, + "opcache.jit_hot_func": 1, + "opcache.jit_hot_return": 1, + "opcache.jit_hot_side_exit": 1, + "xdebug.mode": "off", + "pcov.enabled": 0, + "memory_limit": -1 }, "storage.xml_storage_path": "var", "storage.store_binary": true, @@ -20,34 +28,62 @@ "components": [ { "component": "section", - "tabbed": true, - "tab_labels": [ - "Time", - "Memory" + "title": "Results", + "partition": [ + "subject_name" ], "components": [ { - "component": "section", - "title": "Results", - "components": [ - { - "component": "table_aggregate", - "title": "{{ first(frame.suite_tag) }}", + "component": "table_aggregate", + "title": "{{ first(frame['subject_name']) }}", + "partition": [ + "benchmark_name", + "variant_name" + ], + "row": { + "benchmark": "first(partition['benchmark_name'])", + "set": "first(partition['variant_name'])", + "memory": "first(partition['result_mem_peak']) as memory", + "min": "min(partition['result_time_avg']) as time", + "max": "max(partition['result_time_avg']) as time", + "mode": "mode(partition['result_time_avg']) as time", + "rstdev": "rstdev(partition['result_time_avg'])" + } + } + ] + } + ] + }, + "compare": { + "extends": "overview", + "tabbed": false, + "components": [ + { + "component": "section", + "title": "Comparison", + "partition": [ + "subject_name" + ], + "components": [ + { + "component": "table_aggregate", + "title": "{{ first(frame['subject_name']) }}", + "partition": [ + "variant_name" + ], + "row": { + "set": "first(partition['variant_name'])", + "tools": { + "type": "expand", + "var": "tool", "partition": [ - "benchmark_name", - "subject_name", - "variant_name" + "coalesce(first(subject_groups), benchmark_name)" ], - "row": { - "benchmark": "first(partition['benchmark_name'])", - "memory": "first(partition['result_mem_peak']) as memory", - "min": "min(partition['result_time_avg']) as time", - "max": "max(partition['result_time_avg']) as time", - "mode": "mode(partition['result_time_avg']) as time", - "rstdev": "rstdev(partition['result_time_avg'])" + "cols": { + "{{ key }}": "(mode(tool['result_time_avg']) as time) ~ if(count(partition[contains(subject_groups, 'baseline')]['result_time_avg']) > 0, if(mode(partition[contains(subject_groups, 'baseline')]['result_time_avg']) > 0, ' (' ~ format('%.2fx', mode(tool['result_time_avg']) / mode(partition[contains(subject_groups, 'baseline')]['result_time_avg'])) ~ ')', ''), '')" } } - ] + } } ] } From bc732d0dd3dce107a26c04d8b0fbcd882d4e5e66 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:52:43 +0300 Subject: [PATCH 073/119] Add ConstMaskNode virtual properties --- libs/types/src/ConstMaskNode.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/libs/types/src/ConstMaskNode.php b/libs/types/src/ConstMaskNode.php index 74a44fd..12f0e2e 100644 --- a/libs/types/src/ConstMaskNode.php +++ b/libs/types/src/ConstMaskNode.php @@ -15,6 +15,9 @@ * *_SOME * ^^^^^^ a mask with no namespace at all * ``` + * + * @property-read Name|null $namespace An optional namespace the constant belongs to + * @property-read bool $isFullyQualified */ final class ConstMaskNode extends TypeNode { @@ -39,6 +42,17 @@ public function __construct( parent::__construct($offset); } + public function getNamespace(): ?Name + { + $context = $this->namespaceOrFullyQualified; + + if ($context instanceof Name) { + return $context; + } + + return null; + } + /** * Gets whether the reference is to be read from the root namespace. */ @@ -71,4 +85,18 @@ public function setFullyQualified(bool $isFullyQualified = true): void $this->namespaceOrFullyQualified = $isFullyQualified; } + + public function __get(string $property): Name|bool|null + { + return match ($property) { + 'namespace' => $this->getNamespace(), + 'isFullyQualified' => $this->isFullyQualified(), + default => throw new \Error(\sprintf('Undefined property %s::$%s', self::class, $property)), + }; + } + + public function __isset(string $name): bool + { + return \in_array($name, ['namespace', 'isFullyQualified'], true); + } } From 1e465f47e26aed82bd2835447eb83ce3ac50124b Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Mon, 7 Sep 2026 23:52:56 +0300 Subject: [PATCH 074/119] Fix ConstMaskNode tests --- libs/types/tests/ConstMaskNodeTest.php | 65 +++++++++++++++++++------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/libs/types/tests/ConstMaskNodeTest.php b/libs/types/tests/ConstMaskNodeTest.php index cdd48e8..8892e16 100644 --- a/libs/types/tests/ConstMaskNodeTest.php +++ b/libs/types/tests/ConstMaskNodeTest.php @@ -23,12 +23,12 @@ public function constructorStoresTheMask(): void } #[Test] - public function namespaceIsNullByDefault(): void + public function aMaskIsWrittenWithNoNamespaceByDefault(): void { $node = new ConstMaskNode(new MaskNode([new WildcardNode(), new Identifier('_SOME')])); - self::assertNull($node->namespace); - self::assertFalse($node->isFullyQualified); + self::assertFalse($node->namespaceOrFullyQualified); + self::assertFalse($node->isFullyQualified()); } #[Test] @@ -40,47 +40,80 @@ public function constructorStoresTheNamespace(): void $namespace, ); - self::assertSame($namespace, $node->namespace); + self::assertSame($namespace, $node->namespaceOrFullyQualified); + self::assertFalse($node->isFullyQualified()); } /** - * The leading separator belongs to the node, so a namespace written with - * one is kept as a relative name and lifted into the flag. + * The leading separator belongs to the namespace, so the namespace is the + * one that says whether the reference is a fully qualified one. */ #[Test] - public function aFullyQualifiedNamespaceFillsTheFlag(): void + public function theNamespaceSaysTheReferenceIsFullyQualified(): void { $node = new ConstMaskNode( new MaskNode([new Identifier('SOME_'), new WildcardNode()]), Name::createFromString('\Some\Any'), ); - self::assertTrue($node->isFullyQualified); - self::assertNotNull($node->namespace); - self::assertFalse($node->namespace->isFullyQualified); - self::assertSame('Some\Any', $node->namespace->toString()); + self::assertTrue($node->isFullyQualified()); + } + + /** + * A mask written with no namespace has nothing to carry that separator, + * so it is passed on its own. + */ + #[Test] + public function aMaskWithNoNamespaceIsFullyQualifiedOnItsOwn(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + true, + ); + + self::assertTrue($node->namespaceOrFullyQualified); + self::assertTrue($node->isFullyQualified()); } #[Test] - public function aRelativeNamespaceLeavesTheFlagUnset(): void + public function theSeparatorIsSetOnTheNamespaceItIsWrittenWith(): void { $node = new ConstMaskNode( new MaskNode([new Identifier('SOME_'), new WildcardNode()]), Name::createFromString('Some\Any'), ); - self::assertFalse($node->isFullyQualified); + $node->setFullyQualified(); + + self::assertTrue($node->isFullyQualified()); + self::assertInstanceOf(Name::class, $node->namespaceOrFullyQualified); + self::assertSame('\Some\Any', $node->namespaceOrFullyQualified->toString()); } #[Test] - public function constructorStoresTheFullyQualifiedFlag(): void + public function theSeparatorIsTakenOffTheNamespaceItIsWrittenWith(): void { $node = new ConstMaskNode( new MaskNode([new Identifier('SOME_'), new WildcardNode()]), - true, + Name::createFromString('\Some\Any'), ); - self::assertTrue($node->isFullyQualified); + $node->setFullyQualified(false); + + self::assertFalse($node->isFullyQualified()); + self::assertInstanceOf(Name::class, $node->namespaceOrFullyQualified); + self::assertSame('Some\Any', $node->namespaceOrFullyQualified->toString()); + } + + #[Test] + public function theSeparatorIsSetOnAMaskWrittenWithNoNamespace(): void + { + $node = new ConstMaskNode(new MaskNode([new Identifier('SOME_'), new WildcardNode()])); + + $node->setFullyQualified(); + + self::assertTrue($node->namespaceOrFullyQualified); + self::assertTrue($node->isFullyQualified()); } #[Test] From 539a99963b1b4abe63352aea38a516ff8470956f Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 00:17:44 +0300 Subject: [PATCH 075/119] Rewrite parser --- libs/parser/bin/build | 2 +- libs/parser/resources/grammar.pp3 | 84 +- libs/parser/resources/grammar/callable.pp3 | 28 +- libs/parser/resources/grammar/common.pp3 | 33 +- libs/parser/resources/grammar/lexemes.pp3 | 92 +- libs/parser/resources/grammar/literals.pp3 | 90 +- libs/parser/resources/grammar/named-type.pp3 | 125 +- .../parser/resources/grammar/shape-fields.pp3 | 101 +- .../resources/grammar/template-arguments.pp3 | 24 +- .../resources/grammar/template-parameters.pp3 | 78 + libs/parser/resources/grammar/ternary.pp3 | 14 - .../src/Exception/ConstMaskException.php | 22 + .../src/Exception/SemanticException.php | 8 +- .../src/Exception/ShapeKeyException.php | 22 + .../src/Exception/TemplateBoundException.php | 53 + libs/parser/src/Internal/CompilerExecutor.php | 1575 ++++++++++------- libs/parser/src/Internal/Executor.php | 106 -- libs/parser/src/Internal/FloatDecoder.php | 13 + libs/parser/src/Internal/IntDecoder.php | 73 + .../src/Internal/IntDecoder/BigIntValue.php | 22 + libs/parser/src/Internal/StringDecoder.php | 76 +- .../StringDecoder/ScannerSequenceFetcher.php | 93 - .../SequenceFetcherInterface.php | 36 - ...Fetcher.php => StringSequencesFetcher.php} | 45 +- .../StringDecoder/UtfCharRenderer.php | 4 + 25 files changed, 1619 insertions(+), 1200 deletions(-) create mode 100644 libs/parser/resources/grammar/template-parameters.pp3 delete mode 100644 libs/parser/resources/grammar/ternary.pp3 create mode 100644 libs/parser/src/Exception/ConstMaskException.php create mode 100644 libs/parser/src/Exception/ShapeKeyException.php create mode 100644 libs/parser/src/Exception/TemplateBoundException.php create mode 100644 libs/parser/src/Internal/FloatDecoder.php create mode 100644 libs/parser/src/Internal/IntDecoder.php create mode 100644 libs/parser/src/Internal/IntDecoder/BigIntValue.php delete mode 100644 libs/parser/src/Internal/StringDecoder/ScannerSequenceFetcher.php delete mode 100644 libs/parser/src/Internal/StringDecoder/SequenceFetcherInterface.php rename libs/parser/src/Internal/StringDecoder/{PatternSequenceFetcher.php => StringSequencesFetcher.php} (54%) diff --git a/libs/parser/bin/build b/libs/parser/bin/build index d20ebdf..cad3791 100644 --- a/libs/parser/bin/build +++ b/libs/parser/bin/build @@ -46,5 +46,5 @@ if (!class_exists(Compiler::class)) { ->withTargetPhpVersion(TargetPhpVersion::Php81) ->withClassImport('TypeLang\\Type') ->withClassImport('TypeLang\\Parser\\Exception') - ->withClassImport('TypeLang\\Parser\\Literal') + ->withClassImport('TypeLang\\Parser\\Internal') ->save(__DIR__ . '/../src/Internal/CompilerExecutor.php'); diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3 index a7b9118..8ef5b9c 100644 --- a/libs/parser/resources/grammar.pp3 +++ b/libs/parser/resources/grammar.pp3 @@ -5,14 +5,14 @@ * ----------------------------------------------------------------------------- */ -%include grammar/lexemes // Lexeme/Token Definitions -%include grammar/common // Common Utils -%include grammar/literals // Literal Definitions +%include grammar/lexemes +%include grammar/common +%include grammar/literals %include grammar/template-arguments // T +%include grammar/template-parameters // callable(): void %include grammar/callable // callable(mixed): void %include grammar/shape-fields // array { key: int, ... } %include grammar/named-type // Map, non-empty-string> -%include grammar/ternary // T is A ? B : C %pragma root Type @@ -20,10 +20,6 @@ * ----------------------------------------------------------------------------- * Type Statement * ----------------------------------------------------------------------------- - * - * Constant references to external definitions or - * describe the type in some way. - * */ Type -> { @@ -46,26 +42,6 @@ Type -> { $children[2], $offset, ), - self::T_GTE => new Type\Condition\GreaterThanOrEqualConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_ANGLE_BRACKET_CLOSE => new Type\Condition\GreaterThanConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_LTE => new Type\Condition\LessThanOrEqualConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_ANGLE_BRACKET_OPEN => new Type\Condition\LessThanConditionNode( - $children[0], - $children[2], - $offset, - ), default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( $children[1]->value, $offset, @@ -80,22 +56,24 @@ Type -> { ); } : LogicalType() TernaryExpressionSuffix()? - | VariableLiteral() TernaryExpressionSuffix() + | Variable() TernaryExpressionSuffix() + ; + +TernaryExpressionSuffix + : TernaryExpressionOperator() (Type() | Variable()) + ::T_QMARK:: Type() + ::T_COLON:: Type() + ; + +TernaryExpressionOperator + : + | ; /** * ----------------------------------------------------------------------------- * Logical Statements * ----------------------------------------------------------------------------- - * - * Logical statements denote union or intersection types, like: - * - A | B | C - for union type references. - * - A & B & C - for intersection type references. - * - * A repetition rather than a right recursion: every member of a statement is - * collected in a single list, so the node is built once instead of being - * rebuilt (and re-flattened) at each of its levels. - * */ LogicalType @@ -111,8 +89,6 @@ UnionType -> { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); } - // A logical statement takes an arbitrary number of types, so its - // offset is assigned rather than passed to the constructor $statement = new Type\UnionTypeNode($children); $statement->offset = $offset; @@ -130,8 +106,6 @@ IntersectionType -> { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); } - // A logical statement takes an arbitrary number of types, so its - // offset is assigned rather than passed to the constructor $statement = new Type\IntersectionTypeNode($children); $statement->offset = $offset; @@ -144,28 +118,20 @@ IntersectionType -> { * ----------------------------------------------------------------------------- * Unary Statements * ----------------------------------------------------------------------------- - * - * Unary statements denote terminal types, like: - * - A - for type reference. - * - ?A - for nullable type reference. - * - 'some' - for string literal reference. - * - etc... - * */ UnaryType : NullableType() - | TypesList() + | ListOrOffsetType() ; -// stmt = ?Type NullableType -> { return new Type\NullableTypeNode($children[0], $offset); } - : ::T_QMARK:: TypesList() + : ::T_QMARK:: ListOrOffsetType() ; -TypesList -> { +ListOrOffsetType -> { $statement = $children[0]; $count = \count($children); @@ -173,7 +139,6 @@ TypesList -> { $child = $children[$i]; switch (true) { - // In case of list type case $child === true: if ($this->features->lists === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); @@ -181,7 +146,6 @@ TypesList -> { $statement = new Type\TypesListNode($statement, $offset); break; - // In case of offset access type case $child instanceof Type\TypeNode: if ($this->features->offsets === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); @@ -200,12 +164,10 @@ TypesList -> { return $statement; } : PrimaryType() - TypeListOrOffsetSuffix()* + ListOrOffsetSuffix()* ; -// - Returns `true` in case of a list type (`T[]`). -// - Returns `TypeNode` in case of an offset access type (`T[Y]`). -TypeListOrOffsetSuffix -> { +ListOrOffsetSuffix -> { return $children[0] ?? true; } : ::T_SQUARE_BRACKET_OPEN:: @@ -215,7 +177,7 @@ TypeListOrOffsetSuffix -> { PrimaryType : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: - | ThisLiteral() - | Literal() + | ThisType() + | LiteralType() | NamedType() ; diff --git a/libs/parser/resources/grammar/callable.pp3 b/libs/parser/resources/grammar/callable.pp3 index 9476069..ae91242 100644 --- a/libs/parser/resources/grammar/callable.pp3 +++ b/libs/parser/resources/grammar/callable.pp3 @@ -16,26 +16,20 @@ CallableParameters -> { )? ; -/** - * A parameter is written as a type, optionally followed by the reference and - * the variadic markers, by a name and by the default marker - in that order - * and in no other: - * - * - "" - * - "&" - * - "..." - * - "&..." - * - " $name" - * - "&...$name=" - * - */ CallableParameter -> { - $parameter = new Type\Callable\CallableParameterNode($children[0], offset: $offset); + $parameter = new Type\Callable\CallableParameterNode( + $children[0], + null, + false, + false, + false, + $offset, + ); for ($i = 1, $count = \count($children); $i < $count; ++$i) { $child = $children[$i]; - if ($child instanceof Type\Literal\VariableLiteralNode) { + if ($child instanceof Type\VariableNode) { $parameter->name = $child; continue; @@ -50,8 +44,6 @@ CallableParameter -> { $parameter->isVariadic = true; break; - // A variadic parameter is optional in itself, so a default marker - // beside one describes the same parameter in two different ways default: if ($parameter->isVariadic) { throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); @@ -66,7 +58,7 @@ CallableParameter -> { : Type() ? ? - VariableLiteral()? + Variable()? ? ; diff --git a/libs/parser/resources/grammar/common.pp3 b/libs/parser/resources/grammar/common.pp3 index 5142b52..47a120b 100644 --- a/libs/parser/resources/grammar/common.pp3 +++ b/libs/parser/resources/grammar/common.pp3 @@ -1,15 +1,18 @@ +// "Some\Any" or "\Some\Any" Name : FullQualifiedName() | RelativeName() ; +// "\Some\Any" FullQualifiedName -> { return new Type\Name($children, true, $offset); } : ::T_NS_DELIMITER:: Identifier() (::T_NS_DELIMITER:: Identifier())* ; +// "Some\Any" RelativeName -> { return new Type\Name($children, false, $offset); } @@ -17,8 +20,6 @@ RelativeName -> { ; Identifier -> { - // Only a name written with a trailing space carries one, so the value of - // every other token is the identifier itself return new Type\Identifier( $children->id === self::T_NAME_WITH_SPACE ? \rtrim($children->value) @@ -29,7 +30,8 @@ Identifier -> { : | | - | + | + | | ; @@ -38,3 +40,28 @@ IdentifierWithExtraSpace -> { } : ; + +// Any "$variable" (including "$this") +Variable -> { + return new Type\VariableNode( + new Type\Identifier(\substr($children->value, 1), $offset), + $offset, + ); +} + : + | + ; + +// The "$this" of a "callable(): $this" +ThisType -> { + return new Type\ThisNode($offset); +} + : + ; + +// "*" in "T<*>" or "JSON_*" +WildcardType -> { + return new Type\WildcardNode($offset); +} + : + ; diff --git a/libs/parser/resources/grammar/lexemes.pp3 b/libs/parser/resources/grammar/lexemes.pp3 index 55e0070..6fcbd3b 100644 --- a/libs/parser/resources/grammar/lexemes.pp3 +++ b/libs/parser/resources/grammar/lexemes.pp3 @@ -3,54 +3,81 @@ * ----------------------------------------------------------------------------- * Pattern Fragments * ----------------------------------------------------------------------------- - * - * The pieces the token definitions below are spelled out of. A fragment - * recognizes nothing on its own and reaches no token stream. - * */ +// +// Identifiers +// + %fragment IDENT_HEAD [a-zA-Z_\x80-\xff] %fragment IDENT_TAIL [a-zA-Z0-9\-_\x80-\xff] -%fragment DIGIT [0-9] -%fragment EXPONENT [eE]-?(?&DIGIT)++ -// A keyword ends where an identifier would go on, so that "isEmpty" is a name -// rather than the "is" operator followed by one. %fragment KEYWORD_END (?!(?&IDENT_TAIL)) +// +// Numbers +// + +// The "-" of a "-42" +%fragment SIGN [-+] + +// The "F" of a "0xFF" +%fragment DIGIT_HEAD [0-9] +%fragment BIN_HEAD [01] +%fragment OCT_HEAD [0-7] +%fragment HEX_HEAD [0-9a-fA-F] + +%fragment DEC_HEAD [1-9] + +%fragment DIGIT_TAIL _?(?&DIGIT_HEAD) +%fragment DEC_TAIL _?(?&DIGIT_HEAD) +%fragment BIN_TAIL _?(?&BIN_HEAD) +%fragment OCT_TAIL _?(?&OCT_HEAD) +%fragment HEX_TAIL _?(?&HEX_HEAD) + +// The "FE_DE" of a "0xFE_DE" +%fragment DIGITS (?&DIGIT_HEAD)(?&DIGIT_TAIL)*+ +%fragment DEC_DIGITS (?&DEC_HEAD)(?&DEC_TAIL)*+ +%fragment BIN_DIGITS (?&BIN_HEAD)(?&BIN_TAIL)*+ +%fragment OCT_DIGITS (?&OCT_HEAD)(?&OCT_TAIL)*+ +%fragment HEX_DIGITS (?&HEX_HEAD)(?&HEX_TAIL)*+ + +// A "42.0", a "42." or a ".42" +%fragment FRACTION (?&DIGITS)\.(?:(?&DIGITS))?|\.(?&DIGITS) + +// An "e2", an "E-2" or an "e+2" +%fragment EXPONENT [eE](?&SIGN)?(?&DIGITS) + /** * ----------------------------------------------------------------------------- * Lexeme/Token Definitions * ----------------------------------------------------------------------------- - * - * The lexer takes the first pattern that matches, so the order below is both - * a matter of meaning (a longer token above the shorter one it starts with) - * and a matter of speed (a token that occurs more often above one that does - * not). - * */ -// Whitespace is the most frequent thing there is in a type statement %skip T_WHITESPACE \s++ -// Keywords, above the identifier they would otherwise be read as +// An "is not" or an "IS NOT" %token T_NEQ (?i:is\h++not)(?&KEYWORD_END) +// An "is" or an "IS" %token T_EQ (?i:is)(?&KEYWORD_END) -%token T_BOOL_LITERAL (?i:true|false)(?&KEYWORD_END) +// A "true" or a "TRUE" +%token T_TRUE_LITERAL (?i:true)(?&KEYWORD_END) +// A "false" or a "FALSE" +%token T_FALSE_LITERAL (?i:false)(?&KEYWORD_END) +// A "null" or a "NULL" %token T_NULL_LITERAL (?i:null)(?&KEYWORD_END) // Identifiers + %token T_NAME_WITH_SPACE (?&IDENT_HEAD)(?&IDENT_TAIL)*+\s++ +// An "int", a "non-empty-string" or a "_42" %token T_NAME (?&IDENT_HEAD)(?&IDENT_TAIL)*+ -// Punctuation, ordered by how often a type statement contains it %token T_NS_DELIMITER \\ %token T_OR \| %token T_COMMA , %token T_DOUBLE_COLON :: %token T_COLON : -%token T_LTE <= -%token T_GTE >= %token T_ANGLE_BRACKET_OPEN < %token T_ANGLE_BRACKET_CLOSE > %token T_QMARK \? @@ -66,20 +93,31 @@ %token T_ASTERISK \* // Variables + +// A "$this" %token T_THIS \$this\b +// A "$value" or a "$_" %token T_VARIABLE \$(?&IDENT_HEAD)(?&IDENT_TAIL)*+ // Literals + +// A double quoted "text" %token T_DQ_STRING_LITERAL "[^"\\]*+(?:\\.[^"\\]*+)*+" %token T_SQ_STRING_LITERAL '[^'\\]*+(?:\\.[^'\\]*+)*+' -%token T_PFX_FLOAT_LITERAL -?(?&DIGIT)++\.(?&DIGIT)*+(?:(?&EXPONENT))? -%token T_SFX_FLOAT_LITERAL -?(?&DIGIT)*+\.(?&DIGIT)++(?:(?&EXPONENT))? -%token T_EXP_LITERAL -?(?&DIGIT)++(?&EXPONENT) -%token T_BIN_INT_LITERAL -?0[bB][01_]++ -%token T_OCT_INT_LITERAL -?0[oO][0-7_]++ -%token T_HEX_INT_LITERAL -?0[xX][0-9a-fA-F_]++ -%token T_DEC_INT_LITERAL -?(?&DIGIT)[0-9_]*+ +// A "42.0", a "42.", a ".42", a "42e2", a "-1.5e+3" or a "2_3.4_5e-6_7" +%token T_FLOAT_LITERAL (?&SIGN)?(?:(?&FRACTION)(?:(?&EXPONENT))?|(?&DIGITS)(?&EXPONENT)) +// A "0b1010", a "0B0001_1000" or a "-0b10" +%token T_BIN_INT_LITERAL (?&SIGN)?0[bB](?&BIN_DIGITS) +// A "0o42", a "0O42", a "042" or an "04_23" +%token T_OCT_INT_LITERAL (?&SIGN)?0(?:[oO](?&OCT_DIGITS)|(?&OCT_TAIL)++) +// A "0xDEAD", a "0XDeaD" or a "0xFE_DE" +%token T_HEX_INT_LITERAL (?&SIGN)?0[xX](?&HEX_DIGITS) +// A "42", a "+42_04" or a "0" +%token T_DEC_INT_LITERAL (?&SIGN)?(?:(?&DEC_DIGITS)|0) // Comments + +// A "// text" or a "# text" %skip T_COMMENT (?://|#)[^\r\n]*+ +// A "/* text */" %skip T_DOC_COMMENT /\*.*?\*/ diff --git a/libs/parser/resources/grammar/literals.pp3 b/libs/parser/resources/grammar/literals.pp3 index 968a4c4..2135313 100644 --- a/libs/parser/resources/grammar/literals.pp3 +++ b/libs/parser/resources/grammar/literals.pp3 @@ -1,57 +1,66 @@ -Literal -> { +LiteralType -> { if ($this->features->literals === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); } return $children; } - : StringLiteral() - | FloatLiteral() - | IntLiteral() - | BoolLiteral() - | NullLiteral() + : ScalarType() + | NullLiteralType() ; -VariableLiteral -> { - return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); -} - : - | - ; - -ThisLiteral -> { - return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); -} - : +ScalarType + : StringLiteralType() + | FloatLiteralType() + | IntLiteralType() + | TrueLiteralType() + | FalseLiteralType() ; -StringLiteral - : DoubleQuotedStringLiteral() - | SingleQuotedStringLiteral() +StringLiteralType + : DoubleQuotedStringLiteralType() + | SingleQuotedStringLiteralType() ; -DoubleQuotedStringLiteral -> { - return $this->createDoubleQuotedString($children->value, $offset); +DoubleQuotedStringLiteralType -> { + return new Type\Literal\StringLiteralNode( + Internal\StringDecoder::unpackAndDecode($children->value, true), + $children->value, + $offset, + ); } : ; -SingleQuotedStringLiteral -> { - return $this->createSingleQuotedString($children->value, $offset); +SingleQuotedStringLiteralType -> { + return new Type\Literal\StringLiteralNode( + Internal\StringDecoder::unpackAndDecode($children->value, false), + $children->value, + $offset, + ); } : ; -FloatLiteral -> { - return new Type\Literal\FloatLiteralNode((float) $children->value, $children->value, $offset); +FloatLiteralType -> { + return new Type\Literal\FloatLiteralNode( + Internal\FloatDecoder::decode($children->value), + $children->value, + $offset, + ); } - : - | - | + : ; -IntLiteral -> { - return $this->createInt($children->value, $offset); +IntLiteralType -> { + $decoded = Internal\IntDecoder::decode($children->value); + + return new Type\Literal\IntLiteralNode( + $decoded->value, + $children->value, + $decoded->decimal, + $offset, + ); } : | @@ -59,14 +68,23 @@ IntLiteral -> { | ; -BoolLiteral -> { - return $this->createBool($children->value, $offset); +TrueLiteralType -> { + return new Type\Literal\BoolLiteralNode(true, $children->value, $offset); +} + : + ; + +FalseLiteralType -> { + return new Type\Literal\BoolLiteralNode(false, $children->value, $offset); } - : + : ; -NullLiteral -> { - return new Type\Literal\NullLiteralNode($children->value, $offset); +NullLiteralType -> { + return new Type\Literal\NullLiteralNode( + $children->value, + $offset, + ); } : ; diff --git a/libs/parser/resources/grammar/named-type.pp3 b/libs/parser/resources/grammar/named-type.pp3 index 8a483f7..c835f69 100644 --- a/libs/parser/resources/grammar/named-type.pp3 +++ b/libs/parser/resources/grammar/named-type.pp3 @@ -3,20 +3,25 @@ * ----------------------------------------------------------------------------- * Named Types * ----------------------------------------------------------------------------- - * - * Everything a name may open is read by a single rule, so that the name is - * recognized once instead of once per alternative that begins with it. - * - * Each suffix begins with a token of its own ("*", "::", "(", "<" and "{"), - * so which one to read is decided by the token the reading is at rather than - * by trying them in turn. - * */ NamedType -> { + $head = $children[0]; $suffix = $children[1] ?? null; + if ($head instanceof Type\ConstMaskNode) { + if ($suffix !== null) { + throw Exception\ConstMaskException::becauseNothingFollowsAMask($offset); + } + + return $head; + } + switch (true) { + // Some\Any + case $suffix === null: + return new Type\NamedTypeNode($children[0], null, null, $offset); + // Some\Any case $suffix instanceof Type\Template\TemplateArgumentListNode: return new Type\NamedTypeNode($children[0], $suffix, null, $offset); @@ -31,58 +36,94 @@ NamedType -> { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); } - return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, $offset); + return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); - // Some\Any::CONST and Some\Any::CONST_* - case $suffix instanceof Type\Identifier: - if (isset($children[2])) { - return new Type\ClassConstMaskNode($children[0], $suffix, $offset); + // Some\Any(T): U + case $suffix instanceof Type\Template\TemplateParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); } - return new Type\ClassConstNode($children[0], $suffix, $offset); + return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); - // Some\Any::* - case $suffix === true: - return new Type\ClassConstMaskNode($children[0], offset: $offset); - - // SOME_ANY_* - case $suffix === false: - return new Type\ConstMaskNode($children[0], $offset); - - // Some\Any + // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* default: - return new Type\NamedTypeNode($children[0], null, null, $offset); + if (\count($children) === 3 && $children[2] instanceof Type\Identifier) { + return new Type\ClassConstNode($children[0], $children[2], $offset); + } + + return new Type\ClassConstMaskNode( + $children[0], + new Type\MaskNode(\array_slice($children, 2), $children[2]->offset), + $offset, + ); } } - : Name() NamedTypeSuffix()? + : NameOrMask() NamedTypeSuffix()? ; NamedTypeSuffix - : ConstMaskSuffix() - | ClassConstSuffix() + : ClassConstSuffix() | CallableSuffix() - | TemplateArguments() + | TemplateArguments() !::T_PARENTHESIS_OPEN:: + | TemplateParameters() CallableSuffix() | ShapeFields() ; -// The "*" of a constant mask, like "SOME_*" -ConstMaskSuffix -> { - return false; +NameOrMask -> { + $head = $children[0]; + + // "*_SOME" + if (!$head instanceof Type\Name) { + return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); + } + + // "Some\Any" + if (!isset($children[1])) { + return $head; + } + + $parts = $head->parts; + + if ($children[1] instanceof Type\MaskNode) { + // "Some\Any\*" + $mask = $children[1]; + } else { + // "Some\Any\SOME_*" + $segment = \array_pop($parts); + $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); + } + + return new Type\ConstMaskNode( + $mask, + $parts === [] + ? $head->isFullyQualified + : new Type\Name($parts, $head->isFullyQualified, $head->offset), + $offset, + ); } - : + : Name() ( MaskTail() | NamespacedMask() )? + | LeadingMask() ; -// The "::CONST", "::CONST_*" and "::*" of a class constant -ClassConstSuffix - : ::T_DOUBLE_COLON:: - ( Identifier() ClassConstMaskSuffix()? - | ClassConstMaskSuffix() - ) +LeadingMask + : WildcardType() Identifier() MaskTail()? + ; + +MaskTail + : WildcardType() ( Identifier() WildcardType() )* Identifier()? ; -// The "*" of a class constant mask, like "Some::CONST_*" or "Some::*" -ClassConstMaskSuffix -> { - return true; +NamespacedMask -> { + return new Type\MaskNode($children, $offset); } - : + : ::T_NS_DELIMITER:: MaskTail() + ; + +// The "::CONST", "::CONST_*", "::*" and "::A*B" of a class constant +ClassConstSuffix + : + ( Identifier() MaskTail()? + | MaskTail() + ) ; diff --git a/libs/parser/resources/grammar/shape-fields.pp3 b/libs/parser/resources/grammar/shape-fields.pp3 index 396335b..5992230 100644 --- a/libs/parser/resources/grammar/shape-fields.pp3 +++ b/libs/parser/resources/grammar/shape-fields.pp3 @@ -2,9 +2,8 @@ ShapeFields -> { $count = \count($children); - // An empty shape carries no fields and needs no support for them if ($count === 0) { - return [new Type\Shape\FieldsListNode(offset: $offset)]; + return [new Type\Shape\FieldsListNode([], true, $offset)]; } if ($this->features->shapes === false) { @@ -13,7 +12,7 @@ ShapeFields -> { $parameters = null; - // The "..." suffix, in case of the shape is unsealed by a typed rest + // The "..." suffix if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) { $parameters = $children[$count - 1]; --$count; @@ -21,10 +20,9 @@ ShapeFields -> { if ($children[0] instanceof Type\Shape\FieldsListNode) { $fields = $children[0]; - // Whatever is left beside the fields is the "..." of an unsealed shape $unsealed = $count > 1; } else { - $fields = new Type\Shape\FieldsListNode(offset: $offset); + $fields = new Type\Shape\FieldsListNode([], true, $offset); $unsealed = $count > 0; } @@ -52,28 +50,37 @@ ShapeFieldsUnsealed ; ShapeFieldsList -> { - $explicit = []; + $explicit = false; $implicit = false; + $indexed = []; foreach ($children as $field) { - if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->getIndex(); + if (!$field instanceof Type\Shape\ExplicitFieldNode) { + $implicit = true; - if (isset($explicit[$key])) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); - } + continue; + } - $explicit[$key] = true; - } else { - $implicit = true; + $explicit = true; + + if (!$field instanceof Type\Shape\SimpleFieldNodeInterface) { + continue; + } + + $key = $field->getIndex(); + + if (isset($indexed[$key])) { + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); } + + $indexed[$key] = true; } - if ($explicit !== [] && $implicit) { + if ($explicit && $implicit) { throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); } - return new Type\Shape\FieldsListNode($children, offset: $offset); + return new Type\Shape\FieldsListNode($children, true, $offset); } : ShapeField() ( ::T_COMMA:: ShapeField() @@ -90,45 +97,35 @@ ExplicitField -> { $name = $children[0]; $value = $children[$count - 1]; - // In case of "nullable" suffix defined $optional = $count === 3; return match (true) { - $name instanceof Type\Literal\IntLiteralNode - => new Type\Shape\NumericFieldNode($name, $value, $optional, offset: $offset), + // "a", "true" and "null" + $name instanceof Type\Identifier + => new Type\Shape\NamedFieldNode($name, $value, $optional, $offset), + $name instanceof Type\Literal\IntLiteralNode, $name instanceof Type\Literal\StringLiteralNode - => new Type\Shape\StringNamedFieldNode($name, $value, $optional, offset: $offset), - $name instanceof Type\ClassConstNode - => new Type\Shape\ClassConstFieldNode($name, $value, $optional, offset: $offset), - $name instanceof Type\ClassConstMaskNode - => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional, offset: $offset), + => new Type\Shape\ScalarFieldNode($name, $value, $optional, $offset), + // "Some::CONST", "Some::CONST_*" and "JSON_*" + $name instanceof Type\ClassConstNode, + $name instanceof Type\ClassConstMaskNode, $name instanceof Type\ConstMaskNode - => new Type\Shape\ConstMaskFieldNode($name, $value, $optional, offset: $offset), - default => new Type\Shape\NamedFieldNode($name, $value, $optional, offset: $offset), + => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset), + default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($name->offset), }; } : ShapeKey() ()? ::T_COLON:: ShapeValue() ; ImplicitField -> { - return new Type\Shape\ImplicitFieldNode($children[0], offset: $offset); + return new Type\Shape\ImplicitFieldNode($children[0], false, $offset); } : ShapeValue() ; -/** - * A key written as a bare name is by far the most common one, and telling it - * apart from a constant reference costs a whole name to read. - * - * The predicate answers the same question by the token that follows the name - * instead: a key ends where its value begins, while "SOME_*" and "Some::CONST" - * go on. - */ ShapeKey : Identifier() &ShapeKeyEnd() - | ShapeConstantKey() - | IntLiteral() - | StringLiteral() + | PrimaryType() ; // The "?:" and ":" a shape key is followed by @@ -136,34 +133,6 @@ ShapeKeyEnd : ::T_QMARK::? ::T_COLON:: ; -/** - * A constant reference used as a shape key: the name is read once for every - * form it may take rather than once per form. - */ -ShapeConstantKey -> { - $suffix = $children[1]; - - switch (true) { - // Some\Any::CONST and Some\Any::CONST_* - case $suffix instanceof Type\Identifier: - if (isset($children[2])) { - return new Type\ClassConstMaskNode($children[0], $suffix, $offset); - } - - return new Type\ClassConstNode($children[0], $suffix, $offset); - - // Some\Any::* - case $suffix === true: - return new Type\ClassConstMaskNode($children[0], offset: $offset); - - // SOME_ANY_* - default: - return new Type\ConstMaskNode($children[0], $offset); - } -} - : Name() ( ConstMaskSuffix() | ClassConstSuffix() ) - ; - ShapeValue : Type() ; diff --git a/libs/parser/resources/grammar/template-arguments.pp3 b/libs/parser/resources/grammar/template-arguments.pp3 index 607eeba..fdcdc1d 100644 --- a/libs/parser/resources/grammar/template-arguments.pp3 +++ b/libs/parser/resources/grammar/template-arguments.pp3 @@ -1,3 +1,8 @@ +/** + * ----------------------------------------------------------------------------- + * Template Arguments + * ----------------------------------------------------------------------------- + */ TemplateArguments -> { if ($this->features->generics === false) { @@ -14,32 +19,25 @@ TemplateArguments -> { ; TemplateArgument -> { - $count = \count($children); - $hint = null; - - // An "in T" or an "out T" argument puts a hint in front of the type - if ($count === 2) { + if (\is_array($children)) { if ($this->features->hints === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); } - $hint = $children[0]; + return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset); } - return new Type\Template\TemplateArgumentNode( - $children[$count - 1], - $hint, - $offset, - ); + return new Type\Template\TemplateArgumentNode($children, null, $offset); } : TemplateHintedArgument() | TemplateSimpleArgument() ; TemplateSimpleArgument - : Type() + : WildcardType() + | Type() ; TemplateHintedArgument - : IdentifierWithExtraSpace() Type() + : IdentifierWithExtraSpace() TemplateSimpleArgument() ; diff --git a/libs/parser/resources/grammar/template-parameters.pp3 b/libs/parser/resources/grammar/template-parameters.pp3 new file mode 100644 index 0000000..47f5776 --- /dev/null +++ b/libs/parser/resources/grammar/template-parameters.pp3 @@ -0,0 +1,78 @@ +/** + * ----------------------------------------------------------------------------- + * Template Parameters + * ----------------------------------------------------------------------------- + */ + +TemplateParameters -> { + if ($this->features->generics === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $offset); + } + + return new Type\Template\TemplateParameterListNode($children, $offset); +} + : ::T_ANGLE_BRACKET_OPEN:: + TemplateParameter() ( + ::T_COMMA:: TemplateParameter() + )* ::T_COMMA::? + ::T_ANGLE_BRACKET_CLOSE:: + ; + +TemplateParameter -> { + $upper = $lower = $default = null; + + foreach (\array_slice($children, 1) as $limit) { + if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) { + if ($default !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $limit->offset); + } + + $default = $limit; + + continue; + } + + if ($default !== null) { + throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($limit->operator->offset); + } + + switch ($limit->operator->toLowerString()) { + case 'of': + case 'as': + if ($upper !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $limit->offset); + } + + $upper = $limit; + break; + + case 'super': + if ($lower !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $limit->offset); + } + + $lower = $limit; + break; + + default: + throw Exception\TemplateBoundException::becauseOperatorIsUnknown( + $limit->operator->value, + $limit->operator->offset, + ); + } + } + + return new Type\Template\TemplateParameterNode($children[0], $upper, $lower, $default, $offset); +} + : Identifier() ( TemplateBoundEdge() | TemplateDefault() )* + ; + +TemplateBoundEdge -> { + return new Type\Template\TemplateBoundEdgeNode($children[0], $children[1], $offset); +} + : Identifier() Type() + ; + +TemplateDefault + : ::T_ASSIGN:: Type() + ; diff --git a/libs/parser/resources/grammar/ternary.pp3 b/libs/parser/resources/grammar/ternary.pp3 deleted file mode 100644 index c7e6f4c..0000000 --- a/libs/parser/resources/grammar/ternary.pp3 +++ /dev/null @@ -1,14 +0,0 @@ -TernaryExpressionSuffix - : TernaryExpressionOperator() (Type() | VariableLiteral()) - ::T_QMARK:: Type() - ::T_COLON:: Type() - ; - -TernaryExpressionOperator - : - | - | - | - | - | - ; diff --git a/libs/parser/src/Exception/ConstMaskException.php b/libs/parser/src/Exception/ConstMaskException.php new file mode 100644 index 0000000..02f6ac7 --- /dev/null +++ b/libs/parser/src/Exception/ConstMaskException.php @@ -0,0 +1,22 @@ + $offset + */ + public static function becauseNothingFollowsAMask(int $offset = 0): self + { + $message = 'Constant mask is a type entire and cannot be followed by anything'; + + return new self($offset, $message, self::ERROR_CODE_CONST_MASK); + } +} diff --git a/libs/parser/src/Exception/SemanticException.php b/libs/parser/src/Exception/SemanticException.php index b40aec6..950bd22 100644 --- a/libs/parser/src/Exception/SemanticException.php +++ b/libs/parser/src/Exception/SemanticException.php @@ -18,7 +18,13 @@ abstract class SemanticException extends \LogicException implements ParserExcept final public const ERROR_CODE_INVALID_OPERATOR = 0x05; - protected const CODE_LAST = self::ERROR_CODE_INVALID_OPERATOR; + final public const ERROR_CODE_TEMPLATE_BOUND = 0x06; + + final public const ERROR_CODE_SHAPE_KEY = 0x07; + + final public const ERROR_CODE_CONST_MASK = 0x08; + + protected const CODE_LAST = self::ERROR_CODE_CONST_MASK; /** * @param int<0, max> $offset diff --git a/libs/parser/src/Exception/ShapeKeyException.php b/libs/parser/src/Exception/ShapeKeyException.php new file mode 100644 index 0000000..30d9a1f --- /dev/null +++ b/libs/parser/src/Exception/ShapeKeyException.php @@ -0,0 +1,22 @@ + $offset + */ + public static function becauseKeyIsNotAName(int $offset = 0): self + { + $message = 'Shape key must be a name, a number, a string or a reference to a constant'; + + return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY); + } +} diff --git a/libs/parser/src/Exception/TemplateBoundException.php b/libs/parser/src/Exception/TemplateBoundException.php new file mode 100644 index 0000000..94286b2 --- /dev/null +++ b/libs/parser/src/Exception/TemplateBoundException.php @@ -0,0 +1,53 @@ + $offset + */ + public static function becauseOperatorIsUnknown(string $operator, int $offset = 0): self + { + $message = \sprintf( + 'Template parameter cannot be bounded with "%s", expected one of "of", "as" or "super"', + $operator, + ); + + return new self($offset, $message, self::ERROR_CODE_TEMPLATE_BOUND); + } + + /** + * Occurs when a template parameter is written with the same kind of + * limit more than once. + * + * @param non-empty-string $kind + * @param int<0, max> $offset + */ + public static function becauseBoundIsDuplicated(string $kind, int $offset = 0): self + { + $message = \sprintf('Template parameter cannot have more than one %s', $kind); + + return new self($offset, $message, self::ERROR_CODE_TEMPLATE_BOUND); + } + + /** + * Occurs when a bound is written behind a default, where it reads as + * a bound of the default itself. + * + * @param int<0, max> $offset + */ + public static function becauseDefaultIsNotWrittenLast(int $offset = 0): self + { + $message = 'Template parameter default must be written last, since a bound ' + . 'behind it reads as a bound of the default itself'; + + return new self($offset, $message, self::ERROR_CODE_TEMPLATE_BOUND); + } +} diff --git a/libs/parser/src/Internal/CompilerExecutor.php b/libs/parser/src/Internal/CompilerExecutor.php index 9c5c369..7d57946 100644 --- a/libs/parser/src/Internal/CompilerExecutor.php +++ b/libs/parser/src/Internal/CompilerExecutor.php @@ -8,8 +8,9 @@ namespace TypeLang\Parser\Internal; -use TypeLang\Parser\Exception; use TypeLang\Type; +use TypeLang\Parser\Exception; +use TypeLang\Parser\Internal; \interface_exists(\Phplrt\Contracts\Source\Exception\SourceExceptionInterface::class); \interface_exists(\Phplrt\Contracts\Source\ReadableInterface::class); @@ -39,79 +40,73 @@ abstract class CompilerExecutor implements \Phplrt\Contracts\Parser\ParserInterf /** @var int */ public const T_EQ = 2; /** @var int */ - public const T_BOOL_LITERAL = 3; - /** @var int */ - public const T_NULL_LITERAL = 4; - /** @var int */ - public const T_NAME_WITH_SPACE = 5; + public const T_TRUE_LITERAL = 3; /** @var int */ - public const T_NAME = 6; + public const T_FALSE_LITERAL = 4; /** @var int */ - public const T_NS_DELIMITER = 7; + public const T_NULL_LITERAL = 5; /** @var int */ - public const T_OR = 8; + public const T_NAME_WITH_SPACE = 6; /** @var int */ - public const T_COMMA = 9; + public const T_NAME = 7; /** @var int */ - public const T_DOUBLE_COLON = 10; + public const T_NS_DELIMITER = 8; /** @var int */ - public const T_COLON = 11; + public const T_OR = 9; /** @var int */ - public const T_LTE = 12; + public const T_COMMA = 10; /** @var int */ - public const T_GTE = 13; + public const T_DOUBLE_COLON = 11; /** @var int */ - public const T_ANGLE_BRACKET_OPEN = 14; + public const T_COLON = 12; /** @var int */ - public const T_ANGLE_BRACKET_CLOSE = 15; + public const T_ANGLE_BRACKET_OPEN = 13; /** @var int */ - public const T_QMARK = 16; + public const T_ANGLE_BRACKET_CLOSE = 14; /** @var int */ - public const T_BRACE_OPEN = 17; + public const T_QMARK = 15; /** @var int */ - public const T_BRACE_CLOSE = 18; + public const T_BRACE_OPEN = 16; /** @var int */ - public const T_PARENTHESIS_OPEN = 19; + public const T_BRACE_CLOSE = 17; /** @var int */ - public const T_PARENTHESIS_CLOSE = 20; + public const T_PARENTHESIS_OPEN = 18; /** @var int */ - public const T_SQUARE_BRACKET_OPEN = 21; + public const T_PARENTHESIS_CLOSE = 19; /** @var int */ - public const T_SQUARE_BRACKET_CLOSE = 22; + public const T_SQUARE_BRACKET_OPEN = 20; /** @var int */ - public const T_AMP = 23; + public const T_SQUARE_BRACKET_CLOSE = 21; /** @var int */ - public const T_ELLIPSIS = 24; + public const T_AMP = 22; /** @var int */ - public const T_ASSIGN = 25; + public const T_ELLIPSIS = 23; /** @var int */ - public const T_ASTERISK = 26; + public const T_ASSIGN = 24; /** @var int */ - public const T_THIS = 27; + public const T_ASTERISK = 25; /** @var int */ - public const T_VARIABLE = 28; + public const T_THIS = 26; /** @var int */ - public const T_DQ_STRING_LITERAL = 29; + public const T_VARIABLE = 27; /** @var int */ - public const T_SQ_STRING_LITERAL = 30; + public const T_DQ_STRING_LITERAL = 28; /** @var int */ - public const T_PFX_FLOAT_LITERAL = 31; + public const T_SQ_STRING_LITERAL = 29; /** @var int */ - public const T_SFX_FLOAT_LITERAL = 32; + public const T_FLOAT_LITERAL = 30; /** @var int */ - public const T_EXP_LITERAL = 33; + public const T_BIN_INT_LITERAL = 31; /** @var int */ - public const T_BIN_INT_LITERAL = 34; + public const T_OCT_INT_LITERAL = 32; /** @var int */ - public const T_OCT_INT_LITERAL = 35; + public const T_HEX_INT_LITERAL = 33; /** @var int */ - public const T_HEX_INT_LITERAL = 36; + public const T_DEC_INT_LITERAL = 34; /** @var int */ - public const T_DEC_INT_LITERAL = 37; + public const T_COMMENT = 35; /** @var int */ - public const T_COMMENT = 38; - /** @var int */ - public const T_DOC_COMMENT = 39; + public const T_DOC_COMMENT = 36; /** * @var \Phplrt\Contracts\Parser\ParserInterface @@ -123,10 +118,10 @@ abstract class CompilerExecutor implements \Phplrt\Contracts\Parser\ParserInterf public function __construct() { $this->lexer = new \Phplrt\Lexer\Lexer( - pattern: '/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:(?i:is\\h++not)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:1))|(?:(?:(?i:is)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:2))|(?:(?:(?i:true|false)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:3))|(?:(?:(?i:null)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:4))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+\\s++)(*MARK:5))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:6))|(?:(?:\\\\)(*MARK:7))|(?:(?:\\|)(*MARK:8))|(?:(?:,)(*MARK:9))|(?:(?:::)(*MARK:10))|(?:(?::)(*MARK:11))|(?:(?:<=)(*MARK:12))|(?:(?:>=)(*MARK:13))|(?:(?:<)(*MARK:14))|(?:(?:>)(*MARK:15))|(?:(?:\\?)(*MARK:16))|(?:(?:\\{)(*MARK:17))|(?:(?:\\})(*MARK:18))|(?:(?:\\()(*MARK:19))|(?:(?:\\))(*MARK:20))|(?:(?:\\[)(*MARK:21))|(?:(?:\\])(*MARK:22))|(?:(?:&)(*MARK:23))|(?:(?:\\.\\.\\.)(*MARK:24))|(?:(?:=)(*MARK:25))|(?:(?:\\*)(*MARK:26))|(?:(?:\\$this\\b)(*MARK:27))|(?:(?:\\$(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:28))|(?:(?:"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+")(*MARK:29))|(?:(?:\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\')(*MARK:30))|(?:(?:-?(?:[0-9])++\\.(?:[0-9])*+(?:(?:[eE]-?(?:[0-9])++))?)(*MARK:31))|(?:(?:-?(?:[0-9])*+\\.(?:[0-9])++(?:(?:[eE]-?(?:[0-9])++))?)(*MARK:32))|(?:(?:-?(?:[0-9])++(?:[eE]-?(?:[0-9])++))(*MARK:33))|(?:(?:-?0[bB][01_]++)(*MARK:34))|(?:(?:-?0[oO][0-7_]++)(*MARK:35))|(?:(?:-?0[xX][0-9a-fA-F_]++)(*MARK:36))|(?:(?:-?(?:[0-9])[0-9_]*+)(*MARK:37))|(?:(?:(?:\\/\\/|\\#)[^\\r\\n]*+)(*MARK:38))|(?:(?:\\/\\*.*?\\*\\/)(*MARK:39))|(?:(?:[^\\s]++)(*MARK:40)))/Ssum', + pattern: '/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:(?i:is\\h++not)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:1))|(?:(?:(?i:is)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:2))|(?:(?:(?i:true)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:3))|(?:(?:(?i:false)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:4))|(?:(?:(?i:null)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:5))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+\\s++)(*MARK:6))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:7))|(?:(?:\\\\)(*MARK:8))|(?:(?:\\|)(*MARK:9))|(?:(?:,)(*MARK:10))|(?:(?:::)(*MARK:11))|(?:(?::)(*MARK:12))|(?:(?:<)(*MARK:13))|(?:(?:>)(*MARK:14))|(?:(?:\\?)(*MARK:15))|(?:(?:\\{)(*MARK:16))|(?:(?:\\})(*MARK:17))|(?:(?:\\()(*MARK:18))|(?:(?:\\))(*MARK:19))|(?:(?:\\[)(*MARK:20))|(?:(?:\\])(*MARK:21))|(?:(?:&)(*MARK:22))|(?:(?:\\.\\.\\.)(*MARK:23))|(?:(?:=)(*MARK:24))|(?:(?:\\*)(*MARK:25))|(?:(?:\\$this\\b)(*MARK:26))|(?:(?:\\$(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:27))|(?:(?:"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+")(*MARK:28))|(?:(?:\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\')(*MARK:29))|(?:(?:(?:[-+])?(?:(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+)\\.(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+))?|\\.(?:(?:[0-9])(?:_?(?:[0-9]))*+))(?:(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+)))?|(?:(?:[0-9])(?:_?(?:[0-9]))*+)(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+))))(*MARK:30))|(?:(?:(?:[-+])?0[bB](?:(?:[01])(?:_?(?:[01]))*+))(*MARK:31))|(?:(?:(?:[-+])?0(?:[oO](?:(?:[0-7])(?:_?(?:[0-7]))*+)|(?:_?(?:[0-7]))++))(*MARK:32))|(?:(?:(?:[-+])?0[xX](?:(?:[0-9a-fA-F])(?:_?(?:[0-9a-fA-F]))*+))(*MARK:33))|(?:(?:(?:[-+])?(?:(?:(?:[1-9])(?:_?(?:[0-9]))*+)|0))(*MARK:34))|(?:(?:(?:\\/\\/|\\#)[^\\r\\n]*+)(*MARK:35))|(?:(?:\\/\\*.*?\\*\\/)(*MARK:36))|(?:(?:[^\\s]++)(*MARK:37)))/Ssum', channels: [ 'Hidden', - 38 => 'Hidden', + 35 => 'Hidden', 'Hidden', 'Unknown', ], @@ -134,7 +129,8 @@ public function __construct() 'T_WHITESPACE', 'T_NEQ', 'T_EQ', - 'T_BOOL_LITERAL', + 'T_TRUE_LITERAL', + 'T_FALSE_LITERAL', 'T_NULL_LITERAL', 'T_NAME_WITH_SPACE', 'T_NAME', @@ -143,8 +139,6 @@ public function __construct() 'T_COMMA', 'T_DOUBLE_COLON', 'T_COLON', - 'T_LTE', - 'T_GTE', 'T_ANGLE_BRACKET_OPEN', 'T_ANGLE_BRACKET_CLOSE', 'T_QMARK', @@ -162,9 +156,7 @@ public function __construct() 'T_VARIABLE', 'T_DQ_STRING_LITERAL', 'T_SQ_STRING_LITERAL', - 'T_PFX_FLOAT_LITERAL', - 'T_SFX_FLOAT_LITERAL', - 'T_EXP_LITERAL', + 'T_FLOAT_LITERAL', 'T_BIN_INT_LITERAL', 'T_OCT_INT_LITERAL', 'T_HEX_INT_LITERAL', @@ -177,132 +169,147 @@ public function __construct() $this->parser = new \Phplrt\Parser\Parser( lexer: $this->lexer, grammar: [ - new \Phplrt\Parser\Grammar\Alternation([1, 125]), - new \Phplrt\Parser\Grammar\Concatenation([2, 116]), - new \Phplrt\Parser\Grammar\Concatenation([3, 113]), - new \Phplrt\Parser\Grammar\Concatenation([4, 110]), + new \Phplrt\Parser\Grammar\Alternation([1, 140]), + new \Phplrt\Parser\Grammar\Concatenation([2, 135]), + new \Phplrt\Parser\Grammar\Concatenation([3, 132]), + new \Phplrt\Parser\Grammar\Concatenation([4, 129]), new \Phplrt\Parser\Grammar\Alternation([5, 7]), new \Phplrt\Parser\Grammar\Concatenation([6, 7]), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false), - new \Phplrt\Parser\Grammar\Concatenation([8, 105]), - new \Phplrt\Parser\Grammar\Alternation([9, 12, 13, 28]), + new \Phplrt\Parser\Grammar\Concatenation([8, 124]), + new \Phplrt\Parser\Grammar\Alternation([9, 12, 13, 27]), new \Phplrt\Parser\Grammar\Concatenation([10, 0, 11]), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Alternation([14, 17, 21, 26, 27]), - new \Phplrt\Parser\Grammar\Alternation([15, 16]), + new \Phplrt\Parser\Grammar\Alternation([14, 26]), + new \Phplrt\Parser\Grammar\Alternation([15, 18, 19, 24, 25]), + new \Phplrt\Parser\Grammar\Alternation([16, 17]), new \Phplrt\Parser\Grammar\Lexeme(self::T_DQ_STRING_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQ_STRING_LITERAL, true), - new \Phplrt\Parser\Grammar\Alternation([18, 19, 20]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_PFX_FLOAT_LITERAL, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_SFX_FLOAT_LITERAL, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_EXP_LITERAL, true), - new \Phplrt\Parser\Grammar\Alternation([22, 23, 24, 25]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_FLOAT_LITERAL, true), + new \Phplrt\Parser\Grammar\Alternation([20, 21, 22, 23]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BIN_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_OCT_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_HEX_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_DEC_INT_LITERAL, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_BOOL_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Concatenation([29, 41]), - new \Phplrt\Parser\Grammar\Alternation([30, 40]), - new \Phplrt\Parser\Grammar\Concatenation([31, 32, 38]), + new \Phplrt\Parser\Grammar\Concatenation([28, 53]), + new \Phplrt\Parser\Grammar\Alternation([29, 51]), + new \Phplrt\Parser\Grammar\Concatenation([30, 43]), + new \Phplrt\Parser\Grammar\Alternation([31, 42]), + new \Phplrt\Parser\Grammar\Concatenation([32, 33, 40]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), - new \Phplrt\Parser\Grammar\Alternation([33, 34, 35, 36, 37]), + new \Phplrt\Parser\Grammar\Alternation([34, 35, 36, 37, 38, 39]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_BOOL_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Repetition(39, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([31, 32]), - new \Phplrt\Parser\Grammar\Concatenation([32, 38]), - new \Phplrt\Parser\Grammar\Optional(42), - new \Phplrt\Parser\Grammar\Alternation([43, 44, 50, 71, 80]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), - new \Phplrt\Parser\Grammar\Concatenation([45, 46]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, false), - new \Phplrt\Parser\Grammar\Alternation([47, 49]), - new \Phplrt\Parser\Grammar\Concatenation([32, 48]), - new \Phplrt\Parser\Grammar\Optional(49), + new \Phplrt\Parser\Grammar\Repetition(41, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([32, 33]), + new \Phplrt\Parser\Grammar\Concatenation([33, 40]), + new \Phplrt\Parser\Grammar\Optional(44), + new \Phplrt\Parser\Grammar\Alternation([45, 50]), + new \Phplrt\Parser\Grammar\Concatenation([46, 47, 49]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), - new \Phplrt\Parser\Grammar\Concatenation([10, 51, 11, 68]), - new \Phplrt\Parser\Grammar\Optional(52), - new \Phplrt\Parser\Grammar\Concatenation([53, 64, 67]), - new \Phplrt\Parser\Grammar\Concatenation([0, 54, 56, 58, 62]), - new \Phplrt\Parser\Grammar\Optional(55), + new \Phplrt\Parser\Grammar\Repetition(48, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([33, 46]), + new \Phplrt\Parser\Grammar\Optional(33), + new \Phplrt\Parser\Grammar\Concatenation([32, 45]), + new \Phplrt\Parser\Grammar\Concatenation([46, 33, 52]), + new \Phplrt\Parser\Grammar\Optional(45), + new \Phplrt\Parser\Grammar\Optional(54), + new \Phplrt\Parser\Grammar\Alternation([55, 59, 80, 91, 101]), + new \Phplrt\Parser\Grammar\Concatenation([56, 57]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true), + new \Phplrt\Parser\Grammar\Alternation([58, 45]), + new \Phplrt\Parser\Grammar\Concatenation([33, 52]), + new \Phplrt\Parser\Grammar\Concatenation([10, 60, 11, 77]), + new \Phplrt\Parser\Grammar\Optional(61), + new \Phplrt\Parser\Grammar\Concatenation([62, 73, 76]), + new \Phplrt\Parser\Grammar\Concatenation([0, 63, 65, 67, 71]), + new \Phplrt\Parser\Grammar\Optional(64), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true), - new \Phplrt\Parser\Grammar\Optional(57), + new \Phplrt\Parser\Grammar\Optional(66), new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true), - new \Phplrt\Parser\Grammar\Optional(59), - new \Phplrt\Parser\Grammar\Alternation([60, 61]), + new \Phplrt\Parser\Grammar\Optional(68), + new \Phplrt\Parser\Grammar\Alternation([69, 70]), new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Optional(63), + new \Phplrt\Parser\Grammar\Optional(72), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true), - new \Phplrt\Parser\Grammar\Repetition(65, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([66, 53]), + new \Phplrt\Parser\Grammar\Repetition(74, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([75, 62]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false), - new \Phplrt\Parser\Grammar\Optional(66), - new \Phplrt\Parser\Grammar\Optional(69), - new \Phplrt\Parser\Grammar\Concatenation([70, 0]), + new \Phplrt\Parser\Grammar\Optional(75), + new \Phplrt\Parser\Grammar\Optional(78), + new \Phplrt\Parser\Grammar\Concatenation([79, 0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), - new \Phplrt\Parser\Grammar\Concatenation([72, 73, 77, 67, 79]), + new \Phplrt\Parser\Grammar\Concatenation([81, 90]), + new \Phplrt\Parser\Grammar\Concatenation([82, 83, 87, 76, 89]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([74, 76]), - new \Phplrt\Parser\Grammar\Concatenation([75, 0]), + new \Phplrt\Parser\Grammar\Alternation([84, 86]), + new \Phplrt\Parser\Grammar\Concatenation([85, 86]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), - new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Repetition(78, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([66, 73]), + new \Phplrt\Parser\Grammar\Alternation([46, 0]), + new \Phplrt\Parser\Grammar\Repetition(88, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([75, 83]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Concatenation([81, 82, 67, 104]), + new \Phplrt\Parser\Grammar\Predicate(10, false), + new \Phplrt\Parser\Grammar\Concatenation([92, 59]), + new \Phplrt\Parser\Grammar\Concatenation([82, 93, 99, 76, 89]), + new \Phplrt\Parser\Grammar\Concatenation([33, 94]), + new \Phplrt\Parser\Grammar\Repetition(95, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([96, 97]), + new \Phplrt\Parser\Grammar\Concatenation([33, 0]), + new \Phplrt\Parser\Grammar\Concatenation([98, 0]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, false), + new \Phplrt\Parser\Grammar\Repetition(100, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([75, 93]), + new \Phplrt\Parser\Grammar\Concatenation([102, 103, 76, 123]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([83, 103]), - new \Phplrt\Parser\Grammar\Concatenation([84, 99]), - new \Phplrt\Parser\Grammar\Concatenation([85, 97]), - new \Phplrt\Parser\Grammar\Alternation([86, 96]), - new \Phplrt\Parser\Grammar\Concatenation([87, 94, 70, 0]), - new \Phplrt\Parser\Grammar\Alternation([88, 92, 21, 14]), - new \Phplrt\Parser\Grammar\Concatenation([32, 89]), - new \Phplrt\Parser\Grammar\Predicate(90, true), - new \Phplrt\Parser\Grammar\Concatenation([91, 70]), + new \Phplrt\Parser\Grammar\Alternation([104, 122]), + new \Phplrt\Parser\Grammar\Concatenation([105, 118]), + new \Phplrt\Parser\Grammar\Concatenation([106, 116]), + new \Phplrt\Parser\Grammar\Alternation([107, 115]), + new \Phplrt\Parser\Grammar\Concatenation([108, 113, 79, 0]), + new \Phplrt\Parser\Grammar\Alternation([109, 8]), + new \Phplrt\Parser\Grammar\Concatenation([33, 110]), + new \Phplrt\Parser\Grammar\Predicate(111, true), + new \Phplrt\Parser\Grammar\Concatenation([112, 79]), new \Phplrt\Parser\Grammar\Optional(6), - new \Phplrt\Parser\Grammar\Concatenation([29, 93]), - new \Phplrt\Parser\Grammar\Alternation([43, 44]), - new \Phplrt\Parser\Grammar\Optional(95), + new \Phplrt\Parser\Grammar\Optional(114), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Repetition(98, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([66, 85]), - new \Phplrt\Parser\Grammar\Optional(100), - new \Phplrt\Parser\Grammar\Concatenation([66, 101]), - new \Phplrt\Parser\Grammar\Concatenation([57, 102]), - new \Phplrt\Parser\Grammar\Optional(71), - new \Phplrt\Parser\Grammar\Optional(101), + new \Phplrt\Parser\Grammar\Repetition(117, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([75, 106]), + new \Phplrt\Parser\Grammar\Optional(119), + new \Phplrt\Parser\Grammar\Concatenation([75, 120]), + new \Phplrt\Parser\Grammar\Concatenation([66, 121]), + new \Phplrt\Parser\Grammar\Optional(81), + new \Phplrt\Parser\Grammar\Optional(120), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(106, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([107, 108, 109]), + new \Phplrt\Parser\Grammar\Repetition(125, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([126, 127, 128]), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false), new \Phplrt\Parser\Grammar\Optional(0), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(111, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([112, 4]), + new \Phplrt\Parser\Grammar\Repetition(130, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([131, 4]), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, false), - new \Phplrt\Parser\Grammar\Repetition(114, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([115, 3]), + new \Phplrt\Parser\Grammar\Repetition(133, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([134, 3]), new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, false), - new \Phplrt\Parser\Grammar\Optional(117), - new \Phplrt\Parser\Grammar\Concatenation([118, 124, 6, 0, 70, 0]), - new \Phplrt\Parser\Grammar\Alternation([35, 119, 120, 121, 122, 123]), + new \Phplrt\Parser\Grammar\Optional(136), + new \Phplrt\Parser\Grammar\Concatenation([137, 139, 6, 0, 79, 0]), + new \Phplrt\Parser\Grammar\Alternation([36, 138]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NEQ, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_GTE, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_LTE, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, true), - new \Phplrt\Parser\Grammar\Alternation([0, 59]), - new \Phplrt\Parser\Grammar\Concatenation([59, 117]), + new \Phplrt\Parser\Grammar\Alternation([0, 68]), + new \Phplrt\Parser\Grammar\Concatenation([68, 136]), ], initial: 0, reducers: [ @@ -310,33 +317,37 @@ public function __construct() 2 => $this->reduceUnionType(...), 3 => $this->reduceIntersectionType(...), 5 => self::reduceNullableType(...), - 7 => $this->reduceTypesList(...), - 12 => self::reduceThisLiteral(...), - 13 => $this->reduceLiteral(...), - 15 => $this->reduceDoubleQuotedStringLiteral(...), - 16 => $this->reduceSingleQuotedStringLiteral(...), - 17 => self::reduceFloatLiteral(...), - 21 => $this->reduceIntLiteral(...), - 26 => $this->reduceBoolLiteral(...), - 27 => self::reduceNullLiteral(...), - 28 => $this->reduceNamedType(...), - 30 => self::reduceFullQualifiedName(...), - 32 => self::reduceIdentifier(...), - 40 => self::reduceRelativeName(...), - 43 => self::reduceConstMaskSuffix(...), - 49 => self::reduceClassConstMaskSuffix(...), - 51 => self::reduceCallableParameters(...), - 53 => self::reduceCallableParameter(...), - 59 => self::reduceVariableLiteral(...), - 71 => $this->reduceTemplateArguments(...), - 73 => $this->reduceTemplateArgument(...), - 75 => self::reduceIdentifierWithExtraSpace(...), - 80 => $this->reduceShapeFields(...), - 84 => self::reduceShapeFieldsList(...), - 86 => self::reduceExplicitField(...), - 92 => self::reduceShapeConstantKey(...), - 96 => self::reduceImplicitField(...), - 106 => self::reduceTypeListOrOffsetSuffix(...), + 7 => $this->reduceListOrOffsetType(...), + 12 => self::reduceThisType(...), + 13 => $this->reduceLiteralType(...), + 16 => self::reduceDoubleQuotedStringLiteralType(...), + 17 => self::reduceSingleQuotedStringLiteralType(...), + 18 => self::reduceFloatLiteralType(...), + 19 => self::reduceIntLiteralType(...), + 24 => self::reduceTrueLiteralType(...), + 25 => self::reduceFalseLiteralType(...), + 26 => self::reduceNullLiteralType(...), + 27 => $this->reduceNamedType(...), + 28 => self::reduceNameOrMask(...), + 31 => self::reduceFullQualifiedName(...), + 33 => self::reduceIdentifier(...), + 42 => self::reduceRelativeName(...), + 46 => self::reduceWildcardType(...), + 50 => self::reduceNamespacedMask(...), + 60 => self::reduceCallableParameters(...), + 62 => self::reduceCallableParameter(...), + 68 => self::reduceVariable(...), + 81 => $this->reduceTemplateArguments(...), + 83 => $this->reduceTemplateArgument(...), + 85 => self::reduceIdentifierWithExtraSpace(...), + 92 => $this->reduceTemplateParameters(...), + 93 => self::reduceTemplateParameter(...), + 96 => self::reduceTemplateBoundEdge(...), + 101 => $this->reduceShapeFields(...), + 105 => self::reduceShapeFieldsList(...), + 107 => self::reduceExplicitField(...), + 115 => self::reduceImplicitField(...), + 125 => self::reduceListOrOffsetSuffix(...), ], lookahead: [ [ @@ -346,10 +357,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -367,12 +378,12 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, - 29 => true, true, + 15 => true, + 18 => true, + 25 => true, true, + 28 => true, true, true, true, @@ -387,12 +398,12 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, - 29 => true, true, + 15 => true, + 18 => true, + 25 => true, true, + 28 => true, true, true, true, @@ -407,12 +418,12 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, - 29 => true, true, + 15 => true, + 18 => true, + 25 => true, true, + 28 => true, true, true, true, @@ -427,12 +438,12 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, - 29 => true, true, + 15 => true, + 18 => true, + 25 => true, true, + 28 => true, true, true, true, @@ -441,10 +452,10 @@ public function __construct() true, ], [ - 16 => true, + 15 => true, ], [ - 16 => true, + 15 => true, ], [ 2 => true, @@ -453,11 +464,11 @@ public function __construct() true, true, true, - 19 => true, - 27 => true, - 29 => true, true, + 18 => true, + 25 => true, true, + 28 => true, true, true, true, @@ -472,11 +483,11 @@ public function __construct() true, true, true, - 19 => true, - 27 => true, - 29 => true, true, + 18 => true, + 25 => true, true, + 28 => true, true, true, true, @@ -485,23 +496,33 @@ public function __construct() true, ], [ - 19 => true, + 18 => true, ], [ - 19 => true, + 18 => true, ], [ - 20 => true, + 19 => true, ], [ - 27 => true, + 26 => true, ], [ 3 => true, true, - 29 => true, true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 3 => true, true, + 28 => true, true, true, true, @@ -510,9 +531,12 @@ public function __construct() true, ], [ - 29 => true, + 28 => true, true, ], + [ + 28 => true, + ], [ 29 => true, ], @@ -523,6 +547,7 @@ public function __construct() 31 => true, true, true, + true, ], [ 31 => true, @@ -533,29 +558,37 @@ public function __construct() [ 33 => true, ], - [ - 34 => true, - true, - true, - true, - ], [ 34 => true, ], [ - 35 => true, + 3 => true, ], [ - 36 => true, + 4 => true, ], [ - 37 => true, + 5 => true, ], [ - 3 => true, + 2 => true, + true, + true, + true, + true, + true, + true, + 25 => true, ], [ - 4 => true, + 2 => true, + true, + true, + true, + true, + true, + true, + 25 => true, ], [ 2 => true, @@ -564,6 +597,7 @@ public function __construct() true, true, true, + true, ], [ 2 => true, @@ -572,12 +606,13 @@ public function __construct() true, true, true, + true, ], [ - 7 => true, + 8 => true, ], [ - 7 => true, + 8 => true, ], [ 2 => true, @@ -585,12 +620,13 @@ public function __construct() true, true, true, + true, ], [ - 6 => true, + 7 => true, ], [ - 5 => true, + 6 => true, ], [ 2 => true, @@ -601,9 +637,12 @@ public function __construct() [ 4 => true, ], + [ + 5 => true, + ], null, [ - 7 => true, + 8 => true, ], [ 2 => true, @@ -611,31 +650,48 @@ public function __construct() true, true, true, + true, ], null, [ - 10 => true, - 14 => true, - 17 => true, - 19 => true, - 26 => true, - ], - [ - 26 => true, + 8 => true, + 25 => true, ], [ - 10 => true, + 25 => true, ], [ - 10 => true, + 25 => true, ], + null, [ 2 => true, true, true, true, true, - 26 => true, + true, + ], + null, + [ + 8 => true, + ], + [ + 25 => true, + ], + null, + null, + [ + 11 => true, + 13 => true, + 16 => true, + 18 => true, + ], + [ + 11 => true, + ], + [ + 11 => true, ], [ 2 => true, @@ -643,13 +699,19 @@ public function __construct() true, true, true, + true, + 25 => true, ], - null, [ - 26 => true, + 2 => true, + true, + true, + true, + true, + true, ], [ - 19 => true, + 18 => true, ], null, [ @@ -659,10 +721,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -680,10 +742,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -696,47 +758,50 @@ public function __construct() ], null, [ - 23 => true, + 22 => true, ], null, [ - 24 => true, + 23 => true, ], null, [ - 27 => true, + 26 => true, true, ], [ - 28 => true, + 27 => true, ], [ - 27 => true, + 26 => true, ], null, [ - 25 => true, + 24 => true, ], null, [ - 9 => true, + 10 => true, ], [ - 9 => true, + 10 => true, ], null, null, [ - 11 => true, + 12 => true, ], [ - 11 => true, + 12 => true, ], [ - 14 => true, + 13 => true, ], [ - 14 => true, + 13 => true, + ], + [ + 13 => true, ], [ 2 => true, @@ -745,10 +810,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -760,10 +825,10 @@ public function __construct() true, ], [ - 5 => true, + 6 => true, ], [ - 5 => true, + 6 => true, ], [ 2 => true, @@ -772,10 +837,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -788,18 +853,18 @@ public function __construct() ], null, [ - 9 => true, + 10 => true, ], [ - 15 => true, + 14 => true, ], + null, [ - 17 => true, + 13 => true, ], [ - 17 => true, + 13 => true, ], - null, [ 2 => true, true, @@ -807,20 +872,42 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, - true, + ], + null, + [ + 2 => true, true, true, true, true, true, + 24 => true, + ], + [ + 2 => true, + true, true, true, true, true, ], + [ + 24 => true, + ], + [ + 24 => true, + ], + null, + [ + 10 => true, + ], + [ + 16 => true, + ], + [ + 16 => true, + ], + null, [ 2 => true, true, @@ -828,10 +915,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -849,10 +936,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -870,9 +957,16 @@ public function __construct() true, true, true, - 29 => true, true, - 34 => true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, true, true, true, @@ -884,9 +978,14 @@ public function __construct() true, true, true, - 29 => true, true, - 34 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, true, true, true, @@ -897,13 +996,19 @@ public function __construct() true, true, true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, ], - null, - [ - 11 => true, - 16 => true, - ], - null, [ 2 => true, true, @@ -912,13 +1017,15 @@ public function __construct() true, true, ], + null, [ - 10 => true, - 26 => true, + 12 => true, + 15 => true, ], null, + null, [ - 16 => true, + 15 => true, ], [ 2 => true, @@ -927,10 +1034,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -943,77 +1050,57 @@ public function __construct() ], null, [ - 9 => true, + 10 => true, ], null, [ - 9 => true, + 10 => true, ], [ - 24 => true, + 23 => true, ], null, null, [ - 18 => true, + 17 => true, ], null, [ - 21 => true, + 20 => true, ], [ - 21 => true, + 20 => true, ], null, [ - 22 => true, + 21 => true, ], null, [ - 23 => true, + 22 => true, ], [ - 23 => true, + 22 => true, ], null, [ - 8 => true, + 9 => true, ], [ - 8 => true, + 9 => true, ], null, [ 1 => true, true, - 12 => true, - true, - true, - true, ], [ 1 => true, true, - 12 => true, - true, - true, - true, ], [ 1 => true, ], - [ - 13 => true, - ], - [ - 12 => true, - ], - [ - 14 => true, - ], - [ - 15 => true, - ], [ 2 => true, true, @@ -1021,10 +1108,10 @@ public function __construct() true, true, true, - 16 => true, - 19 => true, - 27 => true, true, + 15 => true, + 18 => true, + 25 => true, true, true, true, @@ -1036,7 +1123,7 @@ public function __construct() true, ], [ - 27 => true, + 26 => true, true, ], ], @@ -1056,12 +1143,10 @@ public function __construct() true, true, false, + false, true, true, true, - false, - false, - false, true, false, false, @@ -1070,6 +1155,9 @@ public function __construct() true, true, true, + true, + true, + true, false, true, false, @@ -1081,16 +1169,24 @@ public function __construct() false, false, false, + false, true, false, false, + false, true, false, false, false, + true, + true, + false, + false, + false, + false, + false, false, false, - true, false, true, true, @@ -1112,24 +1208,24 @@ public function __construct() false, false, false, - true, false, true, + false, true, true, true, false, false, false, - true, false, false, false, true, - false, true, false, false, + true, + false, false, false, false, @@ -1139,7 +1235,7 @@ public function __construct() false, true, false, - false, + true, false, false, false, @@ -1157,6 +1253,12 @@ public function __construct() false, false, false, + true, + false, + false, + false, + false, + false, false, false, false, @@ -1188,19 +1290,19 @@ public function __construct() [ 1, ], - 16 => [ + [ 1, ], - 19 => [ + 15 => [ 1, ], - 29 => [ + 18 => [ 1, ], - [ + 25 => [ 1, ], - [ + 28 => [ 1, ], [ @@ -1221,12 +1323,12 @@ public function __construct() [ 1, ], - 28 => [ - 125, + 27 => [ + 140, ], ], 4 => [ - 16 => [ + 15 => [ 5, ], 2 => [ @@ -1247,19 +1349,19 @@ public function __construct() 7 => [ 7, ], - 19 => [ + 8 => [ 7, ], - 27 => [ + 18 => [ 7, ], - 29 => [ + 25 => [ 7, ], [ 7, ], - [ + 28 => [ 7, ], [ @@ -1282,27 +1384,25 @@ public function __construct() ], ], 8 => [ - 19 => [ + 18 => [ 9, ], - 27 => [ + 26 => [ 12, ], 3 => [ 13, - 28, + 27, ], 4 => [ 13, - 28, - ], - 29 => [ - 13, + 27, ], - [ + 5 => [ 13, + 27, ], - [ + 28 => [ 13, ], [ @@ -1324,319 +1424,414 @@ public function __construct() 13, ], 2 => [ - 28, - ], - 5 => [ - 28, + 27, ], 6 => [ - 28, + 27, ], 7 => [ - 28, + 27, + ], + 8 => [ + 27, + ], + 25 => [ + 27, ], ], 13 => [ - 29 => [ + 3 => [ 14, ], [ 14, ], - [ - 17, + 28 => [ + 14, ], [ - 17, + 14, ], [ - 17, + 14, ], [ - 21, + 14, ], [ - 21, + 14, ], [ - 21, + 14, ], [ - 21, + 14, ], - 3 => [ + 5 => [ 26, ], - 4 => [ - 27, - ], ], [ - 29 => [ + 28 => [ 15, ], [ - 16, + 15, ], - ], - 17 => [ - 31 => [ + [ 18, ], [ 19, ], [ - 20, + 19, + ], + [ + 19, + ], + [ + 19, + ], + 3 => [ + 24, + ], + 4 => [ + 25, + ], + ], + [ + 28 => [ + 16, + ], + [ + 17, ], ], - 21 => [ - 34 => [ + 19 => [ + 31 => [ + 20, + ], + [ + 21, + ], + [ 22, ], [ 23, ], + ], + 28 => [ + 2 => [ + 29, + ], [ - 24, + 29, ], [ - 25, + 29, + ], + [ + 29, + ], + [ + 29, + ], + [ + 29, + ], + [ + 29, + ], + 25 => [ + 51, ], ], - 29 => [ - 7 => [ - 30, + 30 => [ + 8 => [ + 31, ], 2 => [ - 40, + 42, ], 3 => [ - 40, + 42, ], 4 => [ - 40, + 42, ], 5 => [ - 40, + 42, ], 6 => [ - 40, + 42, ], - ], - 32 => [ - 6 => [ - 33, + 7 => [ + 42, ], - 5 => [ + ], + 33 => [ + 7 => [ 34, ], - 2 => [ + 6 => [ 35, ], - 3 => [ + 2 => [ 36, ], + 3 => [ + 37, + ], 4 => [ - 37, + 38, ], - ], - 42 => [ - 26 => [ - 43, + 5 => [ + 39, ], - 10 => [ - 44, + ], + 44 => [ + 25 => [ + 45, ], - 19 => [ + 8 => [ 50, ], - 14 => [ - 71, + ], + 54 => [ + 11 => [ + 55, + ], + 18 => [ + 59, ], - 17 => [ + 13 => [ 80, + 91, + ], + 16 => [ + 101, ], ], - 46 => [ + 57 => [ 2 => [ - 47, + 58, ], [ - 47, + 58, ], [ - 47, + 58, ], [ - 47, + 58, ], [ - 47, + 58, ], - 26 => [ - 49, + [ + 58, ], - ], - 59 => [ - 28 => [ - 60, + 25 => [ + 45, ], + ], + 68 => [ 27 => [ - 61, + 69, + ], + 26 => [ + 70, ], ], - 73 => [ + 83 => [ 2 => [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], - 6 => [ - 76, + [ + 86, + ], + 7 => [ + 86, ], [ - 76, + 86, ], - 16 => [ - 76, + 15 => [ + 86, ], - 19 => [ - 76, + 18 => [ + 86, ], - 27 => [ - 76, + 25 => [ + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, ], [ - 76, + 86, + ], + ], + 86 => [ + 2 => [ + 0, ], [ - 76, + 0, ], - ], - 85 => [ - 16 => [ - 96, + [ + 0, ], - 19 => [ - 96, + [ + 0, ], - 27 => [ - 96, + [ + 0, ], [ - 96, + 0, ], - 31 => [ - 96, + [ + 0, + ], + 15 => [ + 0, + ], + 18 => [ + 0, + ], + 26 => [ + 0, ], [ - 96, + 0, ], [ - 96, + 0, ], - ], - 87 => [ - 2 => [ - 88, - 92, + [ + 0, ], [ - 88, - 92, + 0, ], [ - 88, - 92, + 0, ], [ - 88, - 92, + 0, ], [ - 88, - 92, + 0, ], [ - 92, + 0, ], - 34 => [ - 21, + ], + 95 => [ + 2 => [ + 96, ], [ - 21, + 96, ], [ - 21, + 96, ], [ - 21, + 96, ], - 29 => [ - 14, + [ + 96, ], - 30 => [ - 14, + [ + 96, + ], + 24 => [ + 97, ], ], - 93 => [ - 26 => [ - 43, + 106 => [ + 15 => [ + 115, ], - 10 => [ - 44, + 27 => [ + 115, ], ], - 118 => [ - 2 => [ - 35, + 108 => [ + 8 => [ + 8, ], - 1 => [ - 119, + 18 => [ + 8, ], - 13 => [ - 120, + 25 => [ + 8, + ], + [ + 8, + ], + 28 => [ + 8, + ], + [ + 8, ], - 12 => [ - 121, + [ + 8, + ], + [ + 8, + ], + [ + 8, ], [ - 122, + 8, ], [ - 123, + 8, + ], + ], + 137 => [ + 2 => [ + 36, + ], + 1 => [ + 138, ], ], - 124 => [ + 139 => [ 2 => [ 0, ], @@ -1655,19 +1850,19 @@ public function __construct() [ 0, ], - 16 => [ + [ 0, ], - 19 => [ + 15 => [ 0, ], - 29 => [ + 18 => [ 0, ], - [ + 25 => [ 0, ], - [ + 28 => [ 0, ], [ @@ -1694,7 +1889,8 @@ public function __construct() 'T_WHITESPACE', 'T_NEQ', 'T_EQ', - 'T_BOOL_LITERAL', + 'T_TRUE_LITERAL', + 'T_FALSE_LITERAL', 'T_NULL_LITERAL', 'T_NAME_WITH_SPACE', 'T_NAME', @@ -1703,8 +1899,6 @@ public function __construct() 'T_COMMA', 'T_DOUBLE_COLON', 'T_COLON', - 'T_LTE', - 'T_GTE', 'T_ANGLE_BRACKET_OPEN', 'T_ANGLE_BRACKET_CLOSE', 'T_QMARK', @@ -1722,9 +1916,7 @@ public function __construct() 'T_VARIABLE', 'T_DQ_STRING_LITERAL', 'T_SQ_STRING_LITERAL', - 'T_PFX_FLOAT_LITERAL', - 'T_SFX_FLOAT_LITERAL', - 'T_EXP_LITERAL', + 'T_FLOAT_LITERAL', 'T_BIN_INT_LITERAL', 'T_OCT_INT_LITERAL', 'T_HEX_INT_LITERAL', @@ -1765,26 +1957,6 @@ private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed $children[2], $offset, ), - self::T_GTE => new Type\Condition\GreaterThanOrEqualConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_ANGLE_BRACKET_CLOSE => new Type\Condition\GreaterThanConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_LTE => new Type\Condition\LessThanOrEqualConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_ANGLE_BRACKET_OPEN => new Type\Condition\LessThanConditionNode( - $children[0], - $children[2], - $offset, - ), default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( $children[1]->value, $offset, @@ -1812,8 +1984,6 @@ private function reduceUnionType(\Phplrt\Parser\Context $ctx, mixed $children): throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); } - // A logical statement takes an arbitrary number of types, so its - // offset is assigned rather than passed to the constructor $statement = new Type\UnionTypeNode($children); $statement->offset = $offset; @@ -1833,8 +2003,6 @@ private function reduceIntersectionType(\Phplrt\Parser\Context $ctx, mixed $chil throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); } - // A logical statement takes an arbitrary number of types, so its - // offset is assigned rather than passed to the constructor $statement = new Type\IntersectionTypeNode($children); $statement->offset = $offset; @@ -1849,7 +2017,7 @@ private static function reduceNullableType(\Phplrt\Parser\Context $ctx, mixed $c return new Type\NullableTypeNode($children[0], $offset); } - private function reduceTypesList(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private function reduceListOrOffsetType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; @@ -1861,7 +2029,6 @@ private function reduceTypesList(\Phplrt\Parser\Context $ctx, mixed $children): $child = $children[$i]; switch (true) { - // In case of list type case $child === true: if ($this->features->lists === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); @@ -1869,7 +2036,6 @@ private function reduceTypesList(\Phplrt\Parser\Context $ctx, mixed $children): $statement = new Type\TypesListNode($statement, $offset); break; - // In case of offset access type case $child instanceof Type\TypeNode: if ($this->features->offsets === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); @@ -1888,15 +2054,15 @@ private function reduceTypesList(\Phplrt\Parser\Context $ctx, mixed $children): return $statement; } - private static function reduceThisLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceThisType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); + return new Type\ThisNode($offset); } - private function reduceLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private function reduceLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; @@ -1904,56 +2070,85 @@ private function reduceLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mi if ($this->features->literals === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); } - return $children; } - private function reduceDoubleQuotedStringLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceDoubleQuotedStringLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\StringLiteralNode( + Internal\StringDecoder::unpackAndDecode($children->value, true), + $children->value, + $offset, + ); + } + + private static function reduceSingleQuotedStringLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return $this->createDoubleQuotedString($children->value, $offset); + return new Type\Literal\StringLiteralNode( + Internal\StringDecoder::unpackAndDecode($children->value, false), + $children->value, + $offset, + ); } - private function reduceSingleQuotedStringLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceFloatLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return $this->createSingleQuotedString($children->value, $offset); + return new Type\Literal\FloatLiteralNode( + Internal\FloatDecoder::decode($children->value), + $children->value, + $offset, + ); } - private static function reduceFloatLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceIntLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return new Type\Literal\FloatLiteralNode((float) $children->value, $children->value, $offset); + $decoded = Internal\IntDecoder::decode($children->value); + + return new Type\Literal\IntLiteralNode( + $decoded->value, + $children->value, + $decoded->decimal, + $offset, + ); } - private function reduceIntLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceTrueLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return $this->createInt($children->value, $offset); + return new Type\Literal\BoolLiteralNode(true, $children->value, $offset); } - private function reduceBoolLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceFalseLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return $this->createBool($children->value, $offset); + return new Type\Literal\BoolLiteralNode(false, $children->value, $offset); } - private static function reduceNullLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceNullLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return new Type\Literal\NullLiteralNode($children->value, $offset); + return new Type\Literal\NullLiteralNode( + $children->value, + $offset, + ); } private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -1961,45 +2156,95 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): // The variables below are declared by the compiler $offset = $ctx->begin; + $head = $children[0]; $suffix = $children[1] ?? null; + if ($head instanceof Type\ConstMaskNode) { + if ($suffix !== null) { + throw Exception\ConstMaskException::becauseNothingFollowsAMask($offset); + } + + return $head; + } + switch (true) { + // Some\Any + case $suffix === null: + return new Type\NamedTypeNode($children[0], null, null, $offset); + // Some\Any case $suffix instanceof Type\Template\TemplateArgumentListNode: return new Type\NamedTypeNode($children[0], $suffix, null, $offset); - // Some\Any{name: T, ...} + // Some\Any{name: T, ...} case $suffix instanceof Type\Shape\FieldsListNode: return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); - // Some\Any(T, U): V + // Some\Any(T, U): V case $suffix instanceof Type\Callable\CallableParameterListNode: if ($this->features->callables === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); } - return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, $offset); + return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); - // Some\Any::CONST and Some\Any::CONST_* - case $suffix instanceof Type\Identifier: - if (isset($children[2])) { - return new Type\ClassConstMaskNode($children[0], $suffix, $offset); + // Some\Any(T): U + case $suffix instanceof Type\Template\TemplateParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); } - return new Type\ClassConstNode($children[0], $suffix, $offset); + return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); - // Some\Any::* - case $suffix === true: - return new Type\ClassConstMaskNode($children[0], offset: $offset); + // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* + default: + if (\count($children) === 3 && $children[2] instanceof Type\Identifier) { + return new Type\ClassConstNode($children[0], $children[2], $offset); + } - // SOME_ANY_* - case $suffix === false: - return new Type\ConstMaskNode($children[0], $offset); + return new Type\ClassConstMaskNode( + $children[0], + new Type\MaskNode(\array_slice($children, 2), $children[2]->offset), + $offset, + ); + } + } - // Some\Any - default: - return new Type\NamedTypeNode($children[0], null, null, $offset); + private static function reduceNameOrMask(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $head = $children[0]; + + // "*_SOME" + if (!$head instanceof Type\Name) { + return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); + } + + // "Some\Any" + if (!isset($children[1])) { + return $head; + } + + $parts = $head->parts; + + if ($children[1] instanceof Type\MaskNode) { + // "Some\Any\*" + $mask = $children[1]; + } else { + // "Some\Any\SOME_*" + $segment = \array_pop($parts); + $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); } + + return new Type\ConstMaskNode( + $mask, + $parts === [] + ? $head->isFullyQualified + : new Type\Name($parts, $head->isFullyQualified, $head->offset), + $offset, + ); } private static function reduceFullQualifiedName(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -2015,8 +2260,6 @@ private static function reduceIdentifier(\Phplrt\Parser\Context $ctx, mixed $chi // The variables below are declared by the compiler $offset = $ctx->begin; - // Only a name written with a trailing space carries one, so the value of - // every other token is the identifier itself return new Type\Identifier( $children->id === self::T_NAME_WITH_SPACE ? \rtrim($children->value) @@ -2033,14 +2276,20 @@ private static function reduceRelativeName(\Phplrt\Parser\Context $ctx, mixed $c return new Type\Name($children, false, $offset); } - private static function reduceConstMaskSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceWildcardType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { - return false; + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\WildcardNode($offset); } - private static function reduceClassConstMaskSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceNamespacedMask(\Phplrt\Parser\Context $ctx, mixed $children): mixed { - return true; + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\MaskNode($children, $offset); } private static function reduceCallableParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -2056,12 +2305,19 @@ private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mix // The variables below are declared by the compiler $offset = $ctx->begin; - $parameter = new Type\Callable\CallableParameterNode($children[0], offset: $offset); + $parameter = new Type\Callable\CallableParameterNode( + $children[0], + null, + false, + false, + false, + $offset, + ); for ($i = 1, $count = \count($children); $i < $count; ++$i) { $child = $children[$i]; - if ($child instanceof Type\Literal\VariableLiteralNode) { + if ($child instanceof Type\VariableNode) { $parameter->name = $child; continue; @@ -2076,8 +2332,6 @@ private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mix $parameter->isVariadic = true; break; - // A variadic parameter is optional in itself, so a default marker - // beside one describes the same parameter in two different ways default: if ($parameter->isVariadic) { throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); @@ -2090,12 +2344,15 @@ private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mix return $parameter; } - private static function reduceVariableLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceVariable(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return new Type\Literal\VariableLiteralNode(\substr($children->value, 1), $offset); + return new Type\VariableNode( + new Type\Identifier(\substr($children->value, 1), $offset), + $offset, + ); } private function reduceTemplateArguments(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -2115,23 +2372,15 @@ private function reduceTemplateArgument(\Phplrt\Parser\Context $ctx, mixed $chil // The variables below are declared by the compiler $offset = $ctx->begin; - $count = \count($children); - $hint = null; - - // An "in T" or an "out T" argument puts a hint in front of the type - if ($count === 2) { + if (\is_array($children)) { if ($this->features->hints === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); } - $hint = $children[0]; + return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset); } - return new Type\Template\TemplateArgumentNode( - $children[$count - 1], - $hint, - $offset, - ); + return new Type\Template\TemplateArgumentNode($children, null, $offset); } private static function reduceIdentifierWithExtraSpace(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -2142,6 +2391,77 @@ private static function reduceIdentifierWithExtraSpace(\Phplrt\Parser\Context $c return new Type\Identifier(\rtrim($children->value), $offset); } + private function reduceTemplateParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + if ($this->features->generics === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $offset); + } + + return new Type\Template\TemplateParameterListNode($children, $offset); + } + + private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $upper = $lower = $default = null; + + foreach (\array_slice($children, 1) as $limit) { + if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) { + if ($default !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $limit->offset); + } + + $default = $limit; + + continue; + } + + if ($default !== null) { + throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($limit->operator->offset); + } + + switch ($limit->operator->toLowerString()) { + case 'of': + case 'as': + if ($upper !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $limit->offset); + } + + $upper = $limit; + break; + + case 'super': + if ($lower !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $limit->offset); + } + + $lower = $limit; + break; + + default: + throw Exception\TemplateBoundException::becauseOperatorIsUnknown( + $limit->operator->value, + $limit->operator->offset, + ); + } + } + + return new Type\Template\TemplateParameterNode($children[0], $upper, $lower, $default, $offset); + } + + private static function reduceTemplateBoundEdge(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Template\TemplateBoundEdgeNode($children[0], $children[1], $offset); + } + private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler @@ -2149,9 +2469,8 @@ private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children) $count = \count($children); - // An empty shape carries no fields and needs no support for them if ($count === 0) { - return [new Type\Shape\FieldsListNode(offset: $offset)]; + return [new Type\Shape\FieldsListNode([], true, $offset)]; } if ($this->features->shapes === false) { @@ -2160,7 +2479,7 @@ private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children) $parameters = null; - // The "..." suffix, in case of the shape is unsealed by a typed rest + // The "..." suffix if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) { $parameters = $children[$count - 1]; --$count; @@ -2168,10 +2487,9 @@ private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children) if ($children[0] instanceof Type\Shape\FieldsListNode) { $fields = $children[0]; - // Whatever is left beside the fields is the "..." of an unsealed shape $unsealed = $count > 1; } else { - $fields = new Type\Shape\FieldsListNode(offset: $offset); + $fields = new Type\Shape\FieldsListNode([], true, $offset); $unsealed = $count > 0; } @@ -2191,28 +2509,37 @@ private static function reduceShapeFieldsList(\Phplrt\Parser\Context $ctx, mixed // The variables below are declared by the compiler $offset = $ctx->begin; - $explicit = []; + $explicit = false; $implicit = false; + $indexed = []; foreach ($children as $field) { - if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->getIndex(); + if (!$field instanceof Type\Shape\ExplicitFieldNode) { + $implicit = true; - if (isset($explicit[$key])) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); - } + continue; + } - $explicit[$key] = true; - } else { - $implicit = true; + $explicit = true; + + if (!$field instanceof Type\Shape\SimpleFieldNodeInterface) { + continue; + } + + $key = $field->getIndex(); + + if (isset($indexed[$key])) { + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); } + + $indexed[$key] = true; } - if ($explicit !== [] && $implicit) { + if ($explicit && $implicit) { throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); } - return new Type\Shape\FieldsListNode($children, offset: $offset); + return new Type\Shape\FieldsListNode($children, true, $offset); } private static function reduceExplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -2224,59 +2551,33 @@ private static function reduceExplicitField(\Phplrt\Parser\Context $ctx, mixed $ $name = $children[0]; $value = $children[$count - 1]; - // In case of "nullable" suffix defined $optional = $count === 3; return match (true) { - $name instanceof Type\Literal\IntLiteralNode - => new Type\Shape\NumericFieldNode($name, $value, $optional, offset: $offset), + // "a", "true" and "null" + $name instanceof Type\Identifier + => new Type\Shape\NamedFieldNode($name, $value, $optional, $offset), + $name instanceof Type\Literal\IntLiteralNode, $name instanceof Type\Literal\StringLiteralNode - => new Type\Shape\StringNamedFieldNode($name, $value, $optional, offset: $offset), - $name instanceof Type\ClassConstNode - => new Type\Shape\ClassConstFieldNode($name, $value, $optional, offset: $offset), - $name instanceof Type\ClassConstMaskNode - => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional, offset: $offset), + => new Type\Shape\ScalarFieldNode($name, $value, $optional, $offset), + // "Some::CONST", "Some::CONST_*" and "JSON_*" + $name instanceof Type\ClassConstNode, + $name instanceof Type\ClassConstMaskNode, $name instanceof Type\ConstMaskNode - => new Type\Shape\ConstMaskFieldNode($name, $value, $optional, offset: $offset), - default => new Type\Shape\NamedFieldNode($name, $value, $optional, offset: $offset), + => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset), + default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($name->offset), }; } - private static function reduceShapeConstantKey(\Phplrt\Parser\Context $ctx, mixed $children): mixed - { - // The variables below are declared by the compiler - $offset = $ctx->begin; - - $suffix = $children[1]; - - switch (true) { - // Some\Any::CONST and Some\Any::CONST_* - case $suffix instanceof Type\Identifier: - if (isset($children[2])) { - return new Type\ClassConstMaskNode($children[0], $suffix, $offset); - } - - return new Type\ClassConstNode($children[0], $suffix, $offset); - - // Some\Any::* - case $suffix === true: - return new Type\ClassConstMaskNode($children[0], offset: $offset); - - // SOME_ANY_* - default: - return new Type\ConstMaskNode($children[0], $offset); - } - } - private static function reduceImplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - return new Type\Shape\ImplicitFieldNode($children[0], offset: $offset); + return new Type\Shape\ImplicitFieldNode($children[0], false, $offset); } - private static function reduceTypeListOrOffsetSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceListOrOffsetSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed { return $children[0] ?? true; } diff --git a/libs/parser/src/Internal/Executor.php b/libs/parser/src/Internal/Executor.php index 9a4f974..4dfead3 100644 --- a/libs/parser/src/Internal/Executor.php +++ b/libs/parser/src/Internal/Executor.php @@ -12,9 +12,6 @@ use Phplrt\Parser\Analysis\Result\SuccessfulResult; use Phplrt\Parser\Parser as ParserRuntime; use TypeLang\Parser\TypeParserFeatures; -use TypeLang\Type\Literal\BoolLiteralNode; -use TypeLang\Type\Literal\IntLiteralNode; -use TypeLang\Type\Literal\StringLiteralNode; use TypeLang\Type\TypeNode; /** @@ -57,107 +54,4 @@ public function analyze( return $parser->analyze($source, $mode); } - - /** - * @param non-empty-string $value - * @param int<0, max> $offset - * @throws \InvalidArgumentException in case of value parsing error occurs - */ - protected function createDoubleQuotedString(string $value, int $offset = 0): StringLiteralNode - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - return new StringLiteralNode( - value: StringDecoder::decode(\substr($value, 1, -1)), - raw: $value, - offset: $offset, - ); - } - - /** - * @param non-empty-string $value - * @param int<0, max> $offset - * @throws \InvalidArgumentException in case of value parsing error occurs - */ - protected function createSingleQuotedString(string $value, int $offset = 0): StringLiteralNode - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - return new StringLiteralNode( - value: StringDecoder::unescape(\substr($value, 1, -1)), - raw: $value, - offset: $offset, - ); - } - - /** - * Parse raw integer literal string value: Decimal, hexadecimal - * (like a "0xFF"), octal (like a "0o17" or a "017") and binary - * (like a "0b1010") ones. - * - * @param int<0, max> $offset - */ - protected function createInt(string $value, int $offset = 0): IntLiteralNode - { - [$negative, $decimal] = self::split($value); - - $inverse = '-' . $decimal; - - if ($negative) { - if ((string) \PHP_INT_MIN === $inverse) { - return new IntLiteralNode(\PHP_INT_MIN, $value, $inverse, $offset); - } - - /** @phpstan-ignore-next-line : An "$inverse" variable contain numeric-string */ - return new IntLiteralNode((int) $inverse, $value, $inverse, $offset); - } - - return new IntLiteralNode((int) $decimal, $value, $decimal, $offset); - } - - /** - * @return array{bool, numeric-string} - */ - private static function split(string $literal): array - { - $literal = \str_replace('_', '', $literal); - - if ($negative = ($literal[0] === '-')) { - $literal = \substr($literal, 1); - } - - // One of: [ 0123, 0o23, 0x00, 0b01 ] - if ($literal[0] === '0' && isset($literal[1])) { - /** @var array{bool, numeric-string} */ - return [$negative, match ($literal[1]) { - // hexadecimal - 'x', 'X' => \base_convert(\substr($literal, 2), 16, 10), - // binary - 'b', 'B' => \base_convert(\substr($literal, 2), 2, 10), - // octal - 'o', 'O' => \base_convert(\substr($literal, 2), 8, 10), - // octal (legacy) - default => \base_convert($literal, 8, 10), - }]; - } - - /** @var array{bool, numeric-string} */ - return [$negative, $literal]; - } - - /** - * @param int<0, max> $offset - */ - protected function createBool(string $value, int $offset = 0): BoolLiteralNode - { - return new BoolLiteralNode( - \strtolower($value) === 'true', - $value, - $offset, - ); - } } diff --git a/libs/parser/src/Internal/FloatDecoder.php b/libs/parser/src/Internal/FloatDecoder.php new file mode 100644 index 0000000..cd601b3 --- /dev/null +++ b/libs/parser/src/Internal/FloatDecoder.php @@ -0,0 +1,13 @@ + \base_convert(\substr($value, 2), 16, 10), + // binary + 'b', 'B' => \base_convert(\substr($value, 2), 2, 10), + // octal + 'o', 'O' => \base_convert(\substr($value, 2), 8, 10), + // octal (legacy) + default => \base_convert($value, 8, 10), + }; + } +} diff --git a/libs/parser/src/Internal/IntDecoder/BigIntValue.php b/libs/parser/src/Internal/IntDecoder/BigIntValue.php new file mode 100644 index 0000000..3c2e2de --- /dev/null +++ b/libs/parser/src/Internal/IntDecoder/BigIntValue.php @@ -0,0 +1,22 @@ + + * Every other sequence is left to the {@see decode()}. */ - private const SCANNER_THRESHOLD = 4; - - public static function unescape(string $value): string + public static function unpack(string $value, bool $isDoubleQuoted): string { - return \strtr($value, ["\'" => "'", '\\\\' => '\\']); + if ($isDoubleQuoted) { + return \strtr(\substr($value, 1, -1), ['\\"' => '"']); + } + + return \strtr(\substr($value, 1, -1), ["\'" => "'"]); } /** - * Method for parsing and decode all escaped character sequences: Special - * chars (like a "\n"), hexadecimal (like a "\xFF"), octal (like a "\101") - * and utf-8 (like a "\u{FFFF}") ones. - * - * @link https://www.php.net/manual/en/language.types.string.php + * Decodes the sequences of an unpacked body: A `\\` in a single-quoted + * one, and special chars (like a `\n`), hexadecimal (like a `\xFF`), + * octal (like a `\101`) and utf-8 (like a `\u{FFFF}`) ones in a + * double-quoted one. */ - public static function decode(string $body): string + public static function decode(string $value, bool $isDoubleQuoted): string { - if (!\str_contains($body, '\\')) { - return $body; + if ($isDoubleQuoted === false) { + return \strtr($value, ['\\\\' => '\\']); } - return \strtr($body, self::fetchReplacements($body)); + if (!\str_contains($value, '\\')) { + return $value; + } + + return \strtr($value, StringSequencesFetcher::get($value)); } /** - * Chooses a {@see SequenceFetcherInterface} implementation suitable for - * the passed string. - * - * @return non-empty-array + * Both of the above, in the order a literal is read in. */ - private static function fetchReplacements(string $body): array + public static function unpackAndDecode(string $value, bool $isDoubleQuoted): string { - // Numeric sequences require an additional pass, which can be skipped - // in case of the string does not contain any of their prefixes. - if (@\preg_match(self::NUMERIC_PREFIX_PATTERN, $body) !== 1) { - return SequenceFetcherInterface::ESCAPED_CHARS; - } - - if (\substr_count($body, '\\') <= self::SCANNER_THRESHOLD) { - return ScannerSequenceFetcher::fetch($body); - } - - return PatternSequenceFetcher::fetch($body); + return StringDecoder::decode( + StringDecoder::unpack($value, $isDoubleQuoted), + $isDoubleQuoted, + ); } } diff --git a/libs/parser/src/Internal/StringDecoder/ScannerSequenceFetcher.php b/libs/parser/src/Internal/StringDecoder/ScannerSequenceFetcher.php deleted file mode 100644 index a01f1c4..0000000 --- a/libs/parser/src/Internal/StringDecoder/ScannerSequenceFetcher.php +++ /dev/null @@ -1,93 +0,0 @@ - - $result[$sequence] ??= \chr((int) \hexdec(\substr($sequence, 2))); - $offset += $digits + 1; - - continue; - } - } elseif ($char === 'u') { - $digits = \strspn($body, self::HEX_DIGITS, $offset + 2); - - // An utf-8 sequence, like a "\u{FFFF}" - if ( - $digits !== 0 - && ($body[$offset + 1] ?? '') === '{' - && ($body[$offset + $digits + 2] ?? '') === '}' - ) { - $sequence = \substr($body, $position, $digits + 4); - - /** @var int<0, max> $code */ - $code = (int) \hexdec(\substr($sequence, 3, $digits)); - - $result[$sequence] ??= UtfCharRenderer::render($code); - $offset += $digits + 3; - - continue; - } - } elseif ($char >= '0' && $char <= '7') { - // An octal sequence, like a "\101". Overflowed sequences - // (greater than a "\377") are truncated, like in PHP itself. - $digits = \strspn($body, self::OCTAL_DIGITS, $offset, 3); - $sequence = \substr($body, $position, $digits + 1); - $result[$sequence] ??= \chr(((int) \octdec(\substr($sequence, 1))) & 0xFF); - $offset += $digits; - - continue; - } - - ++$offset; - } - - return $result; - } -} diff --git a/libs/parser/src/Internal/StringDecoder/SequenceFetcherInterface.php b/libs/parser/src/Internal/StringDecoder/SequenceFetcherInterface.php deleted file mode 100644 index 1d5a439..0000000 --- a/libs/parser/src/Internal/StringDecoder/SequenceFetcherInterface.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ - public const ESCAPED_CHARS = [ - '\n' => "\n", - '\r' => "\r", - '\t' => "\t", - '\v' => "\v", - '\e' => "\e", - '\f' => "\f", - '\$' => '$', - '\\"' => '"', - '\\\\' => '\\', - ]; - - /** - * Returns a "sequence => replacement" map of all constant sequences along - * with each hexadecimal (like a "\xFF"), octal (like a "\101") and utf-8 - * (like a "\u{FFFF}") sequence occurred in the passed string. - * - * @link https://www.php.net/manual/en/language.types.string.php - * - * @return non-empty-array - */ - public static function fetch(string $body): array; -} diff --git a/libs/parser/src/Internal/StringDecoder/PatternSequenceFetcher.php b/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php similarity index 54% rename from libs/parser/src/Internal/StringDecoder/PatternSequenceFetcher.php rename to libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php index 3bfca9d..2c441a2 100644 --- a/libs/parser/src/Internal/StringDecoder/PatternSequenceFetcher.php +++ b/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php @@ -5,18 +5,53 @@ namespace TypeLang\Parser\Internal\StringDecoder; /** - * Fetches sequences by a single regexp pass over the whole string. + * @internal this is an internal library class, please do not use it in your code + * @psalm-internal TypeLang\Parser\Internal */ -final class PatternSequenceFetcher implements SequenceFetcherInterface +final class StringSequencesFetcher { + /** + * Sequences with a constant replacement. + * + * @var non-empty-array + */ + private const ESCAPED_CHARS = [ + '\n' => "\n", + '\r' => "\r", + '\t' => "\t", + '\v' => "\v", + '\e' => "\e", + '\f' => "\f", + '\$' => '$', + '\\\\' => '\\', + ]; + + /** + * @var non-empty-string + */ + private const NUMERIC_PREFIX_PATTERN = '/\\\\[uxX0-7]/'; + /** * @var non-empty-string */ - private const NUMERIC_SEQUENCE_PATTERN = '/\\\\(?:u\{([0-9a-fA-F]+)\}|[xX]([0-9a-fA-F]{1,2})|([0-7]{1,3}))/'; + private const NUMERIC_SEQUENCE_PATTERN = '/\\\\(?:u\{([0-9a-fA-F]+)}|[xX]([0-9a-fA-F]{1,2})|([0-7]{1,3}))/'; - public static function fetch(string $body): array + /** + * Returns a "sequence => replacement" map of all constant sequences along + * with each hexadecimal (like a "\xFF"), octal (like a "\101") and utf-8 + * (like a "\u{FFFF}") sequence occurred in the past string. + * + * @link https://www.php.net/manual/en/language.types.string.php + * + * @return non-empty-array + */ + public static function get(string $value): array { - $count = @\preg_match_all(self::NUMERIC_SEQUENCE_PATTERN, $body, $matches, \PREG_SET_ORDER); + if (@\preg_match(self::NUMERIC_PREFIX_PATTERN, $value) !== 1) { + return self::ESCAPED_CHARS; + } + + $count = @\preg_match_all(self::NUMERIC_SEQUENCE_PATTERN, $value, $matches, \PREG_SET_ORDER); if ($count === false || $count === 0) { return self::ESCAPED_CHARS; diff --git a/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php index 33c6fb6..416e737 100644 --- a/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php +++ b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php @@ -4,6 +4,10 @@ namespace TypeLang\Parser\Internal\StringDecoder; +/** + * @internal this is an internal library class, please do not use it in your code + * @psalm-internal TypeLang\Parser\Internal + */ final class UtfCharRenderer { /** From 0b0c8d2ee17ba5197e80299cbbca399c3a186a21 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 00:17:53 +0300 Subject: [PATCH 076/119] Fix type map visitor --- libs/parser/src/Traverser/TypeMapVisitor.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/parser/src/Traverser/TypeMapVisitor.php b/libs/parser/src/Traverser/TypeMapVisitor.php index 76cc0ff..a15be2d 100644 --- a/libs/parser/src/Traverser/TypeMapVisitor.php +++ b/libs/parser/src/Traverser/TypeMapVisitor.php @@ -37,11 +37,19 @@ public function enter(Node $node): ?Command switch (true) { case $node instanceof NamedTypeNode: case $node instanceof CallableTypeNode: - case $node instanceof ConstMaskNode: $node->name = $this->map($node->name); return null; + case $node instanceof ConstMaskNode: + // A mask carries no namespace of its own in case of the + // constant is written with none, and nothing is mapped then + if ($node->namespaceOrFullyQualified instanceof Name) { + $node->namespaceOrFullyQualified = $this->map($node->namespaceOrFullyQualified); + } + + return null; + case $node instanceof ClassConstNode: case $node instanceof ClassConstMaskNode: $node->class = $this->map($node->class); From eb583c43663231319e6022a471a025d4dcf27707 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:23:08 +0300 Subject: [PATCH 077/119] Fix TypeOffsetAccessNode::$access property --- libs/types/src/TypeOffsetAccessNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/types/src/TypeOffsetAccessNode.php b/libs/types/src/TypeOffsetAccessNode.php index fd27d79..fa667b1 100644 --- a/libs/types/src/TypeOffsetAccessNode.php +++ b/libs/types/src/TypeOffsetAccessNode.php @@ -30,7 +30,7 @@ public function __construct( * it is not taken for the place in the source text every node * carries. */ - public readonly TypeNode $access, + public TypeNode $access, int $offset = 0, ) { parent::__construct($type, $offset); From e5a363d4a2aecb66837d4fa8d197d358c984c3e0 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:24:10 +0300 Subject: [PATCH 078/119] Make of/as/super case sensitive --- libs/parser/resources/grammar/template-parameters.pp3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/parser/resources/grammar/template-parameters.pp3 b/libs/parser/resources/grammar/template-parameters.pp3 index 47f5776..f831b16 100644 --- a/libs/parser/resources/grammar/template-parameters.pp3 +++ b/libs/parser/resources/grammar/template-parameters.pp3 @@ -36,7 +36,7 @@ TemplateParameter -> { throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($limit->operator->offset); } - switch ($limit->operator->toLowerString()) { + switch ($limit->operator->value) { case 'of': case 'as': if ($upper !== null) { From 53679eb1b67b87b6d7d62fa5fab4e00a83f43e9c Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:24:55 +0300 Subject: [PATCH 079/119] Split T_EQ + T_NOT and disable lexer's unicode --- libs/parser/resources/grammar.pp3 | 30 +++++++++-------------- libs/parser/resources/grammar/common.pp3 | 1 + libs/parser/resources/grammar/lexemes.pp3 | 15 +++++++----- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3 index 8ef5b9c..8dd08a7 100644 --- a/libs/parser/resources/grammar.pp3 +++ b/libs/parser/resources/grammar.pp3 @@ -14,6 +14,9 @@ %include grammar/shape-fields // array { key: int, ... } %include grammar/named-type // Map, non-empty-string> +// The lexer reads bytes rather than codepoints, so a "Тип" is a name the way a "Type" is +%pragma lexer.pcre.disable u + %pragma root Type /** @@ -31,22 +34,9 @@ Type -> { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); } - $condition = match ($children[1]->id) { - self::T_EQ => new Type\Condition\EqualConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_NEQ => new Type\Condition\NotEqualConditionNode( - $children[0], - $children[2], - $offset, - ), - default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( - $children[1]->value, - $offset, - ), - }; + $condition = $children[1] + ? new Type\Condition\NotEqualConditionNode($children[0], $children[2], $offset) + : new Type\Condition\EqualConditionNode($children[0], $children[2], $offset); return new Type\TernaryExpressionNode( $condition, @@ -65,9 +55,11 @@ TernaryExpressionSuffix ::T_COLON:: Type() ; -TernaryExpressionOperator - : - | +// An "is" or an "is not" +TernaryExpressionOperator -> { + return \count($children) === 2; +} + : ? ; /** diff --git a/libs/parser/resources/grammar/common.pp3 b/libs/parser/resources/grammar/common.pp3 index 47a120b..e273ef2 100644 --- a/libs/parser/resources/grammar/common.pp3 +++ b/libs/parser/resources/grammar/common.pp3 @@ -30,6 +30,7 @@ Identifier -> { : | | + | | | | diff --git a/libs/parser/resources/grammar/lexemes.pp3 b/libs/parser/resources/grammar/lexemes.pp3 index 6fcbd3b..d8351c8 100644 --- a/libs/parser/resources/grammar/lexemes.pp3 +++ b/libs/parser/resources/grammar/lexemes.pp3 @@ -12,6 +12,9 @@ %fragment IDENT_HEAD [a-zA-Z_\x80-\xff] %fragment IDENT_TAIL [a-zA-Z0-9\-_\x80-\xff] +// The "foo_1" of a "$foo_1", which carries no dash +%fragment VAR_TAIL [a-zA-Z0-9_\x80-\xff] + %fragment KEYWORD_END (?!(?&IDENT_TAIL)) // @@ -56,10 +59,10 @@ %skip T_WHITESPACE \s++ -// An "is not" or an "IS NOT" -%token T_NEQ (?i:is\h++not)(?&KEYWORD_END) -// An "is" or an "IS" -%token T_EQ (?i:is)(?&KEYWORD_END) +// An "is" +%token T_EQ is(?&KEYWORD_END) +// A "not" +%token T_NOT not(?&KEYWORD_END) // A "true" or a "TRUE" %token T_TRUE_LITERAL (?i:true)(?&KEYWORD_END) // A "false" or a "FALSE" @@ -95,9 +98,9 @@ // Variables // A "$this" -%token T_THIS \$this\b +%token T_THIS \$this(?!(?&VAR_TAIL)) // A "$value" or a "$_" -%token T_VARIABLE \$(?&IDENT_HEAD)(?&IDENT_TAIL)*+ +%token T_VARIABLE \$(?&IDENT_HEAD)(?&VAR_TAIL)*+ // Literals From 118cc163de30e0c41c48c62c2c4fc1184986203e Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:25:02 +0300 Subject: [PATCH 080/119] Rebuild parser --- libs/parser/src/Internal/CompilerExecutor.php | 524 ++++++++++-------- 1 file changed, 287 insertions(+), 237 deletions(-) diff --git a/libs/parser/src/Internal/CompilerExecutor.php b/libs/parser/src/Internal/CompilerExecutor.php index 7d57946..ed5e181 100644 --- a/libs/parser/src/Internal/CompilerExecutor.php +++ b/libs/parser/src/Internal/CompilerExecutor.php @@ -36,9 +36,9 @@ abstract class CompilerExecutor implements \Phplrt\Contracts\Parser\ParserInterf /** @var int */ public const T_WHITESPACE = 0; /** @var int */ - public const T_NEQ = 1; + public const T_EQ = 1; /** @var int */ - public const T_EQ = 2; + public const T_NOT = 2; /** @var int */ public const T_TRUE_LITERAL = 3; /** @var int */ @@ -118,7 +118,7 @@ abstract class CompilerExecutor implements \Phplrt\Contracts\Parser\ParserInterf public function __construct() { $this->lexer = new \Phplrt\Lexer\Lexer( - pattern: '/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:(?i:is\\h++not)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:1))|(?:(?:(?i:is)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:2))|(?:(?:(?i:true)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:3))|(?:(?:(?i:false)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:4))|(?:(?:(?i:null)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:5))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+\\s++)(*MARK:6))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:7))|(?:(?:\\\\)(*MARK:8))|(?:(?:\\|)(*MARK:9))|(?:(?:,)(*MARK:10))|(?:(?:::)(*MARK:11))|(?:(?::)(*MARK:12))|(?:(?:<)(*MARK:13))|(?:(?:>)(*MARK:14))|(?:(?:\\?)(*MARK:15))|(?:(?:\\{)(*MARK:16))|(?:(?:\\})(*MARK:17))|(?:(?:\\()(*MARK:18))|(?:(?:\\))(*MARK:19))|(?:(?:\\[)(*MARK:20))|(?:(?:\\])(*MARK:21))|(?:(?:&)(*MARK:22))|(?:(?:\\.\\.\\.)(*MARK:23))|(?:(?:=)(*MARK:24))|(?:(?:\\*)(*MARK:25))|(?:(?:\\$this\\b)(*MARK:26))|(?:(?:\\$(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:27))|(?:(?:"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+")(*MARK:28))|(?:(?:\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\')(*MARK:29))|(?:(?:(?:[-+])?(?:(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+)\\.(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+))?|\\.(?:(?:[0-9])(?:_?(?:[0-9]))*+))(?:(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+)))?|(?:(?:[0-9])(?:_?(?:[0-9]))*+)(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+))))(*MARK:30))|(?:(?:(?:[-+])?0[bB](?:(?:[01])(?:_?(?:[01]))*+))(*MARK:31))|(?:(?:(?:[-+])?0(?:[oO](?:(?:[0-7])(?:_?(?:[0-7]))*+)|(?:_?(?:[0-7]))++))(*MARK:32))|(?:(?:(?:[-+])?0[xX](?:(?:[0-9a-fA-F])(?:_?(?:[0-9a-fA-F]))*+))(*MARK:33))|(?:(?:(?:[-+])?(?:(?:(?:[1-9])(?:_?(?:[0-9]))*+)|0))(*MARK:34))|(?:(?:(?:\\/\\/|\\#)[^\\r\\n]*+)(*MARK:35))|(?:(?:\\/\\*.*?\\*\\/)(*MARK:36))|(?:(?:[^\\s]++)(*MARK:37)))/Ssum', + pattern: '/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:is(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:1))|(?:(?:not(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:2))|(?:(?:(?i:true)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:3))|(?:(?:(?i:false)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:4))|(?:(?:(?i:null)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:5))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+\\s++)(*MARK:6))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:7))|(?:(?:\\\\)(*MARK:8))|(?:(?:\\|)(*MARK:9))|(?:(?:,)(*MARK:10))|(?:(?:::)(*MARK:11))|(?:(?::)(*MARK:12))|(?:(?:<)(*MARK:13))|(?:(?:>)(*MARK:14))|(?:(?:\\?)(*MARK:15))|(?:(?:\\{)(*MARK:16))|(?:(?:\\})(*MARK:17))|(?:(?:\\()(*MARK:18))|(?:(?:\\))(*MARK:19))|(?:(?:\\[)(*MARK:20))|(?:(?:\\])(*MARK:21))|(?:(?:&)(*MARK:22))|(?:(?:\\.\\.\\.)(*MARK:23))|(?:(?:=)(*MARK:24))|(?:(?:\\*)(*MARK:25))|(?:(?:\\$this(?!(?:[a-zA-Z0-9_\\x80-\\xff])))(*MARK:26))|(?:(?:\\$(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9_\\x80-\\xff])*+)(*MARK:27))|(?:(?:"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+")(*MARK:28))|(?:(?:\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\')(*MARK:29))|(?:(?:(?:[-+])?(?:(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+)\\.(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+))?|\\.(?:(?:[0-9])(?:_?(?:[0-9]))*+))(?:(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+)))?|(?:(?:[0-9])(?:_?(?:[0-9]))*+)(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+))))(*MARK:30))|(?:(?:(?:[-+])?0[bB](?:(?:[01])(?:_?(?:[01]))*+))(*MARK:31))|(?:(?:(?:[-+])?0(?:[oO](?:(?:[0-7])(?:_?(?:[0-7]))*+)|(?:_?(?:[0-7]))++))(*MARK:32))|(?:(?:(?:[-+])?0[xX](?:(?:[0-9a-fA-F])(?:_?(?:[0-9a-fA-F]))*+))(*MARK:33))|(?:(?:(?:[-+])?(?:(?:(?:[1-9])(?:_?(?:[0-9]))*+)|0))(*MARK:34))|(?:(?:(?:\\/\\/|\\#)[^\\r\\n]*+)(*MARK:35))|(?:(?:\\/\\*.*?\\*\\/)(*MARK:36))|(?:(?:[^\\s]++)(*MARK:37)))/Ssm', channels: [ 'Hidden', 35 => 'Hidden', @@ -127,8 +127,8 @@ public function __construct() ], names: [ 'T_WHITESPACE', - 'T_NEQ', 'T_EQ', + 'T_NOT', 'T_TRUE_LITERAL', 'T_FALSE_LITERAL', 'T_NULL_LITERAL', @@ -169,14 +169,14 @@ public function __construct() $this->parser = new \Phplrt\Parser\Parser( lexer: $this->lexer, grammar: [ - new \Phplrt\Parser\Grammar\Alternation([1, 140]), - new \Phplrt\Parser\Grammar\Concatenation([2, 135]), - new \Phplrt\Parser\Grammar\Concatenation([3, 132]), - new \Phplrt\Parser\Grammar\Concatenation([4, 129]), + new \Phplrt\Parser\Grammar\Alternation([1, 141]), + new \Phplrt\Parser\Grammar\Concatenation([2, 136]), + new \Phplrt\Parser\Grammar\Concatenation([3, 133]), + new \Phplrt\Parser\Grammar\Concatenation([4, 130]), new \Phplrt\Parser\Grammar\Alternation([5, 7]), new \Phplrt\Parser\Grammar\Concatenation([6, 7]), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false), - new \Phplrt\Parser\Grammar\Concatenation([8, 124]), + new \Phplrt\Parser\Grammar\Concatenation([8, 125]), new \Phplrt\Parser\Grammar\Alternation([9, 12, 13, 27]), new \Phplrt\Parser\Grammar\Concatenation([10, 0, 11]), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false), @@ -196,120 +196,121 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Concatenation([28, 53]), - new \Phplrt\Parser\Grammar\Alternation([29, 51]), - new \Phplrt\Parser\Grammar\Concatenation([30, 43]), - new \Phplrt\Parser\Grammar\Alternation([31, 42]), - new \Phplrt\Parser\Grammar\Concatenation([32, 33, 40]), + new \Phplrt\Parser\Grammar\Concatenation([28, 54]), + new \Phplrt\Parser\Grammar\Alternation([29, 52]), + new \Phplrt\Parser\Grammar\Concatenation([30, 44]), + new \Phplrt\Parser\Grammar\Alternation([31, 43]), + new \Phplrt\Parser\Grammar\Concatenation([32, 33, 41]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), - new \Phplrt\Parser\Grammar\Alternation([34, 35, 36, 37, 38, 39]), + new \Phplrt\Parser\Grammar\Alternation([34, 35, 36, 37, 38, 39, 40]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NOT, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Repetition(41, 0, \INF), + new \Phplrt\Parser\Grammar\Repetition(42, 0, \INF), new \Phplrt\Parser\Grammar\Concatenation([32, 33]), - new \Phplrt\Parser\Grammar\Concatenation([33, 40]), - new \Phplrt\Parser\Grammar\Optional(44), - new \Phplrt\Parser\Grammar\Alternation([45, 50]), - new \Phplrt\Parser\Grammar\Concatenation([46, 47, 49]), + new \Phplrt\Parser\Grammar\Concatenation([33, 41]), + new \Phplrt\Parser\Grammar\Optional(45), + new \Phplrt\Parser\Grammar\Alternation([46, 51]), + new \Phplrt\Parser\Grammar\Concatenation([47, 48, 50]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), - new \Phplrt\Parser\Grammar\Repetition(48, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([33, 46]), + new \Phplrt\Parser\Grammar\Repetition(49, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([33, 47]), new \Phplrt\Parser\Grammar\Optional(33), - new \Phplrt\Parser\Grammar\Concatenation([32, 45]), - new \Phplrt\Parser\Grammar\Concatenation([46, 33, 52]), - new \Phplrt\Parser\Grammar\Optional(45), - new \Phplrt\Parser\Grammar\Optional(54), - new \Phplrt\Parser\Grammar\Alternation([55, 59, 80, 91, 101]), - new \Phplrt\Parser\Grammar\Concatenation([56, 57]), + new \Phplrt\Parser\Grammar\Concatenation([32, 46]), + new \Phplrt\Parser\Grammar\Concatenation([47, 33, 53]), + new \Phplrt\Parser\Grammar\Optional(46), + new \Phplrt\Parser\Grammar\Optional(55), + new \Phplrt\Parser\Grammar\Alternation([56, 60, 81, 92, 102]), + new \Phplrt\Parser\Grammar\Concatenation([57, 58]), new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true), - new \Phplrt\Parser\Grammar\Alternation([58, 45]), - new \Phplrt\Parser\Grammar\Concatenation([33, 52]), - new \Phplrt\Parser\Grammar\Concatenation([10, 60, 11, 77]), - new \Phplrt\Parser\Grammar\Optional(61), - new \Phplrt\Parser\Grammar\Concatenation([62, 73, 76]), - new \Phplrt\Parser\Grammar\Concatenation([0, 63, 65, 67, 71]), - new \Phplrt\Parser\Grammar\Optional(64), + new \Phplrt\Parser\Grammar\Alternation([59, 46]), + new \Phplrt\Parser\Grammar\Concatenation([33, 53]), + new \Phplrt\Parser\Grammar\Concatenation([10, 61, 11, 78]), + new \Phplrt\Parser\Grammar\Optional(62), + new \Phplrt\Parser\Grammar\Concatenation([63, 74, 77]), + new \Phplrt\Parser\Grammar\Concatenation([0, 64, 66, 68, 72]), + new \Phplrt\Parser\Grammar\Optional(65), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true), - new \Phplrt\Parser\Grammar\Optional(66), + new \Phplrt\Parser\Grammar\Optional(67), new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true), - new \Phplrt\Parser\Grammar\Optional(68), - new \Phplrt\Parser\Grammar\Alternation([69, 70]), + new \Phplrt\Parser\Grammar\Optional(69), + new \Phplrt\Parser\Grammar\Alternation([70, 71]), new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Optional(72), + new \Phplrt\Parser\Grammar\Optional(73), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true), - new \Phplrt\Parser\Grammar\Repetition(74, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([75, 62]), + new \Phplrt\Parser\Grammar\Repetition(75, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([76, 63]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false), - new \Phplrt\Parser\Grammar\Optional(75), - new \Phplrt\Parser\Grammar\Optional(78), - new \Phplrt\Parser\Grammar\Concatenation([79, 0]), + new \Phplrt\Parser\Grammar\Optional(76), + new \Phplrt\Parser\Grammar\Optional(79), + new \Phplrt\Parser\Grammar\Concatenation([80, 0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), - new \Phplrt\Parser\Grammar\Concatenation([81, 90]), - new \Phplrt\Parser\Grammar\Concatenation([82, 83, 87, 76, 89]), + new \Phplrt\Parser\Grammar\Concatenation([82, 91]), + new \Phplrt\Parser\Grammar\Concatenation([83, 84, 88, 77, 90]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([84, 86]), - new \Phplrt\Parser\Grammar\Concatenation([85, 86]), + new \Phplrt\Parser\Grammar\Alternation([85, 87]), + new \Phplrt\Parser\Grammar\Concatenation([86, 87]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), - new \Phplrt\Parser\Grammar\Alternation([46, 0]), - new \Phplrt\Parser\Grammar\Repetition(88, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([75, 83]), + new \Phplrt\Parser\Grammar\Alternation([47, 0]), + new \Phplrt\Parser\Grammar\Repetition(89, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([76, 84]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), new \Phplrt\Parser\Grammar\Predicate(10, false), - new \Phplrt\Parser\Grammar\Concatenation([92, 59]), - new \Phplrt\Parser\Grammar\Concatenation([82, 93, 99, 76, 89]), - new \Phplrt\Parser\Grammar\Concatenation([33, 94]), - new \Phplrt\Parser\Grammar\Repetition(95, 0, \INF), - new \Phplrt\Parser\Grammar\Alternation([96, 97]), + new \Phplrt\Parser\Grammar\Concatenation([93, 60]), + new \Phplrt\Parser\Grammar\Concatenation([83, 94, 100, 77, 90]), + new \Phplrt\Parser\Grammar\Concatenation([33, 95]), + new \Phplrt\Parser\Grammar\Repetition(96, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([97, 98]), new \Phplrt\Parser\Grammar\Concatenation([33, 0]), - new \Phplrt\Parser\Grammar\Concatenation([98, 0]), + new \Phplrt\Parser\Grammar\Concatenation([99, 0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, false), - new \Phplrt\Parser\Grammar\Repetition(100, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([75, 93]), - new \Phplrt\Parser\Grammar\Concatenation([102, 103, 76, 123]), + new \Phplrt\Parser\Grammar\Repetition(101, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([76, 94]), + new \Phplrt\Parser\Grammar\Concatenation([103, 104, 77, 124]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([104, 122]), - new \Phplrt\Parser\Grammar\Concatenation([105, 118]), - new \Phplrt\Parser\Grammar\Concatenation([106, 116]), - new \Phplrt\Parser\Grammar\Alternation([107, 115]), - new \Phplrt\Parser\Grammar\Concatenation([108, 113, 79, 0]), - new \Phplrt\Parser\Grammar\Alternation([109, 8]), - new \Phplrt\Parser\Grammar\Concatenation([33, 110]), - new \Phplrt\Parser\Grammar\Predicate(111, true), - new \Phplrt\Parser\Grammar\Concatenation([112, 79]), + new \Phplrt\Parser\Grammar\Alternation([105, 123]), + new \Phplrt\Parser\Grammar\Concatenation([106, 119]), + new \Phplrt\Parser\Grammar\Concatenation([107, 117]), + new \Phplrt\Parser\Grammar\Alternation([108, 116]), + new \Phplrt\Parser\Grammar\Concatenation([109, 114, 80, 0]), + new \Phplrt\Parser\Grammar\Alternation([110, 8]), + new \Phplrt\Parser\Grammar\Concatenation([33, 111]), + new \Phplrt\Parser\Grammar\Predicate(112, true), + new \Phplrt\Parser\Grammar\Concatenation([113, 80]), new \Phplrt\Parser\Grammar\Optional(6), - new \Phplrt\Parser\Grammar\Optional(114), + new \Phplrt\Parser\Grammar\Optional(115), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Repetition(117, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([75, 106]), - new \Phplrt\Parser\Grammar\Optional(119), - new \Phplrt\Parser\Grammar\Concatenation([75, 120]), - new \Phplrt\Parser\Grammar\Concatenation([66, 121]), - new \Phplrt\Parser\Grammar\Optional(81), + new \Phplrt\Parser\Grammar\Repetition(118, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([76, 107]), new \Phplrt\Parser\Grammar\Optional(120), + new \Phplrt\Parser\Grammar\Concatenation([76, 121]), + new \Phplrt\Parser\Grammar\Concatenation([67, 122]), + new \Phplrt\Parser\Grammar\Optional(82), + new \Phplrt\Parser\Grammar\Optional(121), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(125, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([126, 127, 128]), + new \Phplrt\Parser\Grammar\Repetition(126, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([127, 128, 129]), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false), new \Phplrt\Parser\Grammar\Optional(0), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(130, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([131, 4]), + new \Phplrt\Parser\Grammar\Repetition(131, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([132, 4]), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, false), - new \Phplrt\Parser\Grammar\Repetition(133, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([134, 3]), + new \Phplrt\Parser\Grammar\Repetition(134, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([135, 3]), new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, false), - new \Phplrt\Parser\Grammar\Optional(136), - new \Phplrt\Parser\Grammar\Concatenation([137, 139, 6, 0, 79, 0]), - new \Phplrt\Parser\Grammar\Alternation([36, 138]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_NEQ, true), - new \Phplrt\Parser\Grammar\Alternation([0, 68]), - new \Phplrt\Parser\Grammar\Concatenation([68, 136]), + new \Phplrt\Parser\Grammar\Optional(137), + new \Phplrt\Parser\Grammar\Concatenation([138, 140, 6, 0, 80, 0]), + new \Phplrt\Parser\Grammar\Concatenation([36, 139]), + new \Phplrt\Parser\Grammar\Optional(37), + new \Phplrt\Parser\Grammar\Alternation([0, 69]), + new \Phplrt\Parser\Grammar\Concatenation([69, 137]), ], initial: 0, reducers: [ @@ -331,27 +332,29 @@ public function __construct() 28 => self::reduceNameOrMask(...), 31 => self::reduceFullQualifiedName(...), 33 => self::reduceIdentifier(...), - 42 => self::reduceRelativeName(...), - 46 => self::reduceWildcardType(...), - 50 => self::reduceNamespacedMask(...), - 60 => self::reduceCallableParameters(...), - 62 => self::reduceCallableParameter(...), - 68 => self::reduceVariable(...), - 81 => $this->reduceTemplateArguments(...), - 83 => $this->reduceTemplateArgument(...), - 85 => self::reduceIdentifierWithExtraSpace(...), - 92 => $this->reduceTemplateParameters(...), - 93 => self::reduceTemplateParameter(...), - 96 => self::reduceTemplateBoundEdge(...), - 101 => $this->reduceShapeFields(...), - 105 => self::reduceShapeFieldsList(...), - 107 => self::reduceExplicitField(...), - 115 => self::reduceImplicitField(...), - 125 => self::reduceListOrOffsetSuffix(...), + 43 => self::reduceRelativeName(...), + 47 => self::reduceWildcardType(...), + 51 => self::reduceNamespacedMask(...), + 61 => self::reduceCallableParameters(...), + 63 => self::reduceCallableParameter(...), + 69 => self::reduceVariable(...), + 82 => $this->reduceTemplateArguments(...), + 84 => $this->reduceTemplateArgument(...), + 86 => self::reduceIdentifierWithExtraSpace(...), + 93 => $this->reduceTemplateParameters(...), + 94 => self::reduceTemplateParameter(...), + 97 => self::reduceTemplateBoundEdge(...), + 102 => $this->reduceShapeFields(...), + 106 => self::reduceShapeFieldsList(...), + 108 => self::reduceExplicitField(...), + 116 => self::reduceImplicitField(...), + 126 => self::reduceListOrOffsetSuffix(...), + 138 => self::reduceTernaryExpressionOperator(...), ], lookahead: [ [ - 2 => true, + 1 => true, + true, true, true, true, @@ -372,7 +375,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -392,7 +396,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -412,7 +417,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -432,7 +438,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -458,7 +465,8 @@ public function __construct() 15 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -477,7 +485,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -571,7 +580,8 @@ public function __construct() 5 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -581,7 +591,8 @@ public function __construct() 25 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -591,7 +602,8 @@ public function __construct() 25 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -600,7 +612,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -615,7 +628,8 @@ public function __construct() 8 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -628,6 +642,9 @@ public function __construct() [ 6 => true, ], + [ + 1 => true, + ], [ 2 => true, ], @@ -645,7 +662,8 @@ public function __construct() 8 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -665,7 +683,8 @@ public function __construct() ], null, [ - 2 => true, + 1 => true, + true, true, true, true, @@ -694,7 +713,8 @@ public function __construct() 11 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -703,7 +723,8 @@ public function __construct() 25 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -715,7 +736,8 @@ public function __construct() ], null, [ - 2 => true, + 1 => true, + true, true, true, true, @@ -736,7 +758,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -804,7 +827,8 @@ public function __construct() 13 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -831,7 +855,8 @@ public function __construct() 6 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -866,7 +891,8 @@ public function __construct() 13 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -875,7 +901,8 @@ public function __construct() ], null, [ - 2 => true, + 1 => true, + true, true, true, true, @@ -884,7 +911,8 @@ public function __construct() 24 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -909,7 +937,8 @@ public function __construct() ], null, [ - 2 => true, + 1 => true, + true, true, true, true, @@ -930,7 +959,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -951,7 +981,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -972,7 +1003,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -991,7 +1023,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -1010,7 +1043,8 @@ public function __construct() true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -1028,7 +1062,8 @@ public function __construct() 15 => true, ], [ - 2 => true, + 1 => true, + true, true, true, true, @@ -1092,17 +1127,14 @@ public function __construct() null, [ 1 => true, - true, ], [ 1 => true, - true, ], + null, [ 1 => true, - ], - [ - 2 => true, + true, true, true, true, @@ -1170,6 +1202,7 @@ public function __construct() false, false, false, + false, true, false, false, @@ -1265,14 +1298,17 @@ public function __construct() false, false, false, - false, + true, false, false, true, ], choicePrediction: [ [ - 2 => [ + 1 => [ + 1, + ], + [ 1, ], [ @@ -1324,13 +1360,16 @@ public function __construct() 1, ], 27 => [ - 140, + 141, ], ], 4 => [ 15 => [ 5, ], + 1 => [ + 7, + ], 2 => [ 7, ], @@ -1423,6 +1462,9 @@ public function __construct() [ 13, ], + 1 => [ + 27, + ], 2 => [ 27, ], @@ -1523,7 +1565,10 @@ public function __construct() ], ], 28 => [ - 2 => [ + 1 => [ + 29, + ], + [ 29, ], [ @@ -1545,30 +1590,33 @@ public function __construct() 29, ], 25 => [ - 51, + 52, ], ], 30 => [ 8 => [ 31, ], + 1 => [ + 43, + ], 2 => [ - 42, + 43, ], 3 => [ - 42, + 43, ], 4 => [ - 42, + 43, ], 5 => [ - 42, + 43, ], 6 => [ - 42, + 43, ], 7 => [ - 42, + 43, ], ], 33 => [ @@ -1578,131 +1626,143 @@ public function __construct() 6 => [ 35, ], - 2 => [ + 1 => [ 36, ], - 3 => [ + 2 => [ 37, ], - 4 => [ + 3 => [ 38, ], - 5 => [ + 4 => [ 39, ], + 5 => [ + 40, + ], ], - 44 => [ + 45 => [ 25 => [ - 45, + 46, ], 8 => [ - 50, + 51, ], ], - 54 => [ + 55 => [ 11 => [ - 55, + 56, ], 18 => [ - 59, + 60, ], 13 => [ - 80, - 91, + 81, + 92, ], 16 => [ - 101, + 102, ], ], - 57 => [ - 2 => [ - 58, + 58 => [ + 1 => [ + 59, + ], + [ + 59, ], [ - 58, + 59, ], [ - 58, + 59, ], [ - 58, + 59, ], [ - 58, + 59, ], [ - 58, + 59, ], 25 => [ - 45, + 46, ], ], - 68 => [ + 69 => [ 27 => [ - 69, + 70, ], 26 => [ - 70, + 71, ], ], - 83 => [ - 2 => [ - 86, + 84 => [ + 1 => [ + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, + ], + [ + 87, ], 7 => [ - 86, + 87, ], [ - 86, + 87, ], 15 => [ - 86, + 87, ], 18 => [ - 86, + 87, ], 25 => [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], [ - 86, + 87, ], ], - 86 => [ - 2 => [ + 87 => [ + 1 => [ + 0, + ], + [ 0, ], [ @@ -1757,38 +1817,41 @@ public function __construct() 0, ], ], - 95 => [ - 2 => [ - 96, + 96 => [ + 1 => [ + 97, ], [ - 96, + 97, ], [ - 96, + 97, ], [ - 96, + 97, ], [ - 96, + 97, ], [ - 96, + 97, ], - 24 => [ + [ 97, ], + 24 => [ + 98, + ], ], - 106 => [ + 107 => [ 15 => [ - 115, + 116, ], 27 => [ - 115, + 116, ], ], - 108 => [ + 109 => [ 8 => [ 8, ], @@ -1823,16 +1886,11 @@ public function __construct() 8, ], ], - 137 => [ - 2 => [ - 36, - ], + 140 => [ 1 => [ - 138, + 0, ], - ], - 139 => [ - 2 => [ + [ 0, ], [ @@ -1887,8 +1945,8 @@ public function __construct() ], expectations: [ 'T_WHITESPACE', - 'T_NEQ', 'T_EQ', + 'T_NOT', 'T_TRUE_LITERAL', 'T_FALSE_LITERAL', 'T_NULL_LITERAL', @@ -1946,22 +2004,9 @@ private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); } - $condition = match ($children[1]->id) { - self::T_EQ => new Type\Condition\EqualConditionNode( - $children[0], - $children[2], - $offset, - ), - self::T_NEQ => new Type\Condition\NotEqualConditionNode( - $children[0], - $children[2], - $offset, - ), - default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( - $children[1]->value, - $offset, - ), - }; + $condition = $children[1] + ? new Type\Condition\NotEqualConditionNode($children[0], $children[2], $offset) + : new Type\Condition\EqualConditionNode($children[0], $children[2], $offset); return new Type\TernaryExpressionNode( $condition, @@ -2425,7 +2470,7 @@ private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mix throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($limit->operator->offset); } - switch ($limit->operator->toLowerString()) { + switch ($limit->operator->value) { case 'of': case 'as': if ($upper !== null) { @@ -2581,4 +2626,9 @@ private static function reduceListOrOffsetSuffix(\Phplrt\Parser\Context $ctx, mi { return $children[0] ?? true; } + + private static function reduceTernaryExpressionOperator(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return \count($children) === 2; + } } From 6437079841a2e637eff31561a8e18a295088cf17 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:25:20 +0300 Subject: [PATCH 081/119] Improve string parser tests --- .../tests/Internal/SequenceFetcherTest.php | 162 ------------------ .../tests/Internal/StringParserTest.php | 56 +++++- 2 files changed, 49 insertions(+), 169 deletions(-) delete mode 100644 libs/parser/tests/Internal/SequenceFetcherTest.php diff --git a/libs/parser/tests/Internal/SequenceFetcherTest.php b/libs/parser/tests/Internal/SequenceFetcherTest.php deleted file mode 100644 index 4c08a59..0000000 --- a/libs/parser/tests/Internal/SequenceFetcherTest.php +++ /dev/null @@ -1,162 +0,0 @@ -}> - */ - public static function provideFetchers(): iterable - { - yield 'scanner' => [ScannerSequenceFetcher::class]; - yield 'pattern' => [PatternSequenceFetcher::class]; - } - - /** - * @return iterable - */ - public static function provideBodies(): iterable - { - yield 'empty' => ['']; - yield 'without sequences' => ['example']; - yield 'constant sequences' => ['a\nb\rc\td\ve\ff\$g\"h']; - yield 'backslash' => ['a\\\\b']; - yield 'backslash before escape sequence' => ['a\\\\nb']; - yield 'backslash before hexadecimal' => ['a\\\\x41b']; - yield 'hexadecimal' => ['\x41']; - yield 'uppercase hexadecimal prefix' => ['\X41']; - yield 'short hexadecimal' => ['\x9']; - yield 'null byte' => ['\x00']; - yield 'octal' => ['\101']; - yield 'octal null byte' => ['\0']; - yield 'octal overflow' => ['\777']; - yield 'unicode' => ['\u{48}']; - yield 'multibyte unicode' => ['\u{1F600}']; - yield 'last unicode code point' => ['\u{10FFFF}']; - yield 'code point above the unicode range' => ['\u{110000}']; - yield 'unknown sequence' => ['a\qb']; - yield 'incomplete unicode' => ['a\u{}b']; - yield 'incomplete hexadecimal' => ['a\xzb']; - yield 'trailing backslash' => ['a\\']; - yield 'repeated sequences' => ['\x41\x41\u{42}\u{42}\101\101']; - yield 'many sequences' => ['\x41\x42\u{43}\104\x45\u{46}\107\x48']; - yield 'raw null byte' => ["a\0b"]; - yield 'invalid utf8' => ["\xFF\xFE"]; - yield 'all kinds at once' => ['\x41\u{42}\103\n\\\\']; - } - - #[Test] - #[DataProvider('provideFetchers')] - public function fetcherImplementsTheContract(string $fetcher): void - { - self::assertTrue(\is_subclass_of($fetcher, SequenceFetcherInterface::class)); - } - - #[Test] - #[DataProvider('provideBodies')] - public function everyScannedSequenceIsAlsoMatchedByThePattern(string $body): void - { - $pattern = PatternSequenceFetcher::fetch($body); - - foreach (ScannerSequenceFetcher::fetch($body) as $sequence => $replacement) { - self::assertSame($replacement, $pattern[$sequence] ?? null, \sprintf( - 'A "%s" sequence replacement must be the same in both fetchers', - $sequence, - )); - } - } - - /** - * The scanner skips an escaped backslash together with the char behind it, - * so a sequence that is not applied does not get into the map at all. The - * pattern based one collects it, but {@see strtr()} never applies it: The - * "\\" replacement wins on that position because of being matched first. - */ - #[Test] - public function scannerSkipsSequencesBehindAnEscapedBackslash(): void - { - $body = 'a\\\\x41b'; - - self::assertArrayNotHasKey('\x41', ScannerSequenceFetcher::fetch($body)); - self::assertArrayHasKey('\x41', PatternSequenceFetcher::fetch($body)); - - self::assertSame('a\x41b', StringDecoder::decode($body)); - } - - #[Test] - #[DataProvider('provideBodies')] - public function bothFetchersDecodeTheBodyIdentically(string $body): void - { - $scanner = \strtr($body, ScannerSequenceFetcher::fetch($body)); - $pattern = \strtr($body, PatternSequenceFetcher::fetch($body)); - - self::assertSame($scanner, $pattern); - self::assertSame($scanner, StringDecoder::decode($body)); - } - - #[Test] - #[DataProvider('provideBodies')] - public function scannerAlwaysContainsConstantSequences(string $body): void - { - $result = ScannerSequenceFetcher::fetch($body); - - foreach (SequenceFetcherInterface::ESCAPED_CHARS as $sequence => $replacement) { - self::assertSame($replacement, $result[$sequence] ?? null); - } - } - - #[Test] - #[DataProvider('provideBodies')] - public function patternAlwaysContainsConstantSequences(string $body): void - { - $result = PatternSequenceFetcher::fetch($body); - - foreach (SequenceFetcherInterface::ESCAPED_CHARS as $sequence => $replacement) { - self::assertSame($replacement, $result[$sequence] ?? null); - } - } - - #[Test] - public function scannerFetchesNumericSequences(): void - { - $result = ScannerSequenceFetcher::fetch('\x41\u{42}\103'); - - self::assertSame('A', $result['\x41'] ?? null); - self::assertSame('B', $result['\u{42}'] ?? null); - self::assertSame('C', $result['\103'] ?? null); - } - - #[Test] - public function patternFetchesNumericSequences(): void - { - $result = PatternSequenceFetcher::fetch('\x41\u{42}\103'); - - self::assertSame('A', $result['\x41'] ?? null); - self::assertSame('B', $result['\u{42}'] ?? null); - self::assertSame('C', $result['\103'] ?? null); - } - - #[Test] - #[DataProvider('provideFetchers')] - public function fetcherReturnsOnlyConstantSequencesForAPlainString(string $fetcher): void - { - self::assertSame(SequenceFetcherInterface::ESCAPED_CHARS, $fetcher::fetch('example')); - } - - #[Test] - public function escapedCharsContainTheBackslashItself(): void - { - self::assertSame('\\', SequenceFetcherInterface::ESCAPED_CHARS['\\\\'] ?? null); - } -} diff --git a/libs/parser/tests/Internal/StringParserTest.php b/libs/parser/tests/Internal/StringParserTest.php index 9a78ee9..d3af246 100644 --- a/libs/parser/tests/Internal/StringParserTest.php +++ b/libs/parser/tests/Internal/StringParserTest.php @@ -25,7 +25,8 @@ public static function provideBodies(): iterable yield 'escape' => ['a\eb', "a\eb"]; yield 'form feed' => ['a\fb', "a\fb"]; yield 'dollar sign' => ['a\$b', 'a$b']; - yield 'double quote' => ['a\"b', 'a"b']; + yield 'double quote' => ['a"b', 'a"b']; + yield 'double quote behind an escaped backslash' => ['a\\\\"b', 'a\\"b']; yield 'backslash' => ['a\\\\b', 'a\\b']; yield 'backslash before escape sequence' => ['a\\\\nb', 'a\nb']; yield 'two backslashes' => ['a\\\\\\\\b', 'a\\\\b']; @@ -56,27 +57,68 @@ public static function provideBodies(): iterable #[DataProvider('provideBodies')] public function bodyIsDecoded(string $body, string $expected): void { - self::assertSame($expected, StringDecoder::decode($body)); + self::assertSame($expected, StringDecoder::decode($body, true)); } #[Test] public function rawNullByteIsNotTouched(): void { - self::assertSame("a\0b", StringDecoder::decode("a\0b")); - self::assertSame("a\0\\b", StringDecoder::decode("a\0" . '\\\\b')); + self::assertSame("a\0b", StringDecoder::decode("a\0b", true)); + self::assertSame("a\0\\b", StringDecoder::decode("a\0" . '\\\\b', true)); } #[Test] public function invalidUtf8IsNotTouched(): void { - self::assertSame("\xFF\xFE", StringDecoder::decode("\xFF\xFE")); + self::assertSame("\xFF\xFE", StringDecoder::decode("\xFF\xFE", true)); } #[Test] public function decodingIsIdempotentForStringsWithoutBackslashes(): void { - $decoded = StringDecoder::decode('a\nb'); + $decoded = StringDecoder::decode('a\nb', true); - self::assertSame($decoded, StringDecoder::decode($decoded)); + self::assertSame($decoded, StringDecoder::decode($decoded, true)); + } + + /** + * @return iterable + */ + public static function provideDoubleQuotedStrings(): iterable + { + yield 'empty' => ['""', '']; + yield 'without sequences' => ['"example"', 'example']; + yield 'quote' => ['"a\"b"', 'a"b']; + yield 'sequence is left alone' => ['"a\nb"', 'a\nb']; + yield 'backslash is left alone' => ['"a\\\\b"', 'a\\\\b']; + yield 'quote behind an escaped backslash' => ['"a\\\\\"b"', 'a\\\\"b']; + yield 'quote beside a sequence' => ['"asd\"\nasd"', 'asd"\nasd']; + } + + #[Test] + #[DataProvider('provideDoubleQuotedStrings')] + public function doubleQuotedStringIsUnpacked(string $value, string $expected): void + { + self::assertSame($expected, StringDecoder::unpack($value, true)); + } + + /** + * @return iterable + */ + public static function provideSingleQuotedStrings(): iterable + { + yield 'empty' => ["''", '']; + yield 'without sequences' => ["'example'", 'example']; + yield 'quote' => ["'a\\'b'", "a'b"]; + yield 'backslash is left alone' => ["'a\\\\b'", 'a\\\\b']; + yield 'sequence is left alone' => ["'a\\nb'", 'a\nb']; + yield 'quote behind an escaped backslash' => ["'a\\\\\\'b'", "a\\\\'b"]; + } + + #[Test] + #[DataProvider('provideSingleQuotedStrings')] + public function singleQuotedStringIsUnpacked(string $value, string $expected): void + { + self::assertSame($expected, StringDecoder::unpack($value, false)); } } From e7044e66c7c255a943520b2475d2763200b35e4a Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:25:32 +0300 Subject: [PATCH 082/119] Add more string + int literal tests --- libs/parser/tests/Literal/IntLiteralTest.php | 2 ++ libs/parser/tests/Literal/StringLiteralTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/libs/parser/tests/Literal/IntLiteralTest.php b/libs/parser/tests/Literal/IntLiteralTest.php index b8389a9..43a01c4 100644 --- a/libs/parser/tests/Literal/IntLiteralTest.php +++ b/libs/parser/tests/Literal/IntLiteralTest.php @@ -35,6 +35,8 @@ public static function provideIntegers(): iterable yield 'zero' => ['0', 0, '0']; yield 'decimal' => ['42', 42, '42']; yield 'negative decimal' => ['-42', -42, '-42']; + yield 'signed decimal' => ['+42', 42, '42']; + yield 'signed hexadecimal' => ['+0x1F', 31, '31']; yield 'hexadecimal' => ['0x1F', 31, '31']; yield 'uppercase hexadecimal prefix' => ['0X1F', 31, '31']; yield 'negative hexadecimal' => ['-0x10', -16, '-16']; diff --git a/libs/parser/tests/Literal/StringLiteralTest.php b/libs/parser/tests/Literal/StringLiteralTest.php index 789b48d..4ae83d3 100644 --- a/libs/parser/tests/Literal/StringLiteralTest.php +++ b/libs/parser/tests/Literal/StringLiteralTest.php @@ -86,6 +86,8 @@ public static function provideSingleQuotedStrings(): iterable yield 'plain' => ["'example'", 'example']; yield 'escaped quote' => ["'a\'b'", "a'b"]; yield 'escaped backslash' => ["'a\\\\b'", 'a\\b']; + yield 'two escaped backslashes' => ["'a\\\\\\\\b'", 'a\\\\b']; + yield 'escaped quote behind an escaped backslash' => ["'a\\\\\\'b'", "a\\'b"]; yield 'escape sequences are not decoded' => ["'a\\nb'", 'a\\nb']; yield 'hexadecimal sequence is not decoded' => ["'a\\x41b'", 'a\\x41b']; yield 'dollar sign is not decoded' => ["'a\$b'", 'a$b']; From 803528a5a1d7d81f10019829075b1533896e8633 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:25:47 +0300 Subject: [PATCH 083/119] Improve type map tests --- .../tests/Traverser/TypeMapVisitorTest.php | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/libs/parser/tests/Traverser/TypeMapVisitorTest.php b/libs/parser/tests/Traverser/TypeMapVisitorTest.php index 834b41d..e1272a9 100644 --- a/libs/parser/tests/Traverser/TypeMapVisitorTest.php +++ b/libs/parser/tests/Traverser/TypeMapVisitorTest.php @@ -13,9 +13,11 @@ use TypeLang\Type\ClassConstNode; use TypeLang\Type\ConstMaskNode; use TypeLang\Type\Identifier; +use TypeLang\Type\MaskNode; use TypeLang\Type\Name; use TypeLang\Type\NamedTypeNode; use TypeLang\Type\UnionTypeNode; +use TypeLang\Type\WildcardNode; final class TypeMapVisitorTest extends TestCase { @@ -45,13 +47,27 @@ public function theNameOfACallableTypeIsTransformed(): void } #[Test] - public function theNameOfAConstMaskIsTransformed(): void + public function theNamespaceOfAConstMaskIsTransformed(): void { - $node = new ConstMaskNode(Name::createFromString('SOME_*')); + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + Name::createFromString('Vendor'), + ); Traverser::new([$this->alias()])->traverse([$node]); - self::assertSame('Aliased', $node->name->toString()); + self::assertInstanceOf(Name::class, $node->namespaceOrFullyQualified); + self::assertSame('Aliased', $node->namespaceOrFullyQualified->toString()); + } + + #[Test] + public function aConstMaskWithoutANamespaceIsLeftAlone(): void + { + $node = new ConstMaskNode(new MaskNode([new WildcardNode(), new Identifier('_SOME')])); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertFalse($node->namespaceOrFullyQualified); } #[Test] From 4eb9060d8a2880dfb0cd79ef08e26ad2f5cd9234 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:25:56 +0300 Subject: [PATCH 084/119] Fix type resolver tests --- libs/parser/tests/TypeResolver/TypeResolverTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/parser/tests/TypeResolver/TypeResolverTest.php b/libs/parser/tests/TypeResolver/TypeResolverTest.php index acd1ffa..207607a 100644 --- a/libs/parser/tests/TypeResolver/TypeResolverTest.php +++ b/libs/parser/tests/TypeResolver/TypeResolverTest.php @@ -288,6 +288,8 @@ public function testResolvesClassInClassConstantMask(): void self::assertSame(<<<'AST' ClassConstMaskNode Name(App\Node) + MaskNode(*) + WildcardNode(*) AST, $this->print($resolver->resolve($this->parse('Node::*')))); } From 62c0550ce05f0b4e0ab0e350c19fc25ff02b9f6f Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 01:26:05 +0300 Subject: [PATCH 085/119] Fix syntax tests --- libs/parser/tests/Syntax/CallableTest.php | 12 +- libs/parser/tests/Syntax/ConditionalTest.php | 33 +- libs/parser/tests/Syntax/ConstantTest.php | 147 +++++++- libs/parser/tests/Syntax/FloatLiteralTest.php | 33 ++ libs/parser/tests/Syntax/GenericTest.php | 13 +- libs/parser/tests/Syntax/GroupingTest.php | 322 ++++++++++++++++++ libs/parser/tests/Syntax/IntLiteralTest.php | 56 +++ .../tests/Syntax/MalformedInputTest.php | 175 ++++++++++ libs/parser/tests/Syntax/NameTest.php | 59 +++- libs/parser/tests/Syntax/OffsetAccessTest.php | 11 + libs/parser/tests/Syntax/ShapeTest.php | 64 +++- .../tests/Syntax/TemplateParameterTest.php | 262 ++++++++++++++ libs/parser/tests/Syntax/TriviaTest.php | 254 ++++++++++++++ libs/parser/tests/Syntax/VariableTest.php | 234 +++++++++++++ libs/parser/tests/Syntax/WildcardTest.php | 98 ++++++ 15 files changed, 1747 insertions(+), 26 deletions(-) create mode 100644 libs/parser/tests/Syntax/GroupingTest.php create mode 100644 libs/parser/tests/Syntax/MalformedInputTest.php create mode 100644 libs/parser/tests/Syntax/TemplateParameterTest.php create mode 100644 libs/parser/tests/Syntax/TriviaTest.php create mode 100644 libs/parser/tests/Syntax/VariableTest.php create mode 100644 libs/parser/tests/Syntax/WildcardTest.php diff --git a/libs/parser/tests/Syntax/CallableTest.php b/libs/parser/tests/Syntax/CallableTest.php index d5aa385..c7a137e 100644 --- a/libs/parser/tests/Syntax/CallableTest.php +++ b/libs/parser/tests/Syntax/CallableTest.php @@ -74,7 +74,8 @@ public function testNamedParameter(): void Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Literal\VariableLiteralNode($name) + VariableNode + Identifier(name) AST, $this->parseAndPrint('foo(T $name)')); } @@ -87,7 +88,8 @@ public function testMixedNamedAndAnonymousParameters(): void Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(A) - Literal\VariableLiteralNode($a) + VariableNode + Identifier(a) Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(B) @@ -118,7 +120,8 @@ public function testOutputNamedParameter(): void Callable\CallableParameterNode(isOutput=true, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Literal\VariableLiteralNode($name) + VariableNode + Identifier(name) AST, $this->parseAndPrint('foo(T &$name)')); } @@ -201,7 +204,8 @@ public function testReferenceAndVariadicMarkersOfATypedParameterAreOrdered(): vo Callable\CallableParameterNode(isOutput=true, isVariadic=true, isOptional=false) NamedTypeNode Name(T) - Literal\VariableLiteralNode($name) + VariableNode + Identifier(name) AST, $this->parseAndPrint('foo(T &...$name)')); } diff --git a/libs/parser/tests/Syntax/ConditionalTest.php b/libs/parser/tests/Syntax/ConditionalTest.php index 9aee273..f11a7cf 100644 --- a/libs/parser/tests/Syntax/ConditionalTest.php +++ b/libs/parser/tests/Syntax/ConditionalTest.php @@ -20,10 +20,6 @@ public static function conditionDataProvider(): iterable { yield 'is' => ['A is B ? C : D', 'EqualConditionNode']; yield 'is not' => ['A is not B ? C : D', 'NotEqualConditionNode']; - yield 'less than' => ['A < B ? C : D', 'LessThanConditionNode']; - yield 'greater than' => ['A > B ? C : D', 'GreaterThanConditionNode']; - yield 'less or equal' => ['A <= B ? C : D', 'LessThanOrEqualConditionNode']; - yield 'greater or equal' => ['A >= B ? C : D', 'GreaterThanOrEqualConditionNode']; } #[DataProvider('conditionDataProvider')] @@ -48,7 +44,8 @@ public function testConditionalWithVariableSubject(): void self::assertSame(<<<'AST' TernaryExpressionNode Condition\EqualConditionNode - Literal\VariableLiteralNode($T) + VariableNode + Identifier(T) NamedTypeNode Name(B) NamedTypeNode @@ -58,6 +55,32 @@ public function testConditionalWithVariableSubject(): void AST, $this->parseAndPrint('$T is B ? C : D')); } + /** + * A variable stands on either side of the operator, and on both at once. + * + * @return iterable + */ + public static function variableOperandDataProvider(): iterable + { + yield 'on the left' => ['($T is B ? C : D)']; + yield 'on the right' => ['(A is $T ? C : D)']; + yield 'on both sides' => ['($A is $B ? C : D)']; + yield 'this on the left' => ['($this is B ? C : D)']; + yield 'this on the right' => ['(A is $this ? C : D)']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('variableOperandDataProvider')] + public function testVariableStandsAsAnOperand(string $type): void + { + $printer = new \TypeLang\Printer\PrettyTypePrinter(); + + self::assertSame($type, $printer->print($this->parse($type))); + } + public function testEqualityOperatorIsNotAllowed(): void { $this->expectParsingException('unexpected "="'); diff --git a/libs/parser/tests/Syntax/ConstantTest.php b/libs/parser/tests/Syntax/ConstantTest.php index 0d83420..749a7d5 100644 --- a/libs/parser/tests/Syntax/ConstantTest.php +++ b/libs/parser/tests/Syntax/ConstantTest.php @@ -4,7 +4,9 @@ namespace TypeLang\Parser\Tests\Syntax; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use TypeLang\Type\ConstMaskNode; /** * Tests for the constant grammar: global constants, class constants and masks. @@ -49,17 +51,52 @@ public function testNamespacedClassConstant(): void public function testGlobalConstantMask(): void { self::assertSame(<<<'AST' - ConstMaskNode - Name(JSON_) + ConstMaskNode(namespaceOrFullyQualified=false) + MaskNode(JSON_*) + Identifier(JSON_) + WildcardNode(*) AST, $this->parseAndPrint('JSON_*')); } + public function testGlobalConstantMaskInANamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(JSON_*) + Identifier(JSON_) + WildcardNode(*) + Name(Path\To) + AST, $this->parseAndPrint('Path\\To\\JSON_*')); + } + + public function testGlobalConstantMaskMayBeginWithAnAsterisk(): void + { + self::assertSame(<<<'AST' + ConstMaskNode(namespaceOrFullyQualified=false) + MaskNode(*_SUFFIX) + WildcardNode(*) + Identifier(_SUFFIX) + AST, $this->parseAndPrint('*_SUFFIX')); + } + + public function testGlobalConstantMaskOfAWholeNamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(*) + WildcardNode(*) + Name(Path\To) + AST, $this->parseAndPrint('Path\\To\\*')); + } + public function testClassConstantMask(): void { self::assertSame(<<<'AST' ClassConstMaskNode Name(Path\To\ClassName) - Identifier(PREFIX_) + MaskNode(PREFIX_*) + Identifier(PREFIX_) + WildcardNode(*) AST, $this->parseAndPrint('Path\\To\\ClassName::PREFIX_*')); } @@ -68,6 +105,8 @@ public function testClassConstantMaskWithoutPrefix(): void self::assertSame(<<<'AST' ClassConstMaskNode Name(Path\To\ClassName) + MaskNode(*) + WildcardNode(*) AST, $this->parseAndPrint('Path\\To\\ClassName::*')); } @@ -78,17 +117,109 @@ public function testClassConstantCannotContainNamespace(): void $this->parse('ClassName::SOME\\ANY'); } - public function testGlobalConstantMaskCannotOmitPrefix(): void + /** + * A mask standing for every constant there is says nothing worth saying, + * so at least one segment of a name is required. + */ + public function testGlobalConstantMaskCannotBeAnAsteriskAlone(): void { - $this->expectParsingException('unexpected "*"'); + $this->expectParsingException('unexpected end of input'); $this->parse('*'); } - public function testAsteriskMustBeTheFinalCharacter(): void + public function testMaskIsMadeOfEverySegmentItIsWrittenOf(): void + { + self::assertSame(<<<'AST' + ClassConstMaskNode + Name(Path\To\ClassName) + MaskNode(PREFIX_*_SUFFIX) + Identifier(PREFIX_) + WildcardNode(*) + Identifier(_SUFFIX) + AST, $this->parseAndPrint('Path\\To\\ClassName::PREFIX_*_SUFFIX')); + } + + public function testMaskMayBeginWithAnAsterisk(): void { - $this->expectParsingException('unexpected "_SUFFIX"'); + self::assertSame(<<<'AST' + ClassConstMaskNode + Name(Path\To\ClassName) + MaskNode(*_SUFFIX) + WildcardNode(*) + Identifier(_SUFFIX) + AST, $this->parseAndPrint('Path\\To\\ClassName::*_SUFFIX')); + } + + /** + * A mask written with no namespace carries the leading separator itself, + * since there is no name for it to belong to. + */ + public function testFullyQualifiedGlobalConstantMask(): void + { + self::assertSame(<<<'AST' + ConstMaskNode(namespaceOrFullyQualified=true) + MaskNode(JSON_*) + Identifier(JSON_) + WildcardNode(*) + AST, $this->parseAndPrint('\JSON_*')); + } + + /** + * The namespace of a mask keeps the separator it is written with, so the + * name is the one that says the reference is a fully qualified one. + */ + public function testFullyQualifiedGlobalConstantMaskInANamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(JSON_*) + Identifier(JSON_) + WildcardNode(*) + Name(\Path\To) + AST, $this->parseAndPrint('\Path\To\JSON_*')); + } + + public function testFullyQualifiedGlobalConstantMaskOfAWholeNamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(*) + WildcardNode(*) + Name(\Path\To) + AST, $this->parseAndPrint('\Path\To\*')); + } + + /** + * @return iterable + */ + public static function maskQualificationDataProvider(): iterable + { + yield 'a mask alone' => ['JSON_*', false]; + yield 'a mask alone behind a separator' => ['\JSON_*', true]; + yield 'a mask in a namespace' => ['Path\To\JSON_*', false]; + yield 'a mask in a namespace behind a separator' => ['\Path\To\JSON_*', true]; + yield 'a namespace whole' => ['Path\To\*', false]; + yield 'a namespace whole behind a separator' => ['\Path\To\*', true]; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('maskQualificationDataProvider')] + public function testAMaskSaysWhetherItIsFullyQualified(string $type, bool $expected): void + { + $node = $this->parse($type); + + self::assertInstanceOf(ConstMaskNode::class, $node); + self::assertSame($expected, $node->isFullyQualified()); + self::assertSame($type, (new \TypeLang\Printer\PrettyTypePrinter())->print($node)); + } + public function testTwoAsterisksInARowAreNotAMask(): void + { + $this->expectParsingException('unexpected "*"'); - $this->parse('Path\\To\\ClassName::PREFIX_*_SUFFIX'); + $this->parse('Path\\To\\ClassName::PREFIX_**'); } } diff --git a/libs/parser/tests/Syntax/FloatLiteralTest.php b/libs/parser/tests/Syntax/FloatLiteralTest.php index 8545dcc..4cc41b7 100644 --- a/libs/parser/tests/Syntax/FloatLiteralTest.php +++ b/libs/parser/tests/Syntax/FloatLiteralTest.php @@ -23,10 +23,43 @@ public static function validFloatDataProvider(): iterable yield 'non-prefixed' => ['.9', 0.9]; yield 'non-suffixed' => ['1.', 1.0]; yield 'negative' => ['-0.9', -0.9]; + yield 'signed' => ['+0.9', 0.9]; + yield 'signed non-prefixed' => ['+.9', 0.9]; yield 'scientific' => ['10e2', 1000.0]; yield 'scientific uppercase' => ['10E2', 1000.0]; yield 'scientific negative exponent' => ['10e-2', 0.1]; + yield 'scientific signed exponent' => ['10e+2', 1000.0]; + yield 'signed scientific' => ['+10e2', 1000.0]; + yield 'signed scientific with signed exponent' => ['+1.5e+3', 1500.0]; + + yield 'separated integer part' => ['4_4.', 44.0]; + yield 'separated fraction part' => ['.4_2', 0.42]; + yield 'separated exponent' => ['42e2_3', 42e23]; + yield 'separated signed exponent' => ['42e+2_3', 42e23]; + yield 'separated in every part' => ['2_3.4_5e-6_7', 23.45e-67]; + } + + /** + * A separator is only allowed between two digits. + * + * @return iterable + */ + public static function invalidSeparatorDataProvider(): iterable + { + yield 'before the dot' => ['1_.5', 'unexpected "_"']; + yield 'after the dot' => ['1._5', 'unexpected "_5"']; + yield 'before the exponent' => ['1.5_e3', 'unexpected "_e3"']; + yield 'inside the exponent' => ['1.5e_3', 'unexpected "e_3"']; + yield 'trailing' => ['1e3_', 'unexpected "_"']; + } + + #[DataProvider('invalidSeparatorDataProvider')] + public function testSeparatorBelongsBetweenDigits(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); } #[DataProvider('validFloatDataProvider')] diff --git a/libs/parser/tests/Syntax/GenericTest.php b/libs/parser/tests/Syntax/GenericTest.php index c6e4efd..9a7f5a2 100644 --- a/libs/parser/tests/Syntax/GenericTest.php +++ b/libs/parser/tests/Syntax/GenericTest.php @@ -89,9 +89,9 @@ public function testCallSiteHint(): void NamedTypeNode Name(array-key) Template\TemplateArgumentNode - Identifier(covariant) NamedTypeNode Name(Request) + Identifier(covariant) AST, $this->parseAndPrint('HashMap')); } @@ -124,9 +124,9 @@ public function testHintFollowedBySpaceAndFullyQualifiedName(): void Name(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode - Identifier(out) NamedTypeNode Name(\Some) + Identifier(out) AST, $this->parseAndPrint('Type')); } @@ -157,9 +157,9 @@ public function testHintFollowedBySpaceAndRelativeName(): void Name(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode - Identifier(out) NamedTypeNode Name(Some) + Identifier(out) AST, $this->parseAndPrint('Type')); } @@ -184,9 +184,14 @@ public function testHintAllowsOnlyIdentifiers(): void $this->parse('Collection<42 User>'); } + /** + * A second hint reads as the bound of a template parameter, and a + * parameter list belongs to a callable, so the statement is refused + * where the parenthesis it would go on with is missing. + */ public function testMultipleHintsAreNotAllowed(): void { - $this->expectParsingException('unexpected "Request"'); + $this->expectParsingException('unexpected end of input'); $this->parse('HashMap'); } diff --git a/libs/parser/tests/Syntax/GroupingTest.php b/libs/parser/tests/Syntax/GroupingTest.php new file mode 100644 index 0000000..ecfc5fe --- /dev/null +++ b/libs/parser/tests/Syntax/GroupingTest.php @@ -0,0 +1,322 @@ +parseAndPrint('int&string|float')); + } + + public function testParenthesesTurnAUnionIntoAMemberOfAnIntersection(): void + { + self::assertSame(<<<'AST' + IntersectionTypeNode + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + AST, $this->parseAndPrint('(A|B)&C')); + } + + /** + * @return iterable + */ + public static function parenthesizedNameDataProvider(): iterable + { + yield 'a single pair' => ['(A)']; + yield 'a pair of pairs' => ['((A))']; + yield 'spaces inside' => ['( A )']; + } + + /** + * Parentheses group and nothing else, so a pair that groups a single type + * reaches no tree. + * + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('parenthesizedNameDataProvider')] + public function testParenthesesAroundASingleTypeAreOfNoMeaning(string $type): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(A) + AST, $this->parseAndPrint($type)); + } + + public function testTheQuestionMarkReachesOverAWholeGroup(): void + { + self::assertSame(<<<'AST' + NullableTypeNode + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + AST, $this->parseAndPrint('?(A|B)')); + } + + /** + * A "[]" is read before the "?" in front of it, so the list is the one + * made nullable and not its element. + */ + public function testTheQuestionMarkReachesOverAListAndNotOverItsElement(): void + { + self::assertSame(<<<'AST' + NullableTypeNode + TypesListNode + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('?int[]')); + } + + public function testAListIsMadeOfAWholeGroup(): void + { + self::assertSame(<<<'AST' + TypesListNode + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + AST, $this->parseAndPrint('(A|B)[]')); + } + + public function testListsNestFromTheInsideOut(): void + { + self::assertSame(<<<'AST' + TypesListNode + TypesListNode + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('int[][]')); + } + + public function testAListFollowsAnOffsetItIsWrittenBehind(): void + { + self::assertSame(<<<'AST' + TypesListNode + TypeOffsetAccessNode + NamedTypeNode + Name(K) + NamedTypeNode + Name(T) + AST, $this->parseAndPrint('T[K][]')); + + $node = $this->parse('T[K][]'); + + self::assertInstanceOf(TypesListNode::class, $node); + self::assertInstanceOf(TypeOffsetAccessNode::class, $node->type); + self::assertInstanceOf(NamedTypeNode::class, $node->type->access); + self::assertSame('K', $node->type->access->name->toString()); + } + + /** + * @return iterable + */ + public static function listedPrimaryTypeDataProvider(): iterable + { + yield 'a generic type' => ['list[]']; + yield 'a shape' => ['array{a: int}[]']; + yield 'a class constant' => ['T::CONST[]']; + yield 'a constant mask' => ['JSON_*[]']; + yield 'a literal' => ["'a'[]"]; + yield 'a group' => ['(int)[]']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('listedPrimaryTypeDataProvider')] + public function testEveryPrimaryTypeCarriesAList(string $type): void + { + self::assertStringStartsWith('TypesListNode', $this->parseAndPrint($type)); + } + + public function testCallablesNestInTheirParameters(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable(callable(int): string): void')); + } + + public function testACallableReturnsACallable(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable(): callable(): void')); + } + + /** + * The branch a condition chooses is a type whole, so a condition of its + * own stands there as well. + */ + public function testAConditionNestsInTheBranchOfAnother(): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\EqualConditionNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + TernaryExpressionNode + Condition\EqualConditionNode + NamedTypeNode + Name(C) + NamedTypeNode + Name(D) + NamedTypeNode + Name(E) + NamedTypeNode + Name(F) + NamedTypeNode + Name(G) + AST, $this->parseAndPrint('A is B ? C is D ? E : F : G')); + } + + public function testAUnionReachesIntoTheBranchOfACondition(): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\EqualConditionNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + UnionTypeNode + NamedTypeNode + Name(D) + NamedTypeNode + Name(E) + AST, $this->parseAndPrint('A is B ? C : D|E')); + } + + public function testTheSubjectOfAConditionIsAWholeUnion(): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\EqualConditionNode + UnionTypeNode + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + AST, $this->parseAndPrint('int|string is A ? B : C')); + } + + public function testShapesNestInOneAnother(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(b) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('array{a: array{b: int}}')); + } + + public function testAGroupStandsAsAShapeValue(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + AST, $this->parseAndPrint('array{a: (A|B)}')); + } + + public function testAnEmptyGroupIsNoType(): void + { + $this->expectParsingException('unexpected ")"'); + + $this->parse('()'); + } + + public function testAGroupIsClosedByTheParenthesisItIsOpenedWith(): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse('(int'); + } + + public function testAnAngleBracketOfItsOwnFollowsNoArgumentList(): void + { + $this->expectParsingException('unexpected ">"'); + + $this->parse('Some>'); + } +} diff --git a/libs/parser/tests/Syntax/IntLiteralTest.php b/libs/parser/tests/Syntax/IntLiteralTest.php index 5e533f3..32a1e54 100644 --- a/libs/parser/tests/Syntax/IntLiteralTest.php +++ b/libs/parser/tests/Syntax/IntLiteralTest.php @@ -22,12 +22,14 @@ public static function validIntDataProvider(): iterable yield 'zero' => ['0', 0]; yield 'decimal' => ['42', 42]; yield 'negative decimal' => ['-42', -42]; + yield 'signed decimal' => ['+42', 42]; yield 'decimal with underscore' => ['1_000_000', 1000000]; yield 'binary' => ['0b10101101', 173]; yield 'binary with underscore' => ['0b10_10_11_01', 173]; yield 'binary uppercase prefix' => ['0B1010', 10]; yield 'negative binary' => ['-0b1010', -10]; + yield 'signed binary' => ['+0b1010', 10]; yield 'octal' => ['0o42', 34]; yield 'octal uppercase prefix' => ['0O42', 34]; @@ -37,6 +39,40 @@ public static function validIntDataProvider(): iterable yield 'hexadecimal' => ['0xDEAD', 57005]; yield 'hexadecimal mixed case' => ['0XDeaD', 57005]; yield 'hexadecimal with underscore' => ['0xDEAD_BEEF', 3735928559]; + yield 'signed hexadecimal' => ['+0xDEAD', 57005]; + yield 'signed octal' => ['+0o42', 34]; + + yield 'legacy octal of the zero alone' => ['00', 0]; + yield 'legacy octal is the prefixed one' => ['0123', 0o123]; + + yield 'separated decimal' => ['42_04', 4204]; + yield 'separated legacy octal' => ['04_23', 275]; + yield 'separated legacy octal beside the zero' => ['0_42', 34]; + yield 'separated octal' => ['0o6_5_5', 429]; + yield 'separated hexadecimal' => ['0xFE_DE', 65246]; + yield 'separated binary' => ['0b0001_1000', 24]; + } + + /** + * A separator is only allowed between two digits. + * + * @return iterable + */ + public static function invalidSeparatorDataProvider(): iterable + { + yield 'trailing' => ['42_', 'unexpected "_"']; + yield 'doubled' => ['4__2', 'unexpected "__2"']; + yield 'after the hexadecimal prefix' => ['0x_FF', 'unexpected "x_FF"']; + yield 'after the binary prefix' => ['0b_1', 'unexpected "b_1"']; + yield 'after the octal prefix' => ['0o_7', 'unexpected "o_7"']; + } + + #[DataProvider('invalidSeparatorDataProvider')] + public function testSeparatorBelongsBetweenDigits(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); } #[DataProvider('validIntDataProvider')] @@ -62,6 +98,26 @@ public function testOctalAllowsOnlyDigitsUpToSeven(): void $this->parse('0o4281'); } + /** + * A leading zero means an octal, so a digit outside that radix cannot + * follow one. + * + * @return iterable + */ + public static function leadingZeroDataProvider(): iterable + { + yield 'eight' => ['08', 'unexpected "8"']; + yield 'nine' => ['09_1', 'unexpected "9_1"']; + } + + #[DataProvider('leadingZeroDataProvider')] + public function testDecimalCannotBeginWithZero(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + public function testHexadecimalAllowsOnlyHexDigits(): void { $this->expectParsingException(); diff --git a/libs/parser/tests/Syntax/MalformedInputTest.php b/libs/parser/tests/Syntax/MalformedInputTest.php new file mode 100644 index 0000000..692104f --- /dev/null +++ b/libs/parser/tests/Syntax/MalformedInputTest.php @@ -0,0 +1,175 @@ + + */ + public static function emptyInputDataProvider(): iterable + { + yield 'nothing at all' => ['']; + yield 'spaces' => [' ']; + yield 'a line terminator' => ["\n"]; + yield 'a tabulation' => ["\t"]; + } + + /** + * @throws \Throwable + */ + #[DataProvider('emptyInputDataProvider')] + public function testAnEmptyInputIsNoType(string $type): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse($type); + } + + /** + * A type that is cut short ends where the input does, so the reading + * stops at the end of it rather than at a token. + * + * @return iterable + */ + public static function truncatedInputDataProvider(): iterable + { + yield 'a dangling union' => ['int|']; + yield 'a dangling intersection' => ['int&']; + yield 'a question mark alone' => ['?']; + yield 'an unclosed shape' => ['array{']; + yield 'an unclosed field' => ['array{a:']; + yield 'an unclosed argument list' => ['Some<']; + yield 'an unclosed parameter list' => ['callable(']; + yield 'an unclosed offset' => ['int[']; + yield 'an unclosed group' => ['(int']; + yield 'a name that is a separator short' => ['Some\\']; + yield 'a class constant that is a name short' => ['Some::']; + yield 'a condition without its branches' => ['A is B ?']; + yield 'a condition of a single branch' => ['A is B ? C']; + yield 'a condition without its comparand' => ['A is']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('truncatedInputDataProvider')] + public function testATruncatedTypeIsRefused(string $type): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse($type); + } + + /** + * @return iterable + */ + public static function strayTokenDataProvider(): iterable + { + yield 'a union delimiter' => ['|', 'unexpected "|"']; + yield 'an intersection delimiter' => ['&', 'unexpected "&"']; + yield 'a comma' => [',', 'unexpected ","']; + yield 'a colon' => [':', 'unexpected ":"']; + yield 'a double colon' => ['::CONST', 'unexpected "::"']; + yield 'a closing brace' => ['}', 'unexpected "}"']; + yield 'a closing bracket' => [']', 'unexpected "]"']; + yield 'a closing parenthesis' => [')', 'unexpected ")"']; + yield 'an ellipsis' => ['...', 'unexpected "..."']; + yield 'an assignment' => ['=', 'unexpected "="']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $message + * @throws \Throwable + */ + #[DataProvider('strayTokenDataProvider')] + public function testPunctuationAloneIsNoType(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + + /** + * @return iterable + */ + public static function trailingTokenDataProvider(): iterable + { + yield 'a question mark behind a type' => ['int?', 'unexpected "?"']; + yield 'an angle bracket too many' => ['Some>', 'unexpected ">"']; + yield 'a brace too many' => ['array{a: int}}', 'unexpected "}"']; + yield 'a bracket too many' => ['int[]]', 'unexpected "]"']; + yield 'an empty argument list' => ['Some<>', 'unexpected ">"']; + yield 'an empty group' => ['()', 'unexpected ")"']; + yield 'a constant of a generic type' => ['Some::CONST', 'unexpected "::"']; + yield 'a second type' => ['int string', 'unexpected "string"']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $message + * @throws \Throwable + */ + #[DataProvider('trailingTokenDataProvider')] + public function testATokenThatFollowsAWholeTypeIsRefused(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + + /** + * A tolerant reading keeps whatever type it has read and says where it + * stopped, rather than refusing the input whole. + * + * @return iterable}> + */ + public static function tolerantInputDataProvider(): iterable + { + yield 'a name and a tail' => ['int foo bar', 'int', 4]; + yield 'a union and a tail' => ['int|string extra', 'int|string', 11]; + yield 'a shape and a tail' => ['array{a: int} tail', 'array{a: int}', 14]; + yield 'a list and an ellipsis' => ['int[] ...', 'int[]', 6]; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $expected + * @param int<0, max> $offset + * @throws \Throwable + */ + #[DataProvider('tolerantInputDataProvider')] + public function testATolerantReadingStopsAtWhatItCannotRead( + string $type, + string $expected, + int $offset, + ): void { + $result = $this->parseTolerant($type); + + self::assertSame($expected, (new \TypeLang\Printer\PrettyTypePrinter())->print($result->type)); + self::assertSame($offset, $result->offset); + } + + /** + * A tolerant reading is tolerant of a tail alone, so an input that opens + * no type at all is refused all the same. + */ + public function testATolerantReadingRefusesAnInputThatOpensNoType(): void + { + $this->expectParsingException(); + + $this->parseTolerant('|int'); + } +} diff --git a/libs/parser/tests/Syntax/NameTest.php b/libs/parser/tests/Syntax/NameTest.php index 4ba827b..3b4f704 100644 --- a/libs/parser/tests/Syntax/NameTest.php +++ b/libs/parser/tests/Syntax/NameTest.php @@ -80,9 +80,66 @@ public function testKeywordAsExplicitTypeReference(string $keyword): void AST, $this->parseAndPrint('\\' . $keyword)); } + /** + * A source text is read as bytes, and every byte of a character outside + * of ASCII is a letter, so a name stands in whatever script it is + * written in. + * + * @return iterable + */ + public static function nonAsciiNameDataProvider(): iterable + { + yield 'latin-1' => ['Über', 'Über']; + yield 'cyrillic' => ['Тип', 'Тип']; + yield 'cjk' => ['你好', '你好']; + yield 'a namespaced name' => ['Проект\Тип', 'Проект\Тип']; + yield 'beside an ascii part' => ['Проект\Type', 'Проект\Type']; + yield 'with a dash' => ['non-empty-Тип', 'non-empty-Тип']; + yield 'with a digit' => ['Тип42', 'Тип42']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $expected + * @throws \Throwable + */ + #[DataProvider('nonAsciiNameDataProvider')] + public function testANameStandsInAnyScript(string $type, string $expected): void + { + self::assertSame( + $expected, + (new \TypeLang\Printer\PrettyTypePrinter())->print($this->parse($type)), + ); + } + + public function testANonAsciiNameStandsAsAShapeKey(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(ключ) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('array{ключ: int}')); + } + + public function testANonAsciiNameStandsAsATemplateArgument(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(list) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + NamedTypeNode + Name(Тип) + AST, $this->parseAndPrint('list<Тип>')); + } public function testNameCannotStartWithDigit(): void { - $this->expectParsingException('unexpected "invalid_name_0"'); + // The underscore belongs to the name, not to the number + $this->expectParsingException('unexpected "_invalid_name_0"'); $this->parse('0_invalid_name_0'); } diff --git a/libs/parser/tests/Syntax/OffsetAccessTest.php b/libs/parser/tests/Syntax/OffsetAccessTest.php index 3f653d0..f77880d 100644 --- a/libs/parser/tests/Syntax/OffsetAccessTest.php +++ b/libs/parser/tests/Syntax/OffsetAccessTest.php @@ -16,6 +16,7 @@ public function testStringOffset(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode + Literal\StringLiteralNode('offset') NamedTypeNode Name(T) AST, $this->parseAndPrint("T['offset']")); @@ -25,6 +26,8 @@ public function testDependentKeyOffset(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode + NamedTypeNode + Name(U) NamedTypeNode Name(T) AST, $this->parseAndPrint('T[U]')); @@ -34,6 +37,7 @@ public function testShapeWithNumericOffset(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode + Literal\IntLiteralNode(0) NamedTypeNode Name(array) Shape\FieldsListNode(isSealed=true) @@ -50,6 +54,13 @@ public function testComplexOffsetWithGenericsAndShapes(): void { self::assertSame(<<<'AST' TypeOffsetAccessNode + NamedTypeNode + Name(object) + Shape\FieldsListNode(isSealed=false) + Shape\NamedFieldNode(isOptional=false) + Identifier(key) + NamedTypeNode + Name(int) NamedTypeNode Name(T) Template\TemplateArgumentListNode diff --git a/libs/parser/tests/Syntax/ShapeTest.php b/libs/parser/tests/Syntax/ShapeTest.php index b40e8a8..118f2eb 100644 --- a/libs/parser/tests/Syntax/ShapeTest.php +++ b/libs/parser/tests/Syntax/ShapeTest.php @@ -4,6 +4,7 @@ namespace TypeLang\Parser\Tests\Syntax; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -35,11 +36,11 @@ public function testNumericExplicitKeys(): void NamedTypeNode Name(array) Shape\FieldsListNode(isSealed=true) - Shape\NumericFieldNode(isOptional=false) + Shape\ScalarFieldNode(isOptional=false) Literal\IntLiteralNode(1) NamedTypeNode Name(first) - Shape\NumericFieldNode(isOptional=false) + Shape\ScalarFieldNode(isOptional=false) Literal\IntLiteralNode(42) NamedTypeNode Name(second) @@ -52,11 +53,11 @@ public function testStringExplicitKeys(): void NamedTypeNode Name(array) Shape\FieldsListNode(isSealed=true) - Shape\StringNamedFieldNode(isOptional=false) + Shape\ScalarFieldNode(isOptional=false) Literal\StringLiteralNode("name-some") NamedTypeNode Name(first) - Shape\StringNamedFieldNode(isOptional=false) + Shape\ScalarFieldNode(isOptional=false) Literal\StringLiteralNode("escape\nchars") NamedTypeNode Name(second) @@ -179,4 +180,59 @@ public function testOptionalValueSyntaxIsNotAllowed(): void $this->parse('array{key: Type?}'); } + + /** + * A "true" and a "null" name a field as the words they are written with, + * not as the values they name elsewhere. + * + * @return iterable + */ + public static function keywordKeyDataProvider(): iterable + { + yield 'true' => ['array{true: int}']; + yield 'false' => ['array{false: int}']; + yield 'null' => ['array{null: int}']; + } + + #[DataProvider('keywordKeyDataProvider')] + public function testKeywordKeyIsAName(string $type): void + { + self::assertSame($type, (new \TypeLang\Printer\PrettyTypePrinter())->print($this->parse($type))); + } + + /** + * A key is a number or a string and nothing else, the way a key of an + * array is. + * + * @return iterable + */ + public static function invalidKeyDataProvider(): iterable + { + yield 'boolean' => ['array{(true): int}']; + yield 'boolean of the other kind' => ['array{(false): int}']; + yield 'null' => ['array{(null): int}']; + yield 'float' => ['array{0.42: int}']; + yield 'float in parentheses' => ['array{(0.42): int}']; + yield 'variable' => ['array{$this: int}']; + yield 'union' => ['array{(A|B): int}']; + } + + #[DataProvider('invalidKeyDataProvider')] + public function testKeyIsANumberAStringOrANameAlone(string $type): void + { + $this->expectParsingException('Shape key must be a name, a number, a string'); + + $this->parse($type); + } + + /** + * A key ends in the ":" its value begins after, so a type that carries + * a colon of its own is no key. + */ + public function testKeyDoesNotReachBeyondAPrimaryType(): void + { + $this->expectParsingException('unexpected ":"'); + + $this->parse('array{T is A ? B : C: int}'); + } } diff --git a/libs/parser/tests/Syntax/TemplateParameterTest.php b/libs/parser/tests/Syntax/TemplateParameterTest.php new file mode 100644 index 0000000..25ccc78 --- /dev/null +++ b/libs/parser/tests/Syntax/TemplateParameterTest.php @@ -0,0 +1,262 @@ +parseAndPrint('callable(T): T')); + } + + public function testEveryLimitIsReadIntoAPlaceOfItsOwn(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(Closure) + Callable\CallableParameterListNode + Template\TemplateParameterListNode + Template\TemplateParameterNode + Identifier(T) + Template\TemplateBoundEdgeNode + Identifier(of) + NamedTypeNode + Name(Some) + Template\TemplateBoundEdgeNode + Identifier(super) + NamedTypeNode + Name(Any) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('Closure()')); + } + + /** + * The word an upper bound is written with is kept as it is written, since + * an "of" and an "as" mean the same and only one of them was typed. + */ + public function testUpperBoundKeepsTheWordItIsWrittenWith(): void + { + $node = $this->parse('Closure()'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + self::assertSame('as', $node->templates->items[0]->upper?->operator->value); + } + + /** + * @return iterable + */ + public static function limitDataProvider(): iterable + { + yield 'of' => ['callable(): void', 'upper']; + yield 'as' => ['callable(): void', 'upper']; + yield 'super' => ['callable(): void', 'lower']; + yield 'assign' => ['callable(): void', 'default']; + } + + /** + * @param non-empty-string $type + * @param 'upper'|'lower'|'default' $filled + * @throws \Throwable + */ + #[DataProvider('limitDataProvider')] + public function testEveryLimitFillsThePlaceItBelongsTo(string $type, string $filled): void + { + $node = $this->parse($type); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $parameter = $node->templates->items[0]; + + self::assertSame('T', $parameter->name->value); + + foreach (['upper', 'lower', 'default'] as $place) { + if ($place === $filled) { + self::assertNotNull($parameter->{$place}, "The {$place} must be filled"); + + continue; + } + + self::assertNull($parameter->{$place}, "The {$place} must be left out"); + } + } + + public function testASingleParameterCarriesEveryLimitItIsWrittenWith(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $parameter = $node->templates->items[0]; + + self::assertSame('of', $parameter->upper?->operator->value); + self::assertSame('super', $parameter->lower?->operator->value); + self::assertNotNull($parameter->default); + } + + public function testAParameterWrittenWithNoLimitCarriesNone(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + $parameter = $node->templates->items[0]; + + self::assertNull($parameter->upper); + self::assertNull($parameter->lower); + self::assertNull($parameter->default); + } + + /** + * @return iterable + */ + public static function invalidLimitDataProvider(): iterable + { + yield 'unknown word' => [ + 'callable(): void', + 'cannot be bounded with "whatever"', + ]; + yield 'an upper bound of another case' => [ + 'callable(): void', + 'cannot be bounded with "OF"', + ]; + yield 'a lower bound of another case' => [ + 'callable(): void', + 'cannot be bounded with "SUPER"', + ]; + yield 'two upper bounds' => [ + 'callable(): void', + 'cannot have more than one upper bound', + ]; + yield 'two lower bounds' => [ + 'callable(): void', + 'cannot have more than one lower bound', + ]; + yield 'two defaults' => [ + 'callable(): void', + 'cannot have more than one default', + ]; + yield 'upper bound behind the default' => [ + 'callable(): void', + 'default must be written last', + ]; + yield 'lower bound behind the default' => [ + 'callable(): void', + 'default must be written last', + ]; + } + + /** + * The two ends are told apart by the word each is written with, so which + * of them comes first says nothing. + */ + public function testTheTwoEndsAreWrittenInEitherOrder(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $parameter = $node->templates->items[0]; + + self::assertSame('of', $parameter->upper?->operator->value); + self::assertSame('super', $parameter->lower?->operator->value); + } + + /** + * @param non-empty-string $type + * @param non-empty-string $message + * @throws \Throwable + */ + #[DataProvider('invalidLimitDataProvider')] + public function testALimitIsWrittenAtMostOnceAndBoundsSomething(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + + public function testSeveralParametersAreRead(): void + { + $node = $this->parse('callable(T, U): V'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $names = []; + + foreach ($node->templates as $parameter) { + $names[] = $parameter->name->value; + } + + self::assertSame(['T', 'U', 'V'], $names); + } + + public function testTrailingCommaIsAllowed(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + self::assertCount(2, $node->templates); + } + + /** + * A "<...>" that no parenthesis follows is an argument list, and an + * argument list describes no bounds. + */ + public function testBoundsBelongToACallableAlone(): void + { + $this->expectParsingException(); + + $this->parse('Collection'); + } + + public function testATypeUsedWithArgumentsIsNotACallable(): void + { + $node = $this->parse('Collection'); + + self::assertInstanceOf(\TypeLang\Type\NamedTypeNode::class, $node); + self::assertNotNull($node->arguments); + } + + public function testParametersAreRefusedWhenGenericsAreDisabled(): void + { + $this->expectParsingException('Template parameters not allowed'); + + $this->parse('callable(T): T', ['generics' => false]); + } + + public function testParametersAreRefusedWhenCallablesAreDisabled(): void + { + $this->expectParsingException('Callable types not allowed'); + + $this->parse('callable(T): T', ['callables' => false]); + } +} diff --git a/libs/parser/tests/Syntax/TriviaTest.php b/libs/parser/tests/Syntax/TriviaTest.php new file mode 100644 index 0000000..b166b33 --- /dev/null +++ b/libs/parser/tests/Syntax/TriviaTest.php @@ -0,0 +1,254 @@ + + */ + public static function commentedUnionDataProvider(): iterable + { + yield 'block comment' => ['int /* c */ | string']; + yield 'two block comments' => ['int /* a */ /* b */ | string']; + yield 'line comment' => ["int // c\n| string"]; + yield 'hash comment' => ["int # c\n| string"]; + yield 'empty hash comment' => ["int #\n| string"]; + yield 'comment behind the delimiter' => ['int|/* c */string']; + yield 'comment on either side' => ['/* a */int|string/* b */']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('commentedUnionDataProvider')] + public function testACommentPartsTwoTokens(string $type): void + { + self::assertSame(<<<'AST' + UnionTypeNode + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + AST, $this->parseAndPrint($type)); + } + + /** + * A comment ends the line it is written on, so whatever follows the line + * is read as usual. + */ + public function testALineCommentEndsWithItsLine(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint("array{\n // the key\n a: int,\n}")); + } + + /** + * @return iterable + */ + public static function commentedNameDataProvider(): iterable + { + yield 'in front of the separator' => ['Some/* c */\Any']; + yield 'behind the separator' => ['Some\/* c */Any']; + } + + /** + * A separator parts the segments of a name on its own, so a comment + * standing beside it parts nothing. + * + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('commentedNameDataProvider')] + public function testACommentStandsInsideAName(string $type): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Some\Any) + AST, $this->parseAndPrint($type)); + } + + public function testACommentStandsInsideAnEmptyShape(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + AST, $this->parseAndPrint('array{ /* c */ }')); + } + + public function testACommentStandsBetweenANameAndItsParameters(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable/* c */(): void')); + } + + /** + * @return iterable + */ + public static function spacedTypeDataProvider(): iterable + { + yield 'around the arguments' => ['Some < int , string >']; + yield 'no space at all' => ['Some']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('spacedTypeDataProvider')] + public function testSpaceAroundATokenIsOfNoMeaning(string $type): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Some) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + NamedTypeNode + Name(int) + Template\TemplateArgumentNode + NamedTypeNode + Name(string) + AST, $this->parseAndPrint($type)); + } + + public function testSpaceStandsAroundEveryPartOfACallable(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + NamedTypeNode + Name(int) + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable ( int ) : void')); + } + + public function testATypeIsSurroundedByBlankLines(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(int) + AST, $this->parseAndPrint("\n\n int \n\n")); + } + + /** + * The two words of an "is not" are two tokens, so whatever stands between + * any other pair of tokens stands between them as well. + * + * @return iterable + */ + public static function partedIsNotDataProvider(): iterable + { + yield 'a single space' => ['A is not B ? C : D']; + yield 'several spaces' => ['A is not B ? C : D']; + yield 'a tabulation' => ["A is\tnot B ? C : D"]; + yield 'a line terminator' => ["A is\nnot B ? C : D"]; + yield 'a block comment' => ['A is/* c */not B ? C : D']; + yield 'a line comment' => ["A is // c\nnot B ? C : D"]; + yield 'a comment and a line terminator' => ["A is /* c */\n not B ? C : D"]; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('partedIsNotDataProvider')] + public function testTriviaPartsTheWordsOfAnIsNot(string $type): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\NotEqualConditionNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + NamedTypeNode + Name(D) + AST, $this->parseAndPrint($type)); + } + + /** + * The words are read the way they are written, so a single word that is + * spelled like the two of them is an ordinary name. + */ + public function testTheWordsOfAnIsNotAreTwoWords(): void + { + $this->expectParsingException(); + + $this->parse('A isnot B ? C : D'); + } + + /** + * @return iterable + */ + public static function casedIsNotDataProvider(): iterable + { + yield 'uppercase' => ['A IS NOT B ? C : D']; + yield 'uppercase negation alone' => ['A is NOT B ? C : D']; + yield 'mixed case' => ['A Is NoT B ? C : D']; + } + + /** + * The words are written the one way they are written, so a word of any + * other case is an ordinary name and opens no condition. + * + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('casedIsNotDataProvider')] + public function testTheWordsOfAnIsNotAreCaseSensitive(string $type): void + { + $this->expectParsingException(); + + $this->parse($type); + } + + public function testAnUnterminatedBlockCommentIsRefused(): void + { + $this->expectParsingException('unrecognized "/*"'); + + $this->parse('/* the comment that never ends'); + } + + public function testACommentAloneIsNoType(): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse('// only a comment'); + } + + public function testSpaceAloneIsNoType(): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse(" \n\t "); + } +} diff --git a/libs/parser/tests/Syntax/VariableTest.php b/libs/parser/tests/Syntax/VariableTest.php new file mode 100644 index 0000000..99cd224 --- /dev/null +++ b/libs/parser/tests/Syntax/VariableTest.php @@ -0,0 +1,234 @@ +parseAndPrint('$this')); + } + + public function testThisStandsAsAReturnType(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + ThisNode + AST, $this->parseAndPrint('callable(): $this')); + } + + public function testThisStandsAsAParameterType(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + ThisNode + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable($this): void')); + } + + /** + * The same word names a parameter once a type stands in front of it, so + * it is read as a variable and not as a type. + */ + public function testThisNamesAParameter(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + NamedTypeNode + Name(int) + VariableNode + Identifier(this) + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable(int $this): void')); + } + + public function testThisIsATemplateArgument(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Some) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + ThisNode + AST, $this->parseAndPrint('Some<$this>')); + } + + public function testThisIsAShapeValue(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + ThisNode + AST, $this->parseAndPrint('array{a: $this}')); + } + + /** + * @return iterable + */ + public static function wrappedThisDataProvider(): iterable + { + yield 'list' => ['$this[]', 'TypesListNode']; + yield 'nullable' => ['?$this', 'NullableTypeNode']; + yield 'union' => ['$this|null', 'UnionTypeNode']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $wrapper + * @throws \Throwable + */ + #[DataProvider('wrappedThisDataProvider')] + public function testThisIsWrappedLikeAnyOtherType(string $type, string $wrapper): void + { + self::assertStringStartsWith($wrapper, $this->parseAndPrint($type)); + } + + /** + * A condition reads its subject as a type first, so the word is the type + * and not the variable it is spelled like. + */ + public function testThisStandsAsATypeOnTheLeftOfACondition(): void + { + $node = $this->parse('$this is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(ThisNode::class, $node->condition->subject); + } + + /** + * The token is written the one way it is written, so a word that only + * looks like it is an ordinary variable. + */ + public function testTheThisTokenIsCaseSensitive(): void + { + $node = $this->parse('$This is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(VariableNode::class, $node->condition->subject); + self::assertSame('This', $node->condition->subject->name->value); + } + + public function testAVariableStandsBesideACondition(): void + { + $node = $this->parse('$value is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(VariableNode::class, $node->condition->subject); + self::assertSame('value', $node->condition->subject->name->value); + } + + /** + * A variable names a place a value is kept in, so it is no type and + * stands nowhere a type is expected. + */ + public function testAVariableIsNoTypeOfItsOwn(): void + { + $this->expectParsingException(); + + $this->parse('$value'); + } + + public function testAVariableIsNoTemplateArgument(): void + { + $this->expectParsingException(); + + $this->parse('Some<$value>'); + } + + public function testThisCarriesNoClassConstant(): void + { + $this->expectParsingException('unexpected "::"'); + + $this->parse('$this::CONST'); + } + + public function testAParameterNameStandsBehindItsTypeAlone(): void + { + $this->expectParsingException(); + + $this->parse('callable($value int): void'); + } + + /** + * A dash belongs to a name and not to a variable, the way it does not + * belong to one in PHP either. + * + * @return iterable + */ + public static function dashedVariableDataProvider(): iterable + { + yield 'an ordinary variable' => ['$value-of is B ? C : D']; + yield 'the this variable' => ['$this-of is B ? C : D']; + yield 'a parameter name' => ['callable(int $value-of): void']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('dashedVariableDataProvider')] + public function testAVariableCarriesNoDash(string $type): void + { + $this->expectParsingException(); + + $this->parse($type); + } + + /** + * @return iterable + */ + public static function variableBodyDataProvider(): iterable + { + yield 'an underscore' => ['$value_1']; + yield 'a leading underscore' => ['$_value']; + yield 'digits' => ['$value42']; + yield 'a word that opens with this' => ['$thisValue']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('variableBodyDataProvider')] + public function testAVariableCarriesWhateverANameCarriesBesideADash(string $type): void + { + $node = $this->parse($type . ' is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(VariableNode::class, $node->condition->subject); + self::assertSame(\substr($type, 1), $node->condition->subject->name->value); + } + public function testTheDollarSignAloneIsNoVariable(): void + { + $this->expectParsingException(); + + $this->parse('$'); + } +} diff --git a/libs/parser/tests/Syntax/WildcardTest.php b/libs/parser/tests/Syntax/WildcardTest.php new file mode 100644 index 0000000..5271f2d --- /dev/null +++ b/libs/parser/tests/Syntax/WildcardTest.php @@ -0,0 +1,98 @@ +parseAndPrint('Collection<*>')); + } + + public function testWildcardBesideAType(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(HashMap) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + NamedTypeNode + Name(array-key) + Template\TemplateArgumentNode + WildcardNode(*) + AST, $this->parseAndPrint('HashMap')); + } + + public function testWildcardCarriesAHint(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Collection) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + WildcardNode(*) + Identifier(out) + AST, $this->parseAndPrint('Collection')); + } + + public function testWildcardIsReadInsideAnUnsealedShape(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + WildcardNode(*) + Shape\FieldsListNode(isSealed=false) + AST, $this->parseAndPrint('array{...<*>}')); + } + + public function testWildcardOffsetIsTheOneItIsWrittenAt(): void + { + $node = $this->parse('Collection'); + + self::assertInstanceOf(\TypeLang\Type\NamedTypeNode::class, $node); + self::assertNotNull($node->arguments); + + $argument = $node->arguments->items[1]; + + self::assertInstanceOf(\TypeLang\Type\WildcardNode::class, $argument->value); + self::assertSame(16, $argument->value->offset); + } + + public function testWildcardIsNotAType(): void + { + $this->expectParsingException(); + + $this->parse('int|*'); + } + + public function testWildcardIsNotACallableParameter(): void + { + $this->expectParsingException(); + + $this->parse('callable(*): void'); + } + + public function testWildcardIsRefusedWhenGenericsAreDisabled(): void + { + $this->expectParsingException('Template arguments not allowed'); + + $this->parse('Collection<*>', ['generics' => false]); + } +} From 19b15c33dc41306c7ac1d60b6294036ab7c39f32 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 02:56:25 +0300 Subject: [PATCH 086/119] Do not import internal namespace into the parser --- libs/parser/bin/build | 5 ++--- libs/parser/resources/grammar/literals.pp3 | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libs/parser/bin/build b/libs/parser/bin/build index cad3791..accea66 100644 --- a/libs/parser/bin/build +++ b/libs/parser/bin/build @@ -38,7 +38,7 @@ if (!class_exists(Compiler::class)) { ->load(FileSource::createFromPathname(__DIR__ . '/../resources/grammar.pp3')) ->generate() ->withNamespaceName('TypeLang\\Parser\\Internal') - ->withClassName('CompilerExecutor') + ->withClassName('CompiledExecutor') ->withClassModifier(ClassModifier::Abstract) // The parser carries the features it recognizes a source with, so it // is declared as a mutable class rather than a readonly one. @@ -46,5 +46,4 @@ if (!class_exists(Compiler::class)) { ->withTargetPhpVersion(TargetPhpVersion::Php81) ->withClassImport('TypeLang\\Type') ->withClassImport('TypeLang\\Parser\\Exception') - ->withClassImport('TypeLang\\Parser\\Internal') - ->save(__DIR__ . '/../src/Internal/CompilerExecutor.php'); + ->save(__DIR__ . '/../src/Internal/CompiledExecutor.php'); diff --git a/libs/parser/resources/grammar/literals.pp3 b/libs/parser/resources/grammar/literals.pp3 index 2135313..2f2dc7d 100644 --- a/libs/parser/resources/grammar/literals.pp3 +++ b/libs/parser/resources/grammar/literals.pp3 @@ -24,7 +24,7 @@ StringLiteralType DoubleQuotedStringLiteralType -> { return new Type\Literal\StringLiteralNode( - Internal\StringDecoder::unpackAndDecode($children->value, true), + StringDecoder::unpackAndDecode($children->value, true), $children->value, $offset, ); @@ -34,7 +34,7 @@ DoubleQuotedStringLiteralType -> { SingleQuotedStringLiteralType -> { return new Type\Literal\StringLiteralNode( - Internal\StringDecoder::unpackAndDecode($children->value, false), + StringDecoder::unpackAndDecode($children->value, false), $children->value, $offset, ); @@ -44,7 +44,7 @@ SingleQuotedStringLiteralType -> { FloatLiteralType -> { return new Type\Literal\FloatLiteralNode( - Internal\FloatDecoder::decode($children->value), + FloatDecoder::decode($children->value), $children->value, $offset, ); @@ -53,7 +53,7 @@ FloatLiteralType -> { ; IntLiteralType -> { - $decoded = Internal\IntDecoder::decode($children->value); + $decoded = IntDecoder::decode($children->value); return new Type\Literal\IntLiteralNode( $decoded->value, From 4a9ad6d534d1844c551de2164a0a8222e546cf4c Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 02:56:42 +0300 Subject: [PATCH 087/119] Improve exceptions --- libs/parser/src/Exception/InternalParseException.php | 8 -------- libs/parser/src/Exception/UnexpectedTokenException.php | 1 - libs/parser/src/Exception/UnrecognizedSyntaxException.php | 3 --- libs/parser/src/Exception/UnrecognizedTokenException.php | 3 --- 4 files changed, 15 deletions(-) diff --git a/libs/parser/src/Exception/InternalParseException.php b/libs/parser/src/Exception/InternalParseException.php index 969fa27..e55f2fe 100644 --- a/libs/parser/src/Exception/InternalParseException.php +++ b/libs/parser/src/Exception/InternalParseException.php @@ -18,14 +18,6 @@ public static function becauseInternalErrorOccurs(string $statement, \Throwable return new self($message, self::ERROR_CODE_INTERNAL_ERROR, $e); } - /** - * Occurs when the parser produces no result for a readable statement. - */ - public static function becauseTypeStatementIsUnreadable(): self - { - return new self('Could not read type statement', self::ERROR_CODE_INTERNAL_ERROR); - } - /** * Occurs when the source content cannot be read. */ diff --git a/libs/parser/src/Exception/UnexpectedTokenException.php b/libs/parser/src/Exception/UnexpectedTokenException.php index 0e46508..b5f54e5 100644 --- a/libs/parser/src/Exception/UnexpectedTokenException.php +++ b/libs/parser/src/Exception/UnexpectedTokenException.php @@ -12,7 +12,6 @@ final class UnexpectedTokenException extends ParseException * Occurs when a known token is found in an unexpected source location. * * @param int<0, max> $offset - * @throws SourceExceptionInterface */ public static function becauseTokenIsUnexpected(string $token, string $statement, int $offset): self { diff --git a/libs/parser/src/Exception/UnrecognizedSyntaxException.php b/libs/parser/src/Exception/UnrecognizedSyntaxException.php index 010fc65..8425a70 100644 --- a/libs/parser/src/Exception/UnrecognizedSyntaxException.php +++ b/libs/parser/src/Exception/UnrecognizedSyntaxException.php @@ -4,8 +4,6 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; - final class UnrecognizedSyntaxException extends ParseException { /** @@ -13,7 +11,6 @@ final class UnrecognizedSyntaxException extends ParseException * grammar rule. * * @param int<0, max> $offset - * @throws SourceExceptionInterface */ public static function becauseSyntaxIsUnrecognized(string $statement, int $offset): self { diff --git a/libs/parser/src/Exception/UnrecognizedTokenException.php b/libs/parser/src/Exception/UnrecognizedTokenException.php index 3f26041..d417a8d 100644 --- a/libs/parser/src/Exception/UnrecognizedTokenException.php +++ b/libs/parser/src/Exception/UnrecognizedTokenException.php @@ -4,15 +4,12 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; - final class UnrecognizedTokenException extends ParseException { /** * Occurs when unable to recognize tokens in source code. * * @param int<0, max> $offset - * @throws SourceExceptionInterface */ public static function becauseTokenIsUnrecognized(string $token, string $statement, int $offset): self { From ca84baa3e0bb2d17ba6b37340f9ebeb90b7687a2 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 02:57:32 +0300 Subject: [PATCH 088/119] Rename Parser::parseTolerant() to Parser::partial() and add Parser::validate() method --- libs/parser/src/InMemoryTypeParser.php | 23 +- ...pilerExecutor.php => CompiledExecutor.php} | 24 +- libs/parser/src/Internal/Executor.php | 229 ++++++++++++++++-- libs/parser/src/ParsedResult.php | 20 -- .../src/Partial/FailureParsedResult.php | 32 +++ libs/parser/src/Partial/ParsedResult.php | 18 ++ .../src/Partial/PartialParsedResult.php | 21 ++ .../src/Partial/SuccessfulParsedResult.php | 33 +++ libs/parser/src/TypeParser.php | 143 ++--------- libs/parser/src/TypeParserInterface.php | 34 ++- libs/parser/src/Validation/CheckResult.php | 12 + .../src/Validation/FailureCheckResult.php | 37 +++ .../src/Validation/PartialCheckResult.php | 11 + .../src/Validation/SuccessfulCheckResult.php | 10 + 14 files changed, 467 insertions(+), 180 deletions(-) rename libs/parser/src/Internal/{CompilerExecutor.php => CompiledExecutor.php} (99%) delete mode 100644 libs/parser/src/ParsedResult.php create mode 100644 libs/parser/src/Partial/FailureParsedResult.php create mode 100644 libs/parser/src/Partial/ParsedResult.php create mode 100644 libs/parser/src/Partial/PartialParsedResult.php create mode 100644 libs/parser/src/Partial/SuccessfulParsedResult.php create mode 100644 libs/parser/src/Validation/CheckResult.php create mode 100644 libs/parser/src/Validation/FailureCheckResult.php create mode 100644 libs/parser/src/Validation/PartialCheckResult.php create mode 100644 libs/parser/src/Validation/SuccessfulCheckResult.php diff --git a/libs/parser/src/InMemoryTypeParser.php b/libs/parser/src/InMemoryTypeParser.php index 3128250..dbcf807 100644 --- a/libs/parser/src/InMemoryTypeParser.php +++ b/libs/parser/src/InMemoryTypeParser.php @@ -10,6 +10,8 @@ use Phplrt\Contracts\Source\SourceFactoryInterface; use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\ParserExceptionInterface; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Type\TypeNode; final class InMemoryTypeParser implements TypeParserInterface @@ -29,6 +31,11 @@ final class InMemoryTypeParser implements TypeParserInterface */ private array $sequences = []; + /** + * @var array + */ + private array $checks = []; + private readonly SourceFactoryInterface $sources; public function __construct( @@ -55,11 +62,23 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode * @throws SourceExceptionInterface * @throws \Throwable */ - public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult + public function partial(#[Language('PHP')] mixed $source): ParsedResult + { + $instance = $this->sources->create($source); + + return $this->sequences[$this->hash($instance)] ??= $this->parser->partial($source); + } + + /** + * @throws ParserExceptionInterface + * @throws SourceExceptionInterface + * @throws \Throwable + */ + public function validate(#[Language('PHP')] mixed $source): CheckResult { $instance = $this->sources->create($source); - return $this->sequences[$this->hash($instance)] ??= $this->parser->parseTolerant($source); + return $this->checks[$this->hash($instance)] ??= $this->parser->validate($source); } /** diff --git a/libs/parser/src/Internal/CompilerExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php similarity index 99% rename from libs/parser/src/Internal/CompilerExecutor.php rename to libs/parser/src/Internal/CompiledExecutor.php index ed5e181..d0c8c6d 100644 --- a/libs/parser/src/Internal/CompilerExecutor.php +++ b/libs/parser/src/Internal/CompiledExecutor.php @@ -8,9 +8,8 @@ namespace TypeLang\Parser\Internal; -use TypeLang\Type; use TypeLang\Parser\Exception; -use TypeLang\Parser\Internal; +use TypeLang\Type; \interface_exists(\Phplrt\Contracts\Source\Exception\SourceExceptionInterface::class); \interface_exists(\Phplrt\Contracts\Source\ReadableInterface::class); @@ -31,7 +30,7 @@ * * @template-implements \Phplrt\Contracts\Parser\ParserInterface */ -abstract class CompilerExecutor implements \Phplrt\Contracts\Parser\ParserInterface +abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterface { /** @var int */ public const T_WHITESPACE = 0; @@ -2115,6 +2114,7 @@ private function reduceLiteralType(\Phplrt\Parser\Context $ctx, mixed $children) if ($this->features->literals === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); } + return $children; } @@ -2124,7 +2124,7 @@ private static function reduceDoubleQuotedStringLiteralType(\Phplrt\Parser\Conte $offset = $ctx->begin; return new Type\Literal\StringLiteralNode( - Internal\StringDecoder::unpackAndDecode($children->value, true), + StringDecoder::unpackAndDecode($children->value, true), $children->value, $offset, ); @@ -2136,7 +2136,7 @@ private static function reduceSingleQuotedStringLiteralType(\Phplrt\Parser\Conte $offset = $ctx->begin; return new Type\Literal\StringLiteralNode( - Internal\StringDecoder::unpackAndDecode($children->value, false), + StringDecoder::unpackAndDecode($children->value, false), $children->value, $offset, ); @@ -2148,7 +2148,7 @@ private static function reduceFloatLiteralType(\Phplrt\Parser\Context $ctx, mixe $offset = $ctx->begin; return new Type\Literal\FloatLiteralNode( - Internal\FloatDecoder::decode($children->value), + FloatDecoder::decode($children->value), $children->value, $offset, ); @@ -2159,7 +2159,7 @@ private static function reduceIntLiteralType(\Phplrt\Parser\Context $ctx, mixed // The variables below are declared by the compiler $offset = $ctx->begin; - $decoded = Internal\IntDecoder::decode($children->value); + $decoded = IntDecoder::decode($children->value); return new Type\Literal\IntLiteralNode( $decoded->value, @@ -2217,15 +2217,15 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): case $suffix === null: return new Type\NamedTypeNode($children[0], null, null, $offset); - // Some\Any + // Some\Any case $suffix instanceof Type\Template\TemplateArgumentListNode: return new Type\NamedTypeNode($children[0], $suffix, null, $offset); - // Some\Any{name: T, ...} + // Some\Any{name: T, ...} case $suffix instanceof Type\Shape\FieldsListNode: return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); - // Some\Any(T, U): V + // Some\Any(T, U): V case $suffix instanceof Type\Callable\CallableParameterListNode: if ($this->features->callables === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); @@ -2233,7 +2233,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); - // Some\Any(T): U + // Some\Any(T): U case $suffix instanceof Type\Template\TemplateParameterListNode: if ($this->features->callables === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); @@ -2241,7 +2241,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); - // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* + // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* default: if (\count($children) === 3 && $children[2] instanceof Type\Identifier) { return new Type\ClassConstNode($children[0], $children[2], $offset); diff --git a/libs/parser/src/Internal/Executor.php b/libs/parser/src/Internal/Executor.php index 4dfead3..e93c736 100644 --- a/libs/parser/src/Internal/Executor.php +++ b/libs/parser/src/Internal/Executor.php @@ -4,14 +4,33 @@ namespace TypeLang\Parser\Internal; -use Phplrt\Contracts\Parser\Exception\ParserExceptionInterface; -use Phplrt\Contracts\Parser\Exception\RuntimeExceptionInterface; +use Phplrt\Contracts\Lexer\Channel; +use Phplrt\Contracts\Position\PositionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; use Phplrt\Contracts\Source\ReadableInterface; use Phplrt\Parser\Analysis\Mode; use Phplrt\Parser\Analysis\Result\FailureResult; +use Phplrt\Parser\Analysis\Result\PartialResult; use Phplrt\Parser\Analysis\Result\SuccessfulResult; +use Phplrt\Parser\Exception\UnexpectedTokenException as GrammarUnexpectedTokenException; use Phplrt\Parser\Parser as ParserRuntime; +use Phplrt\Position\PositionFactory; +use TypeLang\Parser\Exception\InternalParseException; +use TypeLang\Parser\Exception\ParseException; +use TypeLang\Parser\Exception\SemanticException; +use TypeLang\Parser\Exception\SemanticParseException; +use TypeLang\Parser\Exception\UnexpectedTokenException; +use TypeLang\Parser\Exception\UnrecognizedSyntaxException; +use TypeLang\Parser\Exception\UnrecognizedTokenException; +use TypeLang\Parser\Partial\FailureParsedResult; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Partial\PartialParsedResult; +use TypeLang\Parser\Partial\SuccessfulParsedResult; use TypeLang\Parser\TypeParserFeatures; +use TypeLang\Parser\Validation\CheckResult; +use TypeLang\Parser\Validation\FailureCheckResult; +use TypeLang\Parser\Validation\PartialCheckResult; +use TypeLang\Parser\Validation\SuccessfulCheckResult; use TypeLang\Type\TypeNode; /** @@ -21,37 +40,213 @@ * @internal this is an internal library class, please do not use it in your code * @psalm-internal TypeLang\Parser * - * @template-extends CompilerExecutor + * @template-extends CompiledExecutor + * + * @property-read ParserRuntime $parser */ -final class Executor extends CompilerExecutor +final class Executor extends CompiledExecutor { + private readonly PositionFactory $positions; + public function __construct( /** * @api this property is accessible inside the grammar reducers */ protected readonly TypeParserFeatures $features, ) { + $this->positions = new PositionFactory(); + parent::__construct(); } /** - * Reads as much of the given source as the grammar describes and reports - * what it has been made of. + * Reads the source whole and returns the type it is written of. + * + * @throws ParseException in case of the source is no type of its own + */ + public function parse(ReadableInterface $source): TypeNode + { + $result = $this->build($source); + + if (!$result instanceof SuccessfulResult || $result instanceof PartialResult) { + throw $this->createError($result, $source); + } + + return $result->value; + } + + /** + * Reads as much of the source as the grammar describes and returns the + * type that part is written of. + * + * @throws ParseException in case of an internal error occurs + */ + public function partial(ReadableInterface $source): ParsedResult + { + $result = $this->build($source); + + if ($result instanceof PartialResult) { + return new PartialParsedResult( + type: $result->value, + offset: $result->token->offset, + ); + } + + if ($result instanceof SuccessfulResult) { + return new SuccessfulParsedResult($result->value, $this->length($source)); + } + + return new FailureParsedResult( + message: $this->createError($result, $source)->getMessage(), + position: $this->createPosition($source, $result->token->offset), + offset: $result->token->offset, + ); + } + + /** + * Tells whether the source is a type the grammar describes, building + * nothing of it. + * + * @throws ParseException in case of an internal error occurs + */ + public function validate(ReadableInterface $source): CheckResult + { + $result = $this->check($source); + + if ($result instanceof SuccessfulResult && !$result instanceof PartialResult) { + return new SuccessfulCheckResult(); + } + + $error = $this->createError($result, $source); + $offset = $result->token->offset; + $position = $this->createPosition($source, $offset); + + if ($result instanceof PartialResult) { + return new PartialCheckResult( + message: $error->getMessage(), + position: $position, + offset: $offset, + ); + } + + return new FailureCheckResult( + message: $error->getMessage(), + position: $position, + offset: $offset, + ); + } + + /** + * Reads the source into the type it describes. * - * @throws ParserExceptionInterface in case of the parser cannot be built - * @throws RuntimeExceptionInterface in case of the source cannot be read - * @throws \Throwable in case of an internal error occurs + * @return SuccessfulResult|FailureResult + * @throws ParseException in case of the grammar cannot be run */ - public function analyze( - ReadableInterface $source, - Mode $mode = Mode::Tolerant, - ): SuccessfulResult|FailureResult { - $parser = $this->parser; + private function build(ReadableInterface $source): SuccessfulResult|FailureResult + { + assert($this->parser instanceof ParserRuntime); + + try { + return $this->parser->analyze($source, Mode::Tolerant); + } catch (\Throwable $e) { + throw $this->raised($e, $source); + } + } + + /** + * Reads the source without building anything of it. + * + * @return SuccessfulResult|FailureResult + * @throws ParseException in case of the grammar cannot be run + */ + private function check(ReadableInterface $source): SuccessfulResult|FailureResult + { + assert($this->parser instanceof ParserRuntime); + + try { + return $this->parser->analyze($source, Mode::SyntaxCheck); + } catch (\Throwable $e) { + throw $this->raised($e, $source); + } + } + + /** + * Converts whatever the grammar raises while it reads into the error of + * this parser. + */ + private function raised(\Throwable $e, ReadableInterface $source): ParseException + { + return match (true) { + $e instanceof ParseException => $e, + $e instanceof SemanticException + => SemanticParseException::becauseSemanticErrorOccurs($e, $source), + $e instanceof SourceExceptionInterface + => InternalParseException::becauseSourceIsUnreadable($e), + default => InternalParseException::becauseInternalErrorOccurs( + statement: $source->content, + e: $e, + ), + }; + } + + /** + * Returns the length of the source, which is the offset a reading that + * has stopped at nothing ends at. + * + * @return int<0, max> + * @throws ParseException in case of the source cannot be read + */ + private function length(ReadableInterface $source): int + { + try { + return \strlen($source->content); + } catch (\Throwable $e) { + throw $this->raised($e, $source); + } + } + + /** + * @param int<0, max> $offset + * + * @throws SourceExceptionInterface + */ + private function createPosition(ReadableInterface $source, int $offset): PositionInterface + { + return $this->positions->createFromOffset($source, $offset); + } + + /** + * Converts the error of the grammar into the error of this parser. + * + * @param FailureResult|PartialResult $result + * + * @throws SourceExceptionInterface + */ + private function createError(FailureResult|PartialResult $result, ReadableInterface $source): ParseException + { + $error = $result->error; + + if (!$error instanceof GrammarUnexpectedTokenException) { + return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( + statement: $source->content, + offset: $error->token->offset, + ); + } - if (!$parser instanceof ParserRuntime) { - throw new \LogicException('The compiled grammar must be built on top of the phplrt parser'); + // An input the lexer says nothing about is reported as an unrecognized + // one rather than as a token in a wrong place. + if ($error->token->channel === Channel::Unknown) { + return UnrecognizedTokenException::becauseTokenIsUnrecognized( + token: $error->token->value, + statement: $source->content, + offset: $error->token->offset, + ); } - return $parser->analyze($source, $mode); + return UnexpectedTokenException::becauseTokenIsUnexpected( + token: $error->token->value, + statement: $source->content, + offset: $error->token->offset, + ); } } diff --git a/libs/parser/src/ParsedResult.php b/libs/parser/src/ParsedResult.php deleted file mode 100644 index 67d5473..0000000 --- a/libs/parser/src/ParsedResult.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public readonly int $offset, - ) {} -} diff --git a/libs/parser/src/Partial/FailureParsedResult.php b/libs/parser/src/Partial/FailureParsedResult.php new file mode 100644 index 0000000..7e4b202 --- /dev/null +++ b/libs/parser/src/Partial/FailureParsedResult.php @@ -0,0 +1,32 @@ + $offset + */ + public function __construct( + /** + * What stands in the way of the reading. + */ + public readonly string $message, + /** + * The line and the column the reading has stopped at. + */ + public readonly PositionInterface $position, + /** + * The offset the reading has stopped at, which is the one the + * {@see $position} above is made of. + */ + public readonly int $offset, + ) {} +} diff --git a/libs/parser/src/Partial/ParsedResult.php b/libs/parser/src/Partial/ParsedResult.php new file mode 100644 index 0000000..3032bfa --- /dev/null +++ b/libs/parser/src/Partial/ParsedResult.php @@ -0,0 +1,18 @@ +partial($source); + * + * echo \substr($source, $result->offset); + * // => " and more" + * ``` + */ +final class PartialParsedResult extends SuccessfulParsedResult {} diff --git a/libs/parser/src/Partial/SuccessfulParsedResult.php b/libs/parser/src/Partial/SuccessfulParsedResult.php new file mode 100644 index 0000000..0e4c73b --- /dev/null +++ b/libs/parser/src/Partial/SuccessfulParsedResult.php @@ -0,0 +1,33 @@ + $offset + */ + public function __construct( + /** + * The type the source is read into. + */ + public readonly TypeNode $type, + /** + * The offset the reading has stopped at. + */ + public readonly int $offset, + ) {} +} diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php index 45dab9f..b55691a 100644 --- a/libs/parser/src/TypeParser.php +++ b/libs/parser/src/TypeParser.php @@ -5,24 +5,15 @@ namespace TypeLang\Parser; use JetBrains\PhpStorm\Language; -use Phplrt\Contracts\Lexer\Channel; -use Phplrt\Contracts\Parser\Exception\RuntimeExceptionInterface as ParserRuntimeExceptionInterface; use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; use Phplrt\Contracts\Source\ReadableInterface; use Phplrt\Contracts\Source\SourceFactoryInterface; -use Phplrt\Parser\Analysis\Result\FailureResult; -use Phplrt\Parser\Analysis\Result\PartialResult; -use Phplrt\Parser\Analysis\Result\SuccessfulResult; -use Phplrt\Parser\Exception\UnexpectedTokenException as GrammarUnexpectedTokenException; use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\InternalParseException; use TypeLang\Parser\Exception\ParseException; -use TypeLang\Parser\Exception\SemanticException; -use TypeLang\Parser\Exception\SemanticParseException; -use TypeLang\Parser\Exception\UnexpectedTokenException; -use TypeLang\Parser\Exception\UnrecognizedSyntaxException; -use TypeLang\Parser\Exception\UnrecognizedTokenException; use TypeLang\Parser\Internal\Executor; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Type\TypeNode; final class TypeParser implements TypeParserInterface @@ -58,142 +49,42 @@ public function withFeatures(bool ...$features): self public function parse(#[Language('PHP')] mixed $source): TypeNode { - $result = $this->execute($source, false); + $executor = $this->getExecutor(); - return $result->type; + return $executor->parse($this->source($source)); } - public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult + public function partial(#[Language('PHP')] mixed $source): ParsedResult { - return $this->execute($source, true); - } - - /** - * @throws ParseException in case of the source cannot be recognized - */ - private function execute(mixed $source, bool $tolerant): ParsedResult - { - try { - $instance = $this->sources->create($source); - } catch (SourceExceptionInterface $e) { - throw InternalParseException::becauseSourceIsUnreadable($e); - } - - try { - $result = $this->executor()->analyze($instance); - - // A source the grammar reads only in part is a syntax error unless - // the tolerant mode has been asked for. - if ($result instanceof FailureResult - || (!$tolerant && $result instanceof PartialResult) - ) { - throw $this->grammarError($result->error, $instance); - } - - return new ParsedResult( - type: $this->fetchTypeStatement($result), - offset: $this->fetchLastOffset($result, $instance), - ); - } catch (ParseException $e) { - throw $e; - } catch (SemanticException $e) { - throw $this->semanticError($e, $instance); - } catch (SourceExceptionInterface $e) { - throw InternalParseException::becauseSourceIsUnreadable($e); - } catch (\Throwable $e) { - throw $this->internalError($e, $instance); - } - } - - /** - * @param SuccessfulResult $result - * @throws InternalParseException in case of the grammar has built - * something else than a type statement - */ - private function fetchTypeStatement(SuccessfulResult $result): TypeNode - { - $statement = $result->value; - - if (!$statement instanceof TypeNode) { - throw InternalParseException::becauseTypeStatementIsUnreadable(); - } + $executor = $this->getExecutor(); - return $statement; + return $executor->partial($this->source($source)); } - /** - * Returns the offset the analysis has stopped at. - * - * @param SuccessfulResult $result - * @return int<0, max> - * @throws SourceExceptionInterface in case of source content reading error - */ - private function fetchLastOffset(SuccessfulResult $result, ReadableInterface $source): int + public function validate(#[Language('PHP')] mixed $source): CheckResult { - // A source that has been read in full is left at its very end. - if (!$result instanceof PartialResult) { - /** @var int<0, max> */ - return \strlen($source->content); - } + $executor = $this->getExecutor(); - return $result->token->offset; + return $executor->validate($this->source($source)); } /** - * Converts the error of the grammar into the error of this parser. - * - * @throws SourceExceptionInterface in case of source content reading error + * @throws ParseException in case of the source cannot be read */ - private function grammarError(ParserRuntimeExceptionInterface $e, ReadableInterface $source): ParseException + private function source(mixed $source): ReadableInterface { - if (!$e instanceof GrammarUnexpectedTokenException) { - return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( - statement: $source->content, - offset: $e->token->offset, - ); - } - - // An input the lexer says nothing about is reported as an unrecognized - // one rather than as a token in a wrong place. - if ($e->token->channel === Channel::Unknown) { - return UnrecognizedTokenException::becauseTokenIsUnrecognized( - token: $e->token->value, - statement: $source->content, - offset: $e->token->offset, - ); + try { + return $this->sources->create($source); + } catch (SourceExceptionInterface $e) { + throw InternalParseException::becauseSourceIsUnreadable($e); } - - return UnexpectedTokenException::becauseTokenIsUnexpected( - token: $e->token->value, - statement: $source->content, - offset: $e->token->offset, - ); - } - - /** - * @throws SourceExceptionInterface in case of source content reading error - */ - private function semanticError(SemanticException $e, ReadableInterface $source): ParseException - { - return SemanticParseException::becauseSemanticErrorOccurs($e, $source); - } - - /** - * @throws SourceExceptionInterface in case of source content reading error - */ - private function internalError(\Throwable $e, ReadableInterface $source): ParseException - { - return InternalParseException::becauseInternalErrorOccurs( - statement: $source->content, - e: $e, - ); } /** * Returns a lazily created parser recognizing a source with the features * of this one. */ - private function executor(): Executor + private function getExecutor(): Executor { return $this->executor ??= new Executor($this->features); } diff --git a/libs/parser/src/TypeParserInterface.php b/libs/parser/src/TypeParserInterface.php index b2163cc..2b9d99f 100644 --- a/libs/parser/src/TypeParserInterface.php +++ b/libs/parser/src/TypeParserInterface.php @@ -6,6 +6,8 @@ use JetBrains\PhpStorm\Language; use TypeLang\Parser\Exception\ParserExceptionInterface; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Type\TypeNode; /** @@ -49,7 +51,7 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode; * code. * * ``` - * $result = $parser->tolerant('array{ field: result } This is an example'); + * $result = $parser->partial('array{ field: result } This is an example'); * * $result->type; * // => NamedTypeNode{ name: "array", ... @@ -61,7 +63,7 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode; * To get information about the remaining part, just apply `substr` to source: * ``` * $source = 'array{ field: result } This is an example'; - * $result = $parser->tolerant($source); + * $result = $parser->partial($source); * * echo substr($source, $result->offset); * // => "This is an example" @@ -73,5 +75,31 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode; * @throws ParserExceptionInterface in case of parsing exception occurs * @throws \Throwable in case of internal error occurs */ - public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult; + public function partial(#[Language('PHP')] mixed $source): ParsedResult; + + /** + * Tells whether the provided source code is a type, building nothing + * of it. + * + * Nothing is built while a source is checked, so this method is the + * cheaper one of the three whenever the type itself is of no use. + * + * ``` + * $parser->validate('array{ field: result }'); + * // => SuccessfulCheckResult + * + * $parser->validate('array{ field: result } and more'); + * // => PartialCheckResult + * + * $parser->validate('array{'); + * // => FailureCheckResult + * ``` + * + * @param TSource $source source code to check + * @return CheckResult what stands in the way of the reading, in case of + * anything does + * @throws ParserExceptionInterface in case of parsing exception occurs + * @throws \Throwable in case of internal error occurs + */ + public function validate(#[Language('PHP')] mixed $source): CheckResult; } diff --git a/libs/parser/src/Validation/CheckResult.php b/libs/parser/src/Validation/CheckResult.php new file mode 100644 index 0000000..7cf9923 --- /dev/null +++ b/libs/parser/src/Validation/CheckResult.php @@ -0,0 +1,12 @@ + $offset + */ + public function __construct( + /** + * What stands in the way of the reading. + */ + public readonly string $message, + /** + * The line and the column the reading has stopped at. + */ + public readonly PositionInterface $position, + /** + * The offset the reading has stopped at, which is the one the + * {@see $position} above is made of. + */ + public readonly int $offset, + ) {} +} diff --git a/libs/parser/src/Validation/PartialCheckResult.php b/libs/parser/src/Validation/PartialCheckResult.php new file mode 100644 index 0000000..347eb16 --- /dev/null +++ b/libs/parser/src/Validation/PartialCheckResult.php @@ -0,0 +1,11 @@ + Date: Tue, 8 Sep 2026 02:58:45 +0300 Subject: [PATCH 089/119] Fix parser tests --- .../tests/Exception/ParseExceptionTest.php | 12 +-- .../parser/tests/Syntax/FeatureToggleTest.php | 2 +- .../tests/Syntax/MalformedInputTest.php | 78 +++++++++++++++++-- libs/parser/tests/TestCase.php | 23 +++++- libs/parser/tests/TypeParserTest.php | 33 ++++---- 5 files changed, 107 insertions(+), 41 deletions(-) diff --git a/libs/parser/tests/Exception/ParseExceptionTest.php b/libs/parser/tests/Exception/ParseExceptionTest.php index e3865bb..5d41208 100644 --- a/libs/parser/tests/Exception/ParseExceptionTest.php +++ b/libs/parser/tests/Exception/ParseExceptionTest.php @@ -21,7 +21,7 @@ final class ParseExceptionTest extends TestCase #[Test] public function everyParseExceptionIsAParserException(): void { - $exception = InternalParseException::becauseTypeStatementIsUnreadable(); + $exception = InternalParseException::becauseInternalErrorOccurs('int', new \LogicException()); self::assertInstanceOf(ParserExceptionInterface::class, $exception); self::assertInstanceOf(\LogicException::class, $exception); @@ -138,16 +138,6 @@ public function theInternalErrorKeepsThePreviousException(): void self::assertSame(ParseException::ERROR_CODE_INTERNAL_ERROR, $exception->getCode()); } - #[Test] - public function theUnreadableStatementIsReported(): void - { - $exception = InternalParseException::becauseTypeStatementIsUnreadable(); - - self::assertSame('Could not read type statement', $exception->getMessage()); - self::assertSame(ParseException::ERROR_CODE_INTERNAL_ERROR, $exception->getCode()); - self::assertNull($exception->getPrevious()); - } - #[Test] public function theUnreadableSourceIsReportedUsingTheOriginalMessage(): void { diff --git a/libs/parser/tests/Syntax/FeatureToggleTest.php b/libs/parser/tests/Syntax/FeatureToggleTest.php index da44097..bee253a 100644 --- a/libs/parser/tests/Syntax/FeatureToggleTest.php +++ b/libs/parser/tests/Syntax/FeatureToggleTest.php @@ -54,7 +54,7 @@ public function testTrailingTextIsRejectedInStrictMode(): void public function testTrailingTextIsAllowedInTolerantMode(): void { - $result = $this->parseTolerant('int and more text'); + $result = $this->partial('int and more text'); $type = $result->type; self::assertInstanceOf(NamedTypeNode::class, $type); diff --git a/libs/parser/tests/Syntax/MalformedInputTest.php b/libs/parser/tests/Syntax/MalformedInputTest.php index 692104f..ea3a313 100644 --- a/libs/parser/tests/Syntax/MalformedInputTest.php +++ b/libs/parser/tests/Syntax/MalformedInputTest.php @@ -6,6 +6,13 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use TypeLang\Parser\Partial\FailureParsedResult; +use TypeLang\Parser\Partial\PartialParsedResult; +use TypeLang\Parser\Partial\SuccessfulParsedResult; +use TypeLang\Parser\Validation\CheckResult; +use TypeLang\Parser\Validation\FailureCheckResult; +use TypeLang\Parser\Validation\PartialCheckResult; +use TypeLang\Parser\Validation\SuccessfulCheckResult; /** * Tests for the input a type cannot be read out of, that is, the one that is @@ -131,7 +138,7 @@ public function testATokenThatFollowsAWholeTypeIsRefused(string $type, string $m } /** - * A tolerant reading keeps whatever type it has read and says where it + * A partial reading keeps whatever type it has read and says where it * stopped, rather than refusing the input whole. * * @return iterable}> @@ -151,25 +158,80 @@ public static function tolerantInputDataProvider(): iterable * @throws \Throwable */ #[DataProvider('tolerantInputDataProvider')] - public function testATolerantReadingStopsAtWhatItCannotRead( + public function testAPartialReadingStopsAtWhatItCannotRead( string $type, string $expected, int $offset, ): void { - $result = $this->parseTolerant($type); + $result = $this->partial($type); + self::assertInstanceOf(PartialParsedResult::class, $result); self::assertSame($expected, (new \TypeLang\Printer\PrettyTypePrinter())->print($result->type)); self::assertSame($offset, $result->offset); } /** - * A tolerant reading is tolerant of a tail alone, so an input that opens - * no type at all is refused all the same. + * A source read in full is no partial one, so it carries no offset of + * its own. */ - public function testATolerantReadingRefusesAnInputThatOpensNoType(): void + public function testAWholeTypeIsNoPartialReading(): void { - $this->expectParsingException(); + $result = $this->partial('array{a: int}'); - $this->parseTolerant('|int'); + self::assertInstanceOf(SuccessfulParsedResult::class, $result); + self::assertNotInstanceOf(PartialParsedResult::class, $result); + } + + /** + * A partial reading is tolerant of a tail alone, so an input that opens + * no type at all is a failure all the same. + */ + public function testAPartialReadingRefusesAnInputThatOpensNoType(): void + { + $result = $this->partial('|int'); + + self::assertInstanceOf(FailureParsedResult::class, $result); + self::assertStringContainsString('unexpected "|"', $result->message); + self::assertSame(0, $result->offset); + self::assertSame(1, $result->position->line); + self::assertSame(1, $result->position->column); + } + + /** + * @return iterable}> + */ + public static function checkedInputDataProvider(): iterable + { + yield 'a whole type' => ['array{a: int}', SuccessfulCheckResult::class]; + yield 'a type and a tail' => ['array{a: int} tail', PartialCheckResult::class]; + yield 'no type at all' => ['|int', FailureCheckResult::class]; + yield 'an empty source' => ['', FailureCheckResult::class]; + } + + /** + * A check builds nothing, so it says what stands in the way and nothing + * else. + * + * @param non-empty-string $type + * @param class-string $expected + * @throws \Throwable + */ + #[DataProvider('checkedInputDataProvider')] + public function testACheckTellsWhetherASourceIsAWholeType(string $type, string $expected): void + { + self::assertInstanceOf($expected, $this->validate($type)); + } + + /** + * A source read in part is a failure of a check, since a check asks + * about the source whole. + */ + public function testAPartialCheckIsAFailure(): void + { + $result = $this->validate('array{a: int} tail'); + + self::assertInstanceOf(FailureCheckResult::class, $result); + self::assertSame(14, $result->offset); + self::assertStringContainsString('unexpected "tail"', $result->message); } } diff --git a/libs/parser/tests/TestCase.php b/libs/parser/tests/TestCase.php index fba3ad4..ff3f499 100644 --- a/libs/parser/tests/TestCase.php +++ b/libs/parser/tests/TestCase.php @@ -7,8 +7,10 @@ use JetBrains\PhpStorm\Language; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase as BaseTestCase; -use TypeLang\Parser\ParsedResult; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Partial\SuccessfulParsedResult; use TypeLang\Parser\Traverser; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Parser\TypeParser; use TypeLang\Parser\TypeParserFeatures; use TypeLang\Parser\TypeParserInterface; @@ -61,11 +63,22 @@ protected function parse(#[Language('PHP')] string $code, array $options = []): * @param ParserOptionsType $options * @throws \Throwable */ - protected function parseTolerant(#[Language('PHP')] string $code, array $options = []): ParsedResult + protected function partial(#[Language('PHP')] string $code, array $options = []): ParsedResult { $parser = $this->parser($options); - return $parser->parseTolerant($code); + return $parser->partial($code); + } + + /** + * @param ParserOptionsType $options + * @throws \Throwable + */ + protected function validate(#[Language('PHP')] string $code, array $options = []): CheckResult + { + $parser = $this->parser($options); + + return $parser->validate($code); } protected function print(TypeNode $statement): string @@ -93,7 +106,9 @@ protected function parseAndPrint(#[Language('PHP')] string $code, array $options */ protected function tolerantParseAndPrint(#[Language('PHP')] string $code, array $options = []): string { - $result = $this->parseTolerant($code, $options); + $result = $this->partial($code, $options); + + self::assertInstanceOf(SuccessfulParsedResult::class, $result); return $this->print($result->type); } diff --git a/libs/parser/tests/TypeParserTest.php b/libs/parser/tests/TypeParserTest.php index 6c40623..9c35ffc 100644 --- a/libs/parser/tests/TypeParserTest.php +++ b/libs/parser/tests/TypeParserTest.php @@ -7,7 +7,9 @@ use PHPUnit\Framework\Attributes\Test; use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\ParseException; -use TypeLang\Parser\ParsedResult; +use TypeLang\Parser\Partial\FailureParsedResult; +use TypeLang\Parser\Partial\PartialParsedResult; +use TypeLang\Parser\Partial\SuccessfulParsedResult; use TypeLang\Parser\TypeParser; use TypeLang\Parser\TypeParserFeatures; use TypeLang\Type\Name; @@ -109,42 +111,39 @@ public function theSourceFactoryIsInheritedByTheNewInstance(): void } #[Test] - public function parseTolerantReturnsAResult(): void + public function aPartialReadingOfAWholeSourceIsASuccessfulOne(): void { - self::assertInstanceOf(ParsedResult::class, (new TypeParser())->parseTolerant('int')); - } - - #[Test] - public function theOffsetOfACompletelyParsedSourceIsItsLength(): void - { - $result = (new TypeParser())->parseTolerant('int|string'); + $result = (new TypeParser())->partial('int'); - self::assertSame(10, $result->offset); + self::assertInstanceOf(SuccessfulParsedResult::class, $result); + self::assertNotInstanceOf(PartialParsedResult::class, $result); + self::assertInstanceOf(NamedTypeNode::class, $result->type); + self::assertSame('int', $result->type->name->toString()); } #[Test] public function theOffsetPointsToTheUnparsedTail(): void { $source = 'array{ field: result } This is an example'; - $result = (new TypeParser())->parseTolerant($source); + $result = (new TypeParser())->partial($source); + self::assertInstanceOf(PartialParsedResult::class, $result); self::assertSame('This is an example', \substr($source, $result->offset)); } #[Test] - public function theResultContainsTheParsedType(): void + public function aPartialReadingOfNoTypeAtAllIsAFailure(): void { - $result = (new TypeParser())->parseTolerant('int'); + $result = (new TypeParser())->partial('|int'); - self::assertInstanceOf(NamedTypeNode::class, $result->type); - self::assertSame('int', $result->type->name->toString()); + self::assertInstanceOf(FailureParsedResult::class, $result); } #[Test] - public function parsedResultCanBeCreatedManually(): void + public function aPartialResultCanBeCreatedManually(): void { $type = new NamedTypeNode(Name::createFromString('int')); - $result = new ParsedResult($type, 42); + $result = new PartialParsedResult($type, 42); self::assertSame($type, $result->type); self::assertSame(42, $result->offset); From 91fe7f96ccd62873d0fe9809d9a6a40c87424d7f Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 02:58:54 +0300 Subject: [PATCH 090/119] Fix phpstan errors --- libs/parser/phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/parser/phpstan.neon b/libs/parser/phpstan.neon index 5c59721..bf3ffa2 100644 --- a/libs/parser/phpstan.neon +++ b/libs/parser/phpstan.neon @@ -12,5 +12,5 @@ parameters: analyse: # The parser is generated out of the grammar by the phplrt compiler and # is not written by hand. - - src/Internal/CompilerExecutor.php + - src/Internal/CompiledExecutor.php reportUnmatchedIgnoredErrors: false From ba8a4e2bb57752327c3a058c983bd6a1d2385ec1 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 02:59:12 +0300 Subject: [PATCH 091/119] Fix phpdoc TypeCombinator compatibility --- libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php index 57b3455..86f8563 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php @@ -4,6 +4,7 @@ namespace TypeLang\PhpDoc\DocBlock\Combinator; +use TypeLang\Parser\Partial\SuccessfulParsedResult; use TypeLang\Parser\TypeParserInterface; use TypeLang\PhpDoc\DocBlock\Reference\TypeReference; use TypeLang\PhpDoc\Parser\Grammar\CombinatorInterface; @@ -35,10 +36,11 @@ public function __invoke(Cursor $cursor): TypeReference throw new NoMatchException('Expected a type'); } - // Tolerant parsing yields the type together with the offset of the next - // token after it, so the cursor is left at the start of the trailing - // text (e.g. a description) rather than at the end of the buffer. - $result = $this->typeParser->parseTolerant($source); + $result = $this->typeParser->partial($source); + + if (!$result instanceof SuccessfulParsedResult) { + throw new NoMatchException($result->message); + } $cursor->moveTo($start + $result->offset); From 784270bfcbcf615ebea863218f599b4db62f8153 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 04:54:17 +0300 Subject: [PATCH 092/119] Fix php 8.1 compatibility issues in types --- libs/types/tests/Shape/ImplicitFieldNodeTest.php | 1 - libs/types/tests/Template/TemplateArgumentNodeTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/libs/types/tests/Shape/ImplicitFieldNodeTest.php b/libs/types/tests/Shape/ImplicitFieldNodeTest.php index 0ce2842..d1e70df 100644 --- a/libs/types/tests/Shape/ImplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ImplicitFieldNodeTest.php @@ -40,7 +40,6 @@ public function constructorStoresOptionalFlag(): void self::assertTrue($node->isOptional); } - #[Test] #[Test] public function defaultOffsetIsZero(): void { diff --git a/libs/types/tests/Template/TemplateArgumentNodeTest.php b/libs/types/tests/Template/TemplateArgumentNodeTest.php index 8b2ed29..62560ea 100644 --- a/libs/types/tests/Template/TemplateArgumentNodeTest.php +++ b/libs/types/tests/Template/TemplateArgumentNodeTest.php @@ -30,7 +30,6 @@ public function hintDefaultsToNull(): void self::assertNull($node->hint); } - #[Test] #[Test] public function constructorAcceptsIdentifierHint(): void { From d34f33dd91d46513376ae84f2cf1d7fed34bcc08 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 04:54:34 +0300 Subject: [PATCH 093/119] Fix doc block parser php 8.1 compat --- libs/phpdoc/src/DocBlockParser.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/phpdoc/src/DocBlockParser.php b/libs/phpdoc/src/DocBlockParser.php index 3c6b193..75b75b6 100644 --- a/libs/phpdoc/src/DocBlockParser.php +++ b/libs/phpdoc/src/DocBlockParser.php @@ -124,8 +124,9 @@ private function createTagRegistry(array $platforms): TagRegistryInterface } } - return new TagRegistryBuilder($definitions, $aliases) - ->build(); + $builder = new TagRegistryBuilder($definitions, $aliases); + + return $builder->build(); } /** From afd518954a382110bc35ef6043ba8fa737b6b652 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 06:31:14 +0300 Subject: [PATCH 094/119] Fix php-cs-fixer config --- .php-cs-fixer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index d95cfb6..69aa1a9 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -277,7 +277,7 @@ 'unary_operator_spaces' => true, 'whitespace_after_comma_in_array' => true, 'yoda_style' => false, - 'new_expression_parentheses' => true, + 'new_expression_parentheses' => ['use_parentheses' => true], ]) ->setCacheFile(__DIR__ . '/vendor/.cache.php-cs-fixer') ->setFinder($files); From 48fa9072296226b015beeba1114889065b49a1ec Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 06:33:22 +0300 Subject: [PATCH 095/119] Fix phpdoc tests --- .../Description/TaggedDescriptionTest.php | 2 +- libs/phpdoc/tests/DocBlock/DocBlockTest.php | 2 +- .../Grammar/DescriptionGrammarRuleTest.php | 39 +++++++++++-------- .../phpdoc/tests/DocBlock/Tag/TagTestCase.php | 10 ++--- libs/phpdoc/tests/Parser/SplitterTest.php | 4 +- libs/phpdoc/tests/TestCase.php | 35 ++++++----------- 6 files changed, 44 insertions(+), 48 deletions(-) diff --git a/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php b/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php index f7a20b4..76ea5c2 100644 --- a/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php +++ b/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php @@ -124,7 +124,7 @@ public function iteratorYieldsAllComponentsInOrder(): void $components = [new Description('a'), new Tag('see'), new Description('b')]; $description = new TaggedDescription($components); - self::assertSame($components, \iterator_to_array($description, false)); + self::assertSame($components, \iterator_to_array($description->getIterator(), false)); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/DocBlockTest.php b/libs/phpdoc/tests/DocBlock/DocBlockTest.php index 1d85e06..3d1d52b 100644 --- a/libs/phpdoc/tests/DocBlock/DocBlockTest.php +++ b/libs/phpdoc/tests/DocBlock/DocBlockTest.php @@ -111,6 +111,6 @@ public function iteratorYieldsAllTagsInOrder(): void $tags = [new Tag('param'), new Tag('return')]; $docblock = new DocBlock(null, $tags); - self::assertSame($tags, \iterator_to_array($docblock, false)); + self::assertSame($tags, \iterator_to_array($docblock->getIterator(), false)); } } diff --git a/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php index 8cf7456..dfb0942 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php @@ -21,23 +21,28 @@ final class DescriptionGrammarRuleTest extends GrammarRuleTestCase { protected function rule(): DescriptionCombinator { - return (new \ReflectionClass(DescriptionCombinator::class)) - ->newLazyProxy(function (DescriptionCombinator $proxy) { - $registry = new TagRegistry( - definitions: [], - genericTagDefinition: new GenericTagDefinition(), - ); - - return new DescriptionCombinator( - descriptionParser: new BalancedBraceAwareParser( - tagParser: new StringTagParser( - tagFactory: new TagFactory($registry, [ - DescriptionCombinator::NAME => $proxy, - ]), - ), - ), - ); - }); + $registry = new TagRegistry( + definitions: [], + genericTagDefinition: new GenericTagDefinition(), + ); + + $rule = null; + + $nested = static function (Cursor $cursor) use (&$rule): mixed { + \assert($rule instanceof DescriptionCombinator); + + return $rule($cursor); + }; + + return $rule = new DescriptionCombinator( + descriptionParser: new BalancedBraceAwareParser( + tagParser: new StringTagParser( + tagFactory: new TagFactory($registry, [ + DescriptionCombinator::NAME => $nested, + ]), + ), + ), + ); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php b/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php index 4551802..7733a5b 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php +++ b/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php @@ -82,16 +82,16 @@ protected function createPlatform( array $combinators = [], array $aliases = [], ): PlatformInterface { - return new readonly class ('testing', $tags, $aliases, $combinators) implements PlatformInterface { + return new class ('testing', $tags, $aliases, $combinators) implements PlatformInterface { public function __construct( /** @var non-empty-string */ - public string $name, + public readonly string $name, /** @var iterable */ - public iterable $tags, + public readonly iterable $tags, /** @var iterable */ - public iterable $aliases, + public readonly iterable $aliases, /** @var iterable */ - public iterable $combinators, + public readonly iterable $combinators, ) {} }; } diff --git a/libs/phpdoc/tests/Parser/SplitterTest.php b/libs/phpdoc/tests/Parser/SplitterTest.php index d97b22c..898090c 100644 --- a/libs/phpdoc/tests/Parser/SplitterTest.php +++ b/libs/phpdoc/tests/Parser/SplitterTest.php @@ -345,7 +345,9 @@ private static function comment(string ...$lines): string */ private static function segments(iterable $result): array { - return \iterator_to_array($result, false); + return \is_array($result) + ? \array_values($result) + : \iterator_to_array($result, false); } /** diff --git a/libs/phpdoc/tests/TestCase.php b/libs/phpdoc/tests/TestCase.php index 2bf4190..20caf1a 100644 --- a/libs/phpdoc/tests/TestCase.php +++ b/libs/phpdoc/tests/TestCase.php @@ -14,6 +14,7 @@ use TypeLang\PhpDoc\DocBlock\Combinator\VariableCombinator; use TypeLang\PhpDoc\Parser\Description\BalancedBraceAwareParser; use TypeLang\PhpDoc\Parser\Description\DescriptionParserInterface; +use TypeLang\PhpDoc\Parser\Grammar\Cursor; use TypeLang\PhpDoc\Parser\Tag\StringTagParser; use TypeLang\PhpDoc\Parser\TagFactory; use TypeLang\PhpDoc\Parser\TagRegistryBuilder; @@ -42,32 +43,20 @@ private static function buildTagFactory(): TagFactoryInterface ]; $tagFactory = null; + $description = null; - $baseRules[DescriptionCombinator::NAME] = (new \ReflectionClass(DescriptionCombinator::class)) - ->newLazyProxy(function () use (&$tagFactory): DescriptionCombinator { - if ($tagFactory === null) { - return new DescriptionCombinator(new BalancedBraceAwareParser( - new StringTagParser(new TagFactory( - registry: (new TagRegistryBuilder()) - ->build(), - combinators: [ - UriCombinator::NAME => new UriCombinator(), - ReferenceCombinator::NAME => new ReferenceCombinator(), - TypeCombinator::NAME => new TypeCombinator(typeParser: new TypeParser()), - VariableCombinator::NAME => new VariableCombinator(), - ], - )), - )); - } - - return new DescriptionCombinator( - new BalancedBraceAwareParser(new StringTagParser($tagFactory)), - ); - }); + $baseRules[DescriptionCombinator::NAME] = static function (Cursor $cursor) use ( + &$tagFactory, + &$description, + ): mixed { + $description ??= new DescriptionCombinator( + new BalancedBraceAwareParser(new StringTagParser($tagFactory)), + ); - $tagFactory = new TagFactory((new TagRegistryBuilder())->build(), $baseRules); + return $description($cursor); + }; - return $tagFactory; + return $tagFactory = new TagFactory((new TagRegistryBuilder())->build(), $baseRules); } protected static function createDescriptionParser(): DescriptionParserInterface From 4542153d00148ef4ea71d15def778d76c63e0d8a Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 06:33:54 +0300 Subject: [PATCH 096/119] Improve and reorganize parser exceptions --- libs/parser/resources/grammar.pp3 | 34 +- libs/parser/resources/grammar/callable.pp3 | 6 +- libs/parser/resources/grammar/common.pp3 | 9 +- libs/parser/resources/grammar/literals.pp3 | 2 +- libs/parser/resources/grammar/named-type.pp3 | 8 +- .../parser/resources/grammar/shape-fields.pp3 | 12 +- .../resources/grammar/template-arguments.pp3 | 8 +- .../resources/grammar/template-parameters.pp3 | 15 +- .../src/Exception/ConstMaskException.php | 23 +- .../Exception/FeatureNotAllowedException.php | 20 +- libs/parser/src/Exception/Formatter.php | 68 -- .../src/Exception/InternalParseException.php | 18 +- .../Exception/InternalSemanticException.php | 23 +- .../InvalidConditionalOperatorException.php | 24 +- libs/parser/src/Exception/ParseException.php | 29 - libs/parser/src/Exception/ParserException.php | 73 ++ .../Exception/ParserExceptionInterface.php | 5 + .../src/Exception/SemanticException.php | 35 +- .../src/Exception/SemanticParseException.php | 28 - .../ShapeFieldDuplicationException.php | 20 +- .../src/Exception/ShapeKeyException.php | 23 +- .../Exception/ShapeKeysMixingException.php | 21 +- .../src/Exception/TemplateBoundException.php | 73 +- .../Exception/UnexpectedTokenException.php | 24 +- .../Exception/UnrecognizedSyntaxException.php | 16 +- .../Exception/UnrecognizedTokenException.php | 18 +- .../VariadicWithDefaultException.php | 21 +- libs/parser/src/Internal/CompiledExecutor.php | 891 ++++++++++++------ libs/parser/src/Internal/Executor.php | 53 +- libs/parser/src/Traverser/DumperVisitor.php | 4 +- libs/parser/src/TypeParser.php | 11 +- libs/parser/src/TypeResolver.php | 8 +- .../TypeResolver/PhpUseStatementsReader.php | 18 +- .../NamespaceFinder.php | 23 + 34 files changed, 996 insertions(+), 668 deletions(-) delete mode 100644 libs/parser/src/Exception/Formatter.php delete mode 100644 libs/parser/src/Exception/ParseException.php create mode 100644 libs/parser/src/Exception/ParserException.php delete mode 100644 libs/parser/src/Exception/SemanticParseException.php diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3 index 8dd08a7..8d8c43b 100644 --- a/libs/parser/resources/grammar.pp3 +++ b/libs/parser/resources/grammar.pp3 @@ -31,7 +31,7 @@ Type -> { } if ($this->features->conditions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset); } $condition = $children[1] @@ -51,8 +51,9 @@ Type -> { TernaryExpressionSuffix : TernaryExpressionOperator() (Type() | Variable()) - ::T_QMARK:: Type() - ::T_COLON:: Type() + ::T_QMARK:: Type() @error("Syntax error, unexpected {token}, a condition must carry the type it is true of") + ::T_COLON:: @error("Syntax error, unexpected {token}, a condition must be parted with a colon \":\"") + Type() @error("Syntax error, unexpected {token}, a condition must carry the type it is false of") ; // An "is" or an "is not" @@ -78,7 +79,7 @@ UnionType -> { } if ($this->features->unions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset); } $statement = new Type\UnionTypeNode($children); @@ -86,7 +87,11 @@ UnionType -> { return $statement; } - : IntersectionType() (::T_OR:: IntersectionType())* + : IntersectionType() UnionTail()* + ; + +UnionTail + : ::T_OR:: IntersectionType() @error("Syntax error, unexpected {token}, a union type must carry a type after the vertical bar \"|\", for example \"T|U\"") ; IntersectionType -> { @@ -95,7 +100,7 @@ IntersectionType -> { } if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); } $statement = new Type\IntersectionTypeNode($children); @@ -103,7 +108,11 @@ IntersectionType -> { return $statement; } - : UnaryType() (::T_AMP:: UnaryType())* + : UnaryType() IntersectionTail()* + ; + +IntersectionTail + : ::T_AMP:: UnaryType() @error("Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand \"&\", for example \"T&U\"") ; /** @@ -120,7 +129,7 @@ UnaryType NullableType -> { return new Type\NullableTypeNode($children[0], $offset); } - : ::T_QMARK:: ListOrOffsetType() + : ::T_QMARK:: ListOrOffsetType() @error("Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example \"?T\"") ; ListOrOffsetType -> { @@ -133,14 +142,14 @@ ListOrOffsetType -> { switch (true) { case $child === true: if ($this->features->lists === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $source, $offset); } $statement = new Type\TypesListNode($statement, $offset); break; case $child instanceof Type\TypeNode: if ($this->features->offsets === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $source, $offset); } $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset); @@ -148,6 +157,7 @@ ListOrOffsetType -> { default: throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( \get_debug_type($child), + $source, $offset, ); } @@ -164,11 +174,11 @@ ListOrOffsetSuffix -> { } : ::T_SQUARE_BRACKET_OPEN:: Type()? - ::T_SQUARE_BRACKET_CLOSE:: + ::T_SQUARE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, an offset must be closed with a bracket \"]\"") ; PrimaryType - : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: + : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: @error("Syntax error, unexpected {token}, a group must be closed with a bracket \")\"") | ThisType() | LiteralType() | NamedType() diff --git a/libs/parser/resources/grammar/callable.pp3 b/libs/parser/resources/grammar/callable.pp3 index ae91242..7326ab5 100644 --- a/libs/parser/resources/grammar/callable.pp3 +++ b/libs/parser/resources/grammar/callable.pp3 @@ -3,7 +3,7 @@ CallableSuffix : ::T_PARENTHESIS_OPEN:: CallableParameters() - ::T_PARENTHESIS_CLOSE:: + ::T_PARENTHESIS_CLOSE:: @error("Syntax error, unexpected {token}, a parameter list must be closed with a bracket \")\"") CallableReturnType()? ; @@ -46,7 +46,7 @@ CallableParameter -> { default: if ($parameter->isVariadic) { - throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); + throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset); } $parameter->isOptional = true; @@ -63,5 +63,5 @@ CallableParameter -> { ; CallableReturnType - : ::T_COLON:: Type() + : ::T_COLON:: Type() @error("Syntax error, unexpected {token}, a callable must carry a type after the colon \":\"") ; diff --git a/libs/parser/resources/grammar/common.pp3 b/libs/parser/resources/grammar/common.pp3 index e273ef2..6cfc663 100644 --- a/libs/parser/resources/grammar/common.pp3 +++ b/libs/parser/resources/grammar/common.pp3 @@ -9,14 +9,19 @@ Name FullQualifiedName -> { return new Type\Name($children, true, $offset); } - : ::T_NS_DELIMITER:: Identifier() (::T_NS_DELIMITER:: Identifier())* + : ::T_NS_DELIMITER:: Identifier() @error("Syntax error, unexpected {token}, a name must carry a segment after the separator, for example \"\Some\"") + NameTail()* ; // "Some\Any" RelativeName -> { return new Type\Name($children, false, $offset); } - : Identifier() (::T_NS_DELIMITER:: Identifier())* + : Identifier() NameTail()* + ; + +NameTail + : ::T_NS_DELIMITER:: Identifier() @error("Syntax error, unexpected {token}, a name must carry a segment after the separator, for example \"Some\Any\"") ; Identifier -> { diff --git a/libs/parser/resources/grammar/literals.pp3 b/libs/parser/resources/grammar/literals.pp3 index 2f2dc7d..dea24e6 100644 --- a/libs/parser/resources/grammar/literals.pp3 +++ b/libs/parser/resources/grammar/literals.pp3 @@ -1,7 +1,7 @@ LiteralType -> { if ($this->features->literals === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset); } return $children; } diff --git a/libs/parser/resources/grammar/named-type.pp3 b/libs/parser/resources/grammar/named-type.pp3 index c835f69..4825450 100644 --- a/libs/parser/resources/grammar/named-type.pp3 +++ b/libs/parser/resources/grammar/named-type.pp3 @@ -11,7 +11,7 @@ NamedType -> { if ($head instanceof Type\ConstMaskNode) { if ($suffix !== null) { - throw Exception\ConstMaskException::becauseNothingFollowsAMask($offset); + throw Exception\ConstMaskException::becauseNothingFollowsAMask($source, $offset); } return $head; @@ -33,7 +33,7 @@ NamedType -> { // Some\Any(T, U): V case $suffix instanceof Type\Callable\CallableParameterListNode: if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); } return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); @@ -41,7 +41,7 @@ NamedType -> { // Some\Any(T): U case $suffix instanceof Type\Template\TemplateParameterListNode: if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); } return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); @@ -125,5 +125,5 @@ ClassConstSuffix : ( Identifier() MaskTail()? | MaskTail() - ) + ) @error("Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example \"Some::CONST\"") ; diff --git a/libs/parser/resources/grammar/shape-fields.pp3 b/libs/parser/resources/grammar/shape-fields.pp3 index 5992230..bdd0479 100644 --- a/libs/parser/resources/grammar/shape-fields.pp3 +++ b/libs/parser/resources/grammar/shape-fields.pp3 @@ -7,7 +7,7 @@ ShapeFields -> { } if ($this->features->shapes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $source, $offset); } $parameters = null; @@ -42,7 +42,7 @@ ShapeFields -> { | ShapeFieldsUnsealed()? ) ::T_COMMA::? - ::T_BRACE_CLOSE:: + ::T_BRACE_CLOSE:: @error("Syntax error, unexpected {token}, a shape must be closed with a brace \"}\"") ; ShapeFieldsUnsealed @@ -70,14 +70,14 @@ ShapeFieldsList -> { $key = $field->getIndex(); if (isset($indexed[$key])) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $source, $field->offset); } $indexed[$key] = true; } if ($explicit && $implicit) { - throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); + throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset); } return new Type\Shape\FieldsListNode($children, true, $offset); @@ -111,10 +111,10 @@ ExplicitField -> { $name instanceof Type\ClassConstMaskNode, $name instanceof Type\ConstMaskNode => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset), - default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($name->offset), + default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($source, $name->offset), }; } - : ShapeKey() ()? ::T_COLON:: ShapeValue() + : ShapeKey() ()? ::T_COLON:: ShapeValue() @error("Syntax error, unexpected {token}, a shape field must carry a type after the colon \":\"") ; ImplicitField -> { diff --git a/libs/parser/resources/grammar/template-arguments.pp3 b/libs/parser/resources/grammar/template-arguments.pp3 index fdcdc1d..c8b8e3d 100644 --- a/libs/parser/resources/grammar/template-arguments.pp3 +++ b/libs/parser/resources/grammar/template-arguments.pp3 @@ -6,22 +6,22 @@ TemplateArguments -> { if ($this->features->generics === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $source, $offset); } return new Type\Template\TemplateArgumentListNode($children, $offset); } : ::T_ANGLE_BRACKET_OPEN:: - TemplateArgument() ( + TemplateArgument() @error("Syntax error, unexpected {token}, an argument list must carry at least one argument") ( ::T_COMMA:: TemplateArgument() )* ::T_COMMA::? - ::T_ANGLE_BRACKET_CLOSE:: + ::T_ANGLE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, an argument list must be closed with a bracket \">\"") ; TemplateArgument -> { if (\is_array($children)) { if ($this->features->hints === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $source, $offset); } return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset); diff --git a/libs/parser/resources/grammar/template-parameters.pp3 b/libs/parser/resources/grammar/template-parameters.pp3 index f831b16..7d316cf 100644 --- a/libs/parser/resources/grammar/template-parameters.pp3 +++ b/libs/parser/resources/grammar/template-parameters.pp3 @@ -6,16 +6,16 @@ TemplateParameters -> { if ($this->features->generics === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $source, $offset); } return new Type\Template\TemplateParameterListNode($children, $offset); } : ::T_ANGLE_BRACKET_OPEN:: - TemplateParameter() ( + TemplateParameter() @error("Syntax error, unexpected {token}, a parameter list must carry at least one parameter") ( ::T_COMMA:: TemplateParameter() )* ::T_COMMA::? - ::T_ANGLE_BRACKET_CLOSE:: + ::T_ANGLE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, a parameter list must be closed with a bracket \">\"") ; TemplateParameter -> { @@ -24,7 +24,7 @@ TemplateParameter -> { foreach (\array_slice($children, 1) as $limit) { if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) { if ($default !== null) { - throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $limit->offset); + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $source, $limit->offset); } $default = $limit; @@ -33,14 +33,14 @@ TemplateParameter -> { } if ($default !== null) { - throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($limit->operator->offset); + throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($source, $limit->operator->offset); } switch ($limit->operator->value) { case 'of': case 'as': if ($upper !== null) { - throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $limit->offset); + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $source, $limit->offset); } $upper = $limit; @@ -48,7 +48,7 @@ TemplateParameter -> { case 'super': if ($lower !== null) { - throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $limit->offset); + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $source, $limit->offset); } $lower = $limit; @@ -57,6 +57,7 @@ TemplateParameter -> { default: throw Exception\TemplateBoundException::becauseOperatorIsUnknown( $limit->operator->value, + $source, $limit->operator->offset, ); } diff --git a/libs/parser/src/Exception/ConstMaskException.php b/libs/parser/src/Exception/ConstMaskException.php index 02f6ac7..91e81db 100644 --- a/libs/parser/src/Exception/ConstMaskException.php +++ b/libs/parser/src/Exception/ConstMaskException.php @@ -4,19 +4,26 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class ConstMaskException extends SemanticException { /** - * Occurs when a constant mask is followed by something: A mask names - * a family of constants and is a type entire, so it takes neither - * template arguments, nor shape fields, nor anything else. + * Occurs when a constant mask is followed by anything at all. * * @param int<0, max> $offset */ - public static function becauseNothingFollowsAMask(int $offset = 0): self - { - $message = 'Constant mask is a type entire and cannot be followed by anything'; - - return new self($offset, $message, self::ERROR_CODE_CONST_MASK); + public static function becauseNothingFollowsAMask( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Constant mask is a type entire and cannot be followed by anything', + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/FeatureNotAllowedException.php b/libs/parser/src/Exception/FeatureNotAllowedException.php index e39e51b..61d63e9 100644 --- a/libs/parser/src/Exception/FeatureNotAllowedException.php +++ b/libs/parser/src/Exception/FeatureNotAllowedException.php @@ -4,6 +4,8 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class FeatureNotAllowedException extends SemanticException { /** @@ -13,10 +15,18 @@ final class FeatureNotAllowedException extends SemanticException * @param non-empty-string $name * @param int<0, max> $offset */ - public static function becauseFeatureIsNotAllowed(string $name, int $offset = 0): self - { - $message = \sprintf('%s not allowed', \ucfirst($name)); - - return new self($offset, $message); + public static function becauseFeatureIsNotAllowed( + string $name, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('%s not allowed', \ucfirst($name)), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/Formatter.php b/libs/parser/src/Exception/Formatter.php deleted file mode 100644 index 9df22ee..0000000 --- a/libs/parser/src/Exception/Formatter.php +++ /dev/null @@ -1,68 +0,0 @@ - 'end of input', - '"' => 'double quote (")', - default => \sprintf('"%s"', \addcslashes($expr, '"')), - }; - } - - public static function source(string $statement): string - { - $statement = \trim($statement); - - if ($statement === '') { - return ''; - } - - $printer = new PrettyTokenPrinter(); - - return $printer->printValue(new Token( - id: 0, - name: null, - channel: Channel::Default, - value: $statement, - )); - } - - /** - * @param int<0, max> $offset - * @return non-empty-string - */ - public static function suffix(string $statement, int $offset): string - { - if (\str_contains($statement, "\n")) { - $positions = new PositionFactory(); - - $pos = $positions->createFromOffset( - source: StringSource::createFromString($statement), - offset: $offset, - ); - - return \sprintf('on line %d at column %d', $pos->line, $pos->column); - } - - return \sprintf('at column %d', $offset + 1); - } -} diff --git a/libs/parser/src/Exception/InternalParseException.php b/libs/parser/src/Exception/InternalParseException.php index e55f2fe..2b63d30 100644 --- a/libs/parser/src/Exception/InternalParseException.php +++ b/libs/parser/src/Exception/InternalParseException.php @@ -4,25 +4,17 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; +use Phplrt\Contracts\Source\ReadableInterface; -final class InternalParseException extends ParseException +final class InternalParseException extends ParserException { /** * Occurs when an unexpected error is raised while parsing a statement. */ - public static function becauseInternalErrorOccurs(string $statement, \Throwable $e): self + public static function becauseInternalErrorOccurs(ReadableInterface $source, \Throwable $e): self { - $message = \sprintf('An internal error occurred while parsing %s', Formatter::source($statement)); + $message = \sprintf('An internal error occurred while parsing %s', self::printSource($source)); - return new self($message, self::ERROR_CODE_INTERNAL_ERROR, $e); - } - - /** - * Occurs when the source content cannot be read. - */ - public static function becauseSourceIsUnreadable(SourceExceptionInterface $e): self - { - return new self($e->getMessage(), self::ERROR_CODE_INTERNAL_ERROR, $e); + return new self($message, $source, 0, $e); } } diff --git a/libs/parser/src/Exception/InternalSemanticException.php b/libs/parser/src/Exception/InternalSemanticException.php index 42d6264..7ec8d42 100644 --- a/libs/parser/src/Exception/InternalSemanticException.php +++ b/libs/parser/src/Exception/InternalSemanticException.php @@ -4,18 +4,27 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class InternalSemanticException extends SemanticException { /** - * Occurs when an unexpected sub-node is encountered while building a - * square bracket type and signals a bug in the parser itself. + * Occurs when the grammar builds a sub-node the reducer knows nothing of. * * @param int<0, max> $offset */ - public static function becauseSubNodeIsUnexpected(string $type, int $offset = 0): self - { - $message = \sprintf('Internal error, unexpected square bracket sub-node %s', $type); - - return new self($offset, $message); + public static function becauseSubNodeIsUnexpected( + string $type, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Internal error, unexpected square bracket sub-node %s', $type), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/InvalidConditionalOperatorException.php b/libs/parser/src/Exception/InvalidConditionalOperatorException.php index dcc549f..8bb1f77 100644 --- a/libs/parser/src/Exception/InvalidConditionalOperatorException.php +++ b/libs/parser/src/Exception/InvalidConditionalOperatorException.php @@ -4,18 +4,28 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class InvalidConditionalOperatorException extends SemanticException { /** - * Occurs when a conditional expression uses an unsupported operator. + * Occurs when a condition is written with an operator the grammar knows + * nothing of. * - * @param non-empty-string $operator * @param int<0, max> $offset */ - public static function becauseConditionalOperatorIsInvalid(string $operator, int $offset = 0): self - { - $message = \sprintf('Invalid conditional operator "%s"', $operator); - - return new self($offset, $message, self::ERROR_CODE_INVALID_OPERATOR); + public static function becauseConditionalOperatorIsInvalid( + string $operator, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Invalid conditional operator "%s"', $operator), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/ParseException.php b/libs/parser/src/Exception/ParseException.php deleted file mode 100644 index 3df51ce..0000000 --- a/libs/parser/src/Exception/ParseException.php +++ /dev/null @@ -1,29 +0,0 @@ -getMessage(); + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" + * + * (string) $e; + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" on line 1 at column 7 + * ``` + */ +abstract class ParserException extends \LogicException implements + ParserExceptionInterface +{ + private static ?PrettyTokenPrinter $printer = null; + + public function __construct( + string $message, + public readonly ReadableInterface $source, + int $code = 0, + ?\Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); + } + + /** + * ``` + * "int" (T_NAME) + * end of input + * "%" (unknown token) + * ``` + */ + protected static function printToken(TokenInterface $token): string + { + return (self::$printer ??= new PrettyTokenPrinter()) + ->print($token); + } + + /** + * ``` + * "array{a: int}" + * "array{a: int, b: str…" (12+) + * + * ``` + */ + protected static function printSource(ReadableInterface $source): string + { + $content = \trim($source->content); + + if ($content === '') { + return ''; + } + + return self::printToken(new Token( + id: 0, + name: null, + channel: Channel::Default, + value: $content, + )); + } +} diff --git a/libs/parser/src/Exception/ParserExceptionInterface.php b/libs/parser/src/Exception/ParserExceptionInterface.php index 658d9f7..537ef35 100644 --- a/libs/parser/src/Exception/ParserExceptionInterface.php +++ b/libs/parser/src/Exception/ParserExceptionInterface.php @@ -4,4 +4,9 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + +/** + * @property-read ReadableInterface $source + */ interface ParserExceptionInterface extends \Throwable {} diff --git a/libs/parser/src/Exception/SemanticException.php b/libs/parser/src/Exception/SemanticException.php index 950bd22..1ebf95d 100644 --- a/libs/parser/src/Exception/SemanticException.php +++ b/libs/parser/src/Exception/SemanticException.php @@ -4,38 +4,17 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + /** * Error occurring while validating the semantics of a syntactically correct * type statement. */ -abstract class SemanticException extends \LogicException implements ParserExceptionInterface +abstract class SemanticException extends ParsingException { - final public const ERROR_CODE_SHAPE_KEY_DUPLICATION = 0x01; - - final public const ERROR_CODE_SHAPE_KEY_MIX = 0x02; - - final public const ERROR_CODE_VARIADIC_WITH_DEFAULT = 0x03; - - final public const ERROR_CODE_INVALID_OPERATOR = 0x05; - - final public const ERROR_CODE_TEMPLATE_BOUND = 0x06; - - final public const ERROR_CODE_SHAPE_KEY = 0x07; - - final public const ERROR_CODE_CONST_MASK = 0x08; - - protected const CODE_LAST = self::ERROR_CODE_CONST_MASK; - - /** - * @param int<0, max> $offset - */ - final public function __construct( - public readonly int $offset, - string $message, - int $code = 0, - ?\Throwable $previous = null, - ) { - parent::__construct($message, $code, $previous); + protected static function describe(string $message, ReadableInterface $source): string + { + return \sprintf('%s in %s', $message, self::printSource($source)); } /** @@ -43,6 +22,6 @@ final public function __construct( */ public function getOffset(): int { - return $this->offset; + return $this->token->offset; } } diff --git a/libs/parser/src/Exception/SemanticParseException.php b/libs/parser/src/Exception/SemanticParseException.php deleted file mode 100644 index 6732e7e..0000000 --- a/libs/parser/src/Exception/SemanticParseException.php +++ /dev/null @@ -1,28 +0,0 @@ -getMessage()), - Formatter::source($source->content), - Formatter::suffix($source->content, $e->getOffset()), - ]); - - return new self($message, self::ERROR_CODE_SEMANTIC_ERROR_BASE + $e->getCode()); - } -} diff --git a/libs/parser/src/Exception/ShapeFieldDuplicationException.php b/libs/parser/src/Exception/ShapeFieldDuplicationException.php index 0381f6f..50fb0c1 100644 --- a/libs/parser/src/Exception/ShapeFieldDuplicationException.php +++ b/libs/parser/src/Exception/ShapeFieldDuplicationException.php @@ -4,6 +4,8 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class ShapeFieldDuplicationException extends SemanticException { /** @@ -12,10 +14,18 @@ final class ShapeFieldDuplicationException extends SemanticException * @param non-empty-string $key * @param int<0, max> $offset */ - public static function becauseShapeFieldIsDuplicated(string $key, int $offset = 0): self - { - $message = \sprintf('Duplicate key "%s"', $key); - - return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY_DUPLICATION); + public static function becauseShapeFieldIsDuplicated( + string $key, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Duplicate key "%s"', $key), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/ShapeKeyException.php b/libs/parser/src/Exception/ShapeKeyException.php index 30d9a1f..194d5bd 100644 --- a/libs/parser/src/Exception/ShapeKeyException.php +++ b/libs/parser/src/Exception/ShapeKeyException.php @@ -4,19 +4,26 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class ShapeKeyException extends SemanticException { /** - * Occurs when a shape field is written with a key that is no key: A type - * is read the way a constant reference is, and only some of what it may - * be names a field. + * Occurs when a shape key is written as something else than a name. * * @param int<0, max> $offset */ - public static function becauseKeyIsNotAName(int $offset = 0): self - { - $message = 'Shape key must be a name, a number, a string or a reference to a constant'; - - return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY); + public static function becauseKeyIsNotAName( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Shape key must be a name, a number, a string or a reference to a constant', + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/ShapeKeysMixingException.php b/libs/parser/src/Exception/ShapeKeysMixingException.php index c416432..fd82349 100644 --- a/libs/parser/src/Exception/ShapeKeysMixingException.php +++ b/libs/parser/src/Exception/ShapeKeysMixingException.php @@ -4,17 +4,26 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class ShapeKeysMixingException extends SemanticException { /** - * Occurs when a shape mixes explicit and implicit keys. + * Occurs when a shape declares both explicit and implicit keys. * * @param int<0, max> $offset */ - public static function becauseShapeKeysAreMixed(int $offset = 0): self - { - $message = 'Cannot mix explicit and implicit shape keys'; - - return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY_MIX); + public static function becauseShapeKeysAreMixed( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Cannot mix explicit and implicit shape keys', + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/TemplateBoundException.php b/libs/parser/src/Exception/TemplateBoundException.php index 94286b2..5620eb2 100644 --- a/libs/parser/src/Exception/TemplateBoundException.php +++ b/libs/parser/src/Exception/TemplateBoundException.php @@ -4,50 +4,71 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class TemplateBoundException extends SemanticException { /** - * Occurs when a template parameter is bounded with a word that bounds - * nothing. + * Occurs when a template parameter is bounded with a word the grammar + * knows nothing of. * - * @param non-empty-string $operator * @param int<0, max> $offset */ - public static function becauseOperatorIsUnknown(string $operator, int $offset = 0): self - { - $message = \sprintf( - 'Template parameter cannot be bounded with "%s", expected one of "of", "as" or "super"', - $operator, + public static function becauseOperatorIsUnknown( + string $operator, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf( + 'Template parameter cannot be bounded with "%s", expected one of "of", "as" or "super"', + $operator, + ), + $source, + ), + $source, + self::createToken($source, $offset), ); - - return new self($offset, $message, self::ERROR_CODE_TEMPLATE_BOUND); } /** - * Occurs when a template parameter is written with the same kind of - * limit more than once. + * Occurs when a template parameter carries the same limit twice. * - * @param non-empty-string $kind * @param int<0, max> $offset */ - public static function becauseBoundIsDuplicated(string $kind, int $offset = 0): self - { - $message = \sprintf('Template parameter cannot have more than one %s', $kind); - - return new self($offset, $message, self::ERROR_CODE_TEMPLATE_BOUND); + public static function becauseBoundIsDuplicated( + string $kind, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Template parameter cannot have more than one %s', $kind), + $source, + ), + $source, + self::createToken($source, $offset), + ); } /** - * Occurs when a bound is written behind a default, where it reads as - * a bound of the default itself. + * Occurs when a template parameter carries a bound behind its default. * * @param int<0, max> $offset */ - public static function becauseDefaultIsNotWrittenLast(int $offset = 0): self - { - $message = 'Template parameter default must be written last, since a bound ' - . 'behind it reads as a bound of the default itself'; - - return new self($offset, $message, self::ERROR_CODE_TEMPLATE_BOUND); + public static function becauseDefaultIsNotWrittenLast( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Template parameter default must be written last, since a bound ' + . 'behind it reads as a bound of the default itself', + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/UnexpectedTokenException.php b/libs/parser/src/Exception/UnexpectedTokenException.php index b5f54e5..ae0e2a4 100644 --- a/libs/parser/src/Exception/UnexpectedTokenException.php +++ b/libs/parser/src/Exception/UnexpectedTokenException.php @@ -4,23 +4,27 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\ReadableInterface; -final class UnexpectedTokenException extends ParseException +final class UnexpectedTokenException extends ParsingException { /** * Occurs when a known token is found in an unexpected source location. * - * @param int<0, max> $offset + * The message is the one the grammar carries, whether it is written in + * an "@error" directive or worded by the parser itself. */ - public static function becauseTokenIsUnexpected(string $token, string $statement, int $offset): self - { - $message = \vsprintf('Syntax error, unexpected %s%s %s', [ - Formatter::token($token), - $token === $statement ? '' : ' in ' . Formatter::source($statement), - Formatter::suffix($statement, $offset), + public static function becauseTokenIsUnexpected( + string $message, + ReadableInterface $source, + TokenInterface $token, + ): self { + $message = \vsprintf('%s in %s', [ + $message, + self::printSource($source), ]); - return new self($message, self::ERROR_CODE_UNEXPECTED_TOKEN); + return new self($message, $source, $token); } } diff --git a/libs/parser/src/Exception/UnrecognizedSyntaxException.php b/libs/parser/src/Exception/UnrecognizedSyntaxException.php index 8425a70..3ea50e8 100644 --- a/libs/parser/src/Exception/UnrecognizedSyntaxException.php +++ b/libs/parser/src/Exception/UnrecognizedSyntaxException.php @@ -4,21 +4,19 @@ namespace TypeLang\Parser\Exception; -final class UnrecognizedSyntaxException extends ParseException +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\ReadableInterface; + +final class UnrecognizedSyntaxException extends ParsingException { /** * Occurs when the parser reaches a state that does not match any known * grammar rule. - * - * @param int<0, max> $offset */ - public static function becauseSyntaxIsUnrecognized(string $statement, int $offset): self + public static function becauseSyntaxIsUnrecognized(ReadableInterface $source, TokenInterface $token): self { - $message = \vsprintf('Internal syntax error, in %s %s', [ - Formatter::source($statement), - Formatter::suffix($statement, $offset), - ]); + $message = \sprintf('Internal syntax error in %s', self::printSource($source)); - return new self($message, self::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR); + return new self($message, $source, $token); } } diff --git a/libs/parser/src/Exception/UnrecognizedTokenException.php b/libs/parser/src/Exception/UnrecognizedTokenException.php index d417a8d..ea9e04a 100644 --- a/libs/parser/src/Exception/UnrecognizedTokenException.php +++ b/libs/parser/src/Exception/UnrecognizedTokenException.php @@ -4,21 +4,21 @@ namespace TypeLang\Parser\Exception; -final class UnrecognizedTokenException extends ParseException +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\ReadableInterface; + +final class UnrecognizedTokenException extends ParsingException { /** * Occurs when unable to recognize tokens in source code. - * - * @param int<0, max> $offset */ - public static function becauseTokenIsUnrecognized(string $token, string $statement, int $offset): self + public static function becauseTokenIsUnrecognized(ReadableInterface $source, TokenInterface $token): self { - $message = \vsprintf('Syntax error, unrecognized %s%s %s', [ - Formatter::token($token), - $token === $statement ? '' : ' in ' . Formatter::source($statement), - Formatter::suffix($statement, $offset), + $message = \vsprintf('Syntax error, unexpected %s in %s', [ + self::printToken($token), + self::printSource($source), ]); - return new self($message, self::ERROR_CODE_UNRECOGNIZED_TOKEN); + return new self($message, $source, $token); } } diff --git a/libs/parser/src/Exception/VariadicWithDefaultException.php b/libs/parser/src/Exception/VariadicWithDefaultException.php index 9f85b44..11b9a99 100644 --- a/libs/parser/src/Exception/VariadicWithDefaultException.php +++ b/libs/parser/src/Exception/VariadicWithDefaultException.php @@ -4,17 +4,26 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class VariadicWithDefaultException extends SemanticException { /** - * Occurs when a variadic parameter also declares a default value. + * Occurs when a variadic parameter is written with a default. * * @param int<0, max> $offset */ - public static function becauseVariadicHasDefault(int $offset = 0): self - { - $message = 'Cannot have variadic param with a default'; - - return new self($offset, $message, self::ERROR_CODE_VARIADIC_WITH_DEFAULT); + public static function becauseVariadicHasDefault( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Cannot have variadic param with a default', + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Internal/CompiledExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php index d0c8c6d..0ef89d2 100644 --- a/libs/parser/src/Internal/CompiledExecutor.php +++ b/libs/parser/src/Internal/CompiledExecutor.php @@ -1,7 +1,7 @@ parser = new \Phplrt\Parser\Parser( lexer: $this->lexer, grammar: [ - new \Phplrt\Parser\Grammar\Alternation([1, 141]), - new \Phplrt\Parser\Grammar\Concatenation([2, 136]), - new \Phplrt\Parser\Grammar\Concatenation([3, 133]), - new \Phplrt\Parser\Grammar\Concatenation([4, 130]), - new \Phplrt\Parser\Grammar\Alternation([5, 7]), + new \Phplrt\Parser\Grammar\Alternation([1, 155]), + new \Phplrt\Parser\Grammar\Concatenation([2, 147]), + new \Phplrt\Parser\Grammar\Concatenation([3, 143]), + new \Phplrt\Parser\Grammar\Concatenation([4, 139]), + new \Phplrt\Parser\Grammar\Alternation([5, 8]), new \Phplrt\Parser\Grammar\Concatenation([6, 7]), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false), - new \Phplrt\Parser\Grammar\Concatenation([8, 125]), - new \Phplrt\Parser\Grammar\Alternation([9, 12, 13, 27]), - new \Phplrt\Parser\Grammar\Concatenation([10, 0, 11]), + new \Phplrt\Parser\Grammar\Concatenation([8]), + new \Phplrt\Parser\Grammar\Concatenation([9, 134]), + new \Phplrt\Parser\Grammar\Alternation([10, 13, 14, 28]), + new \Phplrt\Parser\Grammar\Concatenation([11, 0, 12]), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Alternation([14, 26]), - new \Phplrt\Parser\Grammar\Alternation([15, 18, 19, 24, 25]), - new \Phplrt\Parser\Grammar\Alternation([16, 17]), + new \Phplrt\Parser\Grammar\Alternation([15, 27]), + new \Phplrt\Parser\Grammar\Alternation([16, 19, 20, 25, 26]), + new \Phplrt\Parser\Grammar\Alternation([17, 18]), new \Phplrt\Parser\Grammar\Lexeme(self::T_DQ_STRING_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQ_STRING_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FLOAT_LITERAL, true), - new \Phplrt\Parser\Grammar\Alternation([20, 21, 22, 23]), + new \Phplrt\Parser\Grammar\Alternation([21, 22, 23, 24]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BIN_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_OCT_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_HEX_INT_LITERAL, true), @@ -195,13 +196,14 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Concatenation([28, 54]), - new \Phplrt\Parser\Grammar\Alternation([29, 52]), - new \Phplrt\Parser\Grammar\Concatenation([30, 44]), - new \Phplrt\Parser\Grammar\Alternation([31, 43]), - new \Phplrt\Parser\Grammar\Concatenation([32, 33, 41]), + new \Phplrt\Parser\Grammar\Concatenation([29, 57]), + new \Phplrt\Parser\Grammar\Alternation([30, 55]), + new \Phplrt\Parser\Grammar\Concatenation([31, 47]), + new \Phplrt\Parser\Grammar\Alternation([32, 46]), + new \Phplrt\Parser\Grammar\Concatenation([33, 34, 43]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), - new \Phplrt\Parser\Grammar\Alternation([34, 35, 36, 37, 38, 39, 40]), + new \Phplrt\Parser\Grammar\Concatenation([35]), + new \Phplrt\Parser\Grammar\Alternation([36, 37, 38, 39, 40, 41, 42]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true), @@ -209,107 +211,119 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Repetition(42, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([32, 33]), - new \Phplrt\Parser\Grammar\Concatenation([33, 41]), - new \Phplrt\Parser\Grammar\Optional(45), - new \Phplrt\Parser\Grammar\Alternation([46, 51]), - new \Phplrt\Parser\Grammar\Concatenation([47, 48, 50]), + new \Phplrt\Parser\Grammar\Repetition(44, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([33, 45]), + new \Phplrt\Parser\Grammar\Concatenation([35]), + new \Phplrt\Parser\Grammar\Concatenation([35, 43]), + new \Phplrt\Parser\Grammar\Optional(48), + new \Phplrt\Parser\Grammar\Alternation([49, 54]), + new \Phplrt\Parser\Grammar\Concatenation([50, 51, 53]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), - new \Phplrt\Parser\Grammar\Repetition(49, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([33, 47]), - new \Phplrt\Parser\Grammar\Optional(33), - new \Phplrt\Parser\Grammar\Concatenation([32, 46]), - new \Phplrt\Parser\Grammar\Concatenation([47, 33, 53]), - new \Phplrt\Parser\Grammar\Optional(46), - new \Phplrt\Parser\Grammar\Optional(55), - new \Phplrt\Parser\Grammar\Alternation([56, 60, 81, 92, 102]), - new \Phplrt\Parser\Grammar\Concatenation([57, 58]), + new \Phplrt\Parser\Grammar\Repetition(52, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([35, 50]), + new \Phplrt\Parser\Grammar\Optional(35), + new \Phplrt\Parser\Grammar\Concatenation([33, 49]), + new \Phplrt\Parser\Grammar\Concatenation([50, 35, 56]), + new \Phplrt\Parser\Grammar\Optional(49), + new \Phplrt\Parser\Grammar\Optional(58), + new \Phplrt\Parser\Grammar\Alternation([59, 63, 86, 98, 110]), + new \Phplrt\Parser\Grammar\Concatenation([60, 61]), new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true), - new \Phplrt\Parser\Grammar\Alternation([59, 46]), - new \Phplrt\Parser\Grammar\Concatenation([33, 53]), - new \Phplrt\Parser\Grammar\Concatenation([10, 61, 11, 78]), - new \Phplrt\Parser\Grammar\Optional(62), - new \Phplrt\Parser\Grammar\Concatenation([63, 74, 77]), - new \Phplrt\Parser\Grammar\Concatenation([0, 64, 66, 68, 72]), + new \Phplrt\Parser\Grammar\Alternation([62, 49]), + new \Phplrt\Parser\Grammar\Concatenation([35, 56]), + new \Phplrt\Parser\Grammar\Concatenation([11, 64, 81, 82]), new \Phplrt\Parser\Grammar\Optional(65), + new \Phplrt\Parser\Grammar\Concatenation([66, 77, 80]), + new \Phplrt\Parser\Grammar\Concatenation([0, 67, 69, 71, 75]), + new \Phplrt\Parser\Grammar\Optional(68), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true), - new \Phplrt\Parser\Grammar\Optional(67), + new \Phplrt\Parser\Grammar\Optional(70), new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true), - new \Phplrt\Parser\Grammar\Optional(69), - new \Phplrt\Parser\Grammar\Alternation([70, 71]), + new \Phplrt\Parser\Grammar\Optional(72), + new \Phplrt\Parser\Grammar\Alternation([73, 74]), new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Optional(73), + new \Phplrt\Parser\Grammar\Optional(76), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true), - new \Phplrt\Parser\Grammar\Repetition(75, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([76, 63]), + new \Phplrt\Parser\Grammar\Repetition(78, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([79, 66]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false), - new \Phplrt\Parser\Grammar\Optional(76), new \Phplrt\Parser\Grammar\Optional(79), - new \Phplrt\Parser\Grammar\Concatenation([80, 0]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), + new \Phplrt\Parser\Grammar\Optional(83), + new \Phplrt\Parser\Grammar\Concatenation([84, 85]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), - new \Phplrt\Parser\Grammar\Concatenation([82, 91]), - new \Phplrt\Parser\Grammar\Concatenation([83, 84, 88, 77, 90]), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Concatenation([87, 97]), + new \Phplrt\Parser\Grammar\Concatenation([88, 89, 94, 80, 96]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([85, 87]), - new \Phplrt\Parser\Grammar\Concatenation([86, 87]), + new \Phplrt\Parser\Grammar\Concatenation([90]), + new \Phplrt\Parser\Grammar\Alternation([91, 93]), + new \Phplrt\Parser\Grammar\Concatenation([92, 93]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), - new \Phplrt\Parser\Grammar\Alternation([47, 0]), - new \Phplrt\Parser\Grammar\Repetition(89, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([76, 84]), + new \Phplrt\Parser\Grammar\Alternation([50, 0]), + new \Phplrt\Parser\Grammar\Repetition(95, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([79, 90]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Predicate(10, false), - new \Phplrt\Parser\Grammar\Concatenation([93, 60]), - new \Phplrt\Parser\Grammar\Concatenation([83, 94, 100, 77, 90]), - new \Phplrt\Parser\Grammar\Concatenation([33, 95]), - new \Phplrt\Parser\Grammar\Repetition(96, 0, \INF), - new \Phplrt\Parser\Grammar\Alternation([97, 98]), - new \Phplrt\Parser\Grammar\Concatenation([33, 0]), - new \Phplrt\Parser\Grammar\Concatenation([99, 0]), + new \Phplrt\Parser\Grammar\Predicate(11, false), + new \Phplrt\Parser\Grammar\Concatenation([99, 63]), + new \Phplrt\Parser\Grammar\Concatenation([88, 100, 107, 80, 109]), + new \Phplrt\Parser\Grammar\Concatenation([101]), + new \Phplrt\Parser\Grammar\Concatenation([35, 102]), + new \Phplrt\Parser\Grammar\Repetition(103, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([104, 105]), + new \Phplrt\Parser\Grammar\Concatenation([35, 0]), + new \Phplrt\Parser\Grammar\Concatenation([106, 0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, false), - new \Phplrt\Parser\Grammar\Repetition(101, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([76, 94]), - new \Phplrt\Parser\Grammar\Concatenation([103, 104, 77, 124]), + new \Phplrt\Parser\Grammar\Repetition(108, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([79, 101]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), + new \Phplrt\Parser\Grammar\Concatenation([111, 112, 80, 133]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([105, 123]), - new \Phplrt\Parser\Grammar\Concatenation([106, 119]), - new \Phplrt\Parser\Grammar\Concatenation([107, 117]), - new \Phplrt\Parser\Grammar\Alternation([108, 116]), - new \Phplrt\Parser\Grammar\Concatenation([109, 114, 80, 0]), - new \Phplrt\Parser\Grammar\Alternation([110, 8]), - new \Phplrt\Parser\Grammar\Concatenation([33, 111]), - new \Phplrt\Parser\Grammar\Predicate(112, true), - new \Phplrt\Parser\Grammar\Concatenation([113, 80]), + new \Phplrt\Parser\Grammar\Alternation([113, 132]), + new \Phplrt\Parser\Grammar\Concatenation([114, 128]), + new \Phplrt\Parser\Grammar\Concatenation([115, 126]), + new \Phplrt\Parser\Grammar\Alternation([116, 125]), + new \Phplrt\Parser\Grammar\Concatenation([117, 122, 84, 124]), + new \Phplrt\Parser\Grammar\Alternation([118, 9]), + new \Phplrt\Parser\Grammar\Concatenation([35, 119]), + new \Phplrt\Parser\Grammar\Predicate(120, true), + new \Phplrt\Parser\Grammar\Concatenation([121, 84]), new \Phplrt\Parser\Grammar\Optional(6), - new \Phplrt\Parser\Grammar\Optional(115), + new \Phplrt\Parser\Grammar\Optional(123), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Repetition(118, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([76, 107]), - new \Phplrt\Parser\Grammar\Optional(120), - new \Phplrt\Parser\Grammar\Concatenation([76, 121]), - new \Phplrt\Parser\Grammar\Concatenation([67, 122]), - new \Phplrt\Parser\Grammar\Optional(82), - new \Phplrt\Parser\Grammar\Optional(121), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Repetition(127, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([79, 115]), + new \Phplrt\Parser\Grammar\Optional(129), + new \Phplrt\Parser\Grammar\Concatenation([79, 130]), + new \Phplrt\Parser\Grammar\Concatenation([70, 131]), + new \Phplrt\Parser\Grammar\Optional(87), + new \Phplrt\Parser\Grammar\Optional(130), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(126, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([127, 128, 129]), + new \Phplrt\Parser\Grammar\Repetition(135, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([136, 137, 138]), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false), new \Phplrt\Parser\Grammar\Optional(0), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(131, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([132, 4]), + new \Phplrt\Parser\Grammar\Repetition(140, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([141, 142]), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, false), - new \Phplrt\Parser\Grammar\Repetition(134, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([135, 3]), + new \Phplrt\Parser\Grammar\Concatenation([4]), + new \Phplrt\Parser\Grammar\Repetition(144, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([145, 146]), new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, false), - new \Phplrt\Parser\Grammar\Optional(137), - new \Phplrt\Parser\Grammar\Concatenation([138, 140, 6, 0, 80, 0]), - new \Phplrt\Parser\Grammar\Concatenation([36, 139]), - new \Phplrt\Parser\Grammar\Optional(37), - new \Phplrt\Parser\Grammar\Alternation([0, 69]), - new \Phplrt\Parser\Grammar\Concatenation([69, 137]), + new \Phplrt\Parser\Grammar\Concatenation([3]), + new \Phplrt\Parser\Grammar\Optional(148), + new \Phplrt\Parser\Grammar\Concatenation([149, 151, 6, 152, 153, 154]), + new \Phplrt\Parser\Grammar\Concatenation([38, 150]), + new \Phplrt\Parser\Grammar\Optional(39), + new \Phplrt\Parser\Grammar\Alternation([0, 72]), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Concatenation([72, 148]), ], initial: 0, reducers: [ @@ -317,38 +331,38 @@ public function __construct() 2 => $this->reduceUnionType(...), 3 => $this->reduceIntersectionType(...), 5 => self::reduceNullableType(...), - 7 => $this->reduceListOrOffsetType(...), - 12 => self::reduceThisType(...), - 13 => $this->reduceLiteralType(...), - 16 => self::reduceDoubleQuotedStringLiteralType(...), - 17 => self::reduceSingleQuotedStringLiteralType(...), - 18 => self::reduceFloatLiteralType(...), - 19 => self::reduceIntLiteralType(...), - 24 => self::reduceTrueLiteralType(...), - 25 => self::reduceFalseLiteralType(...), - 26 => self::reduceNullLiteralType(...), - 27 => $this->reduceNamedType(...), - 28 => self::reduceNameOrMask(...), - 31 => self::reduceFullQualifiedName(...), - 33 => self::reduceIdentifier(...), - 43 => self::reduceRelativeName(...), - 47 => self::reduceWildcardType(...), - 51 => self::reduceNamespacedMask(...), - 61 => self::reduceCallableParameters(...), - 63 => self::reduceCallableParameter(...), - 69 => self::reduceVariable(...), - 82 => $this->reduceTemplateArguments(...), - 84 => $this->reduceTemplateArgument(...), - 86 => self::reduceIdentifierWithExtraSpace(...), - 93 => $this->reduceTemplateParameters(...), - 94 => self::reduceTemplateParameter(...), - 97 => self::reduceTemplateBoundEdge(...), - 102 => $this->reduceShapeFields(...), - 106 => self::reduceShapeFieldsList(...), - 108 => self::reduceExplicitField(...), - 116 => self::reduceImplicitField(...), - 126 => self::reduceListOrOffsetSuffix(...), - 138 => self::reduceTernaryExpressionOperator(...), + 8 => $this->reduceListOrOffsetType(...), + 13 => self::reduceThisType(...), + 14 => $this->reduceLiteralType(...), + 17 => self::reduceDoubleQuotedStringLiteralType(...), + 18 => self::reduceSingleQuotedStringLiteralType(...), + 19 => self::reduceFloatLiteralType(...), + 20 => self::reduceIntLiteralType(...), + 25 => self::reduceTrueLiteralType(...), + 26 => self::reduceFalseLiteralType(...), + 27 => self::reduceNullLiteralType(...), + 28 => $this->reduceNamedType(...), + 29 => self::reduceNameOrMask(...), + 32 => self::reduceFullQualifiedName(...), + 35 => self::reduceIdentifier(...), + 46 => self::reduceRelativeName(...), + 50 => self::reduceWildcardType(...), + 54 => self::reduceNamespacedMask(...), + 64 => self::reduceCallableParameters(...), + 66 => self::reduceCallableParameter(...), + 72 => self::reduceVariable(...), + 87 => $this->reduceTemplateArguments(...), + 90 => $this->reduceTemplateArgument(...), + 92 => self::reduceIdentifierWithExtraSpace(...), + 99 => $this->reduceTemplateParameters(...), + 101 => self::reduceTemplateParameter(...), + 104 => self::reduceTemplateBoundEdge(...), + 110 => $this->reduceShapeFields(...), + 114 => self::reduceShapeFieldsList(...), + 116 => self::reduceExplicitField(...), + 125 => self::reduceImplicitField(...), + 135 => self::reduceListOrOffsetSuffix(...), + 149 => self::reduceTernaryExpressionOperator(...), ], lookahead: [ [ @@ -503,6 +517,26 @@ public function __construct() true, true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], [ 18 => true, ], @@ -635,6 +669,15 @@ public function __construct() true, true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], [ 7 => true, ], @@ -669,6 +712,15 @@ public function __construct() true, true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], null, [ 8 => true, @@ -809,6 +861,9 @@ public function __construct() 10 => true, ], null, + [ + 19 => true, + ], null, [ 12 => true, @@ -816,6 +871,28 @@ public function __construct() [ 12 => true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], [ 13 => true, ], @@ -847,6 +924,28 @@ public function __construct() true, true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], [ 6 => true, ], @@ -898,6 +997,15 @@ public function __construct() true, true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], null, [ 1 => true, @@ -928,6 +1036,9 @@ public function __construct() [ 10 => true, ], + [ + 14 => true, + ], [ 16 => true, ], @@ -1082,6 +1193,28 @@ public function __construct() true, true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], null, [ 10 => true, @@ -1116,6 +1249,27 @@ public function __construct() [ 22 => true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], null, [ 9 => true, @@ -1123,6 +1277,27 @@ public function __construct() [ 9 => true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], null, [ 1 => true, @@ -1153,6 +1328,53 @@ public function __construct() true, true, ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 12 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], [ 26 => true, true, @@ -1166,6 +1388,7 @@ public function __construct() false, true, false, + false, true, false, false, @@ -1192,6 +1415,7 @@ public function __construct() false, true, false, + false, true, false, false, @@ -1202,6 +1426,7 @@ public function __construct() false, false, false, + false, true, false, false, @@ -1241,8 +1466,11 @@ public function __construct() false, false, false, + false, + false, true, false, + false, true, true, true, @@ -1253,6 +1481,7 @@ public function __construct() false, false, true, + false, true, false, false, @@ -1261,6 +1490,7 @@ public function __construct() false, false, false, + false, true, false, false, @@ -1275,6 +1505,7 @@ public function __construct() false, false, false, + false, true, false, false, @@ -1297,9 +1528,14 @@ public function __construct() false, false, false, + false, + false, true, false, false, + false, + false, + false, true, ], choicePrediction: [ @@ -1359,7 +1595,7 @@ public function __construct() 1, ], 27 => [ - 141, + 155, ], ], 4 => [ @@ -1367,193 +1603,190 @@ public function __construct() 5, ], 1 => [ - 7, + 8, ], 2 => [ - 7, + 8, ], 3 => [ - 7, + 8, ], 4 => [ - 7, + 8, ], 5 => [ - 7, + 8, ], 6 => [ - 7, + 8, ], 7 => [ - 7, + 8, ], 8 => [ - 7, + 8, ], 18 => [ - 7, + 8, ], 25 => [ - 7, + 8, ], [ - 7, + 8, ], 28 => [ - 7, + 8, ], [ - 7, + 8, ], [ - 7, + 8, ], [ - 7, + 8, ], [ - 7, + 8, ], [ - 7, + 8, ], [ - 7, + 8, ], ], - 8 => [ + 9 => [ 18 => [ - 9, + 10, ], 26 => [ - 12, + 13, ], 3 => [ - 13, - 27, + 14, + 28, ], 4 => [ - 13, - 27, + 14, + 28, ], 5 => [ - 13, - 27, + 14, + 28, ], 28 => [ - 13, + 14, ], [ - 13, + 14, ], [ - 13, + 14, ], [ - 13, + 14, ], [ - 13, + 14, ], [ - 13, + 14, ], [ - 13, + 14, ], 1 => [ - 27, + 28, ], 2 => [ - 27, + 28, ], 6 => [ - 27, + 28, ], 7 => [ - 27, + 28, ], 8 => [ - 27, + 28, ], 25 => [ - 27, + 28, ], ], - 13 => [ + 14 => [ 3 => [ - 14, + 15, ], [ - 14, + 15, ], 28 => [ - 14, + 15, ], [ - 14, + 15, ], [ - 14, + 15, ], [ - 14, + 15, ], [ - 14, + 15, ], [ - 14, + 15, ], [ - 14, + 15, ], 5 => [ - 26, + 27, ], ], [ 28 => [ - 15, + 16, ], [ - 15, + 16, ], [ - 18, + 19, ], [ - 19, + 20, ], [ - 19, + 20, ], [ - 19, + 20, ], [ - 19, + 20, ], 3 => [ - 24, + 25, ], 4 => [ - 25, + 26, ], ], [ 28 => [ - 16, + 17, ], [ - 17, + 18, ], ], - 19 => [ + 20 => [ 31 => [ - 20, - ], - [ 21, ], [ @@ -1562,202 +1795,205 @@ public function __construct() [ 23, ], + [ + 24, + ], ], - 28 => [ + 29 => [ 1 => [ - 29, + 30, ], [ - 29, + 30, ], [ - 29, + 30, ], [ - 29, + 30, ], [ - 29, + 30, ], [ - 29, + 30, ], [ - 29, + 30, ], [ - 29, + 30, ], 25 => [ - 52, + 55, ], ], - 30 => [ + 31 => [ 8 => [ - 31, + 32, ], 1 => [ - 43, + 46, ], 2 => [ - 43, + 46, ], 3 => [ - 43, + 46, ], 4 => [ - 43, + 46, ], 5 => [ - 43, + 46, ], 6 => [ - 43, + 46, ], 7 => [ - 43, + 46, ], ], - 33 => [ + 35 => [ 7 => [ - 34, + 36, ], 6 => [ - 35, + 37, ], 1 => [ - 36, + 38, ], 2 => [ - 37, + 39, ], 3 => [ - 38, + 40, ], 4 => [ - 39, + 41, ], 5 => [ - 40, + 42, ], ], - 45 => [ + 48 => [ 25 => [ - 46, + 49, ], 8 => [ - 51, + 54, ], ], - 55 => [ + 58 => [ 11 => [ - 56, + 59, ], 18 => [ - 60, + 63, ], 13 => [ - 81, - 92, + 86, + 98, ], 16 => [ - 102, + 110, ], ], - 58 => [ + 61 => [ 1 => [ - 59, + 62, ], [ - 59, + 62, ], [ - 59, + 62, ], [ - 59, + 62, ], [ - 59, + 62, ], [ - 59, + 62, ], [ - 59, + 62, ], 25 => [ - 46, + 49, ], ], - 69 => [ + 72 => [ 27 => [ - 70, + 73, ], 26 => [ - 71, + 74, ], ], - 84 => [ + 90 => [ 1 => [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], 7 => [ - 87, + 93, ], [ - 87, + 93, ], 15 => [ - 87, + 93, ], 18 => [ - 87, + 93, ], 25 => [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], [ - 87, + 93, ], ], - 87 => [ + 93 => [ 1 => [ 0, ], @@ -1816,76 +2052,76 @@ public function __construct() 0, ], ], - 96 => [ + 103 => [ 1 => [ - 97, + 104, ], [ - 97, + 104, ], [ - 97, + 104, ], [ - 97, + 104, ], [ - 97, + 104, ], [ - 97, + 104, ], [ - 97, + 104, ], 24 => [ - 98, + 105, ], ], - 107 => [ + 115 => [ 15 => [ - 116, + 125, ], 27 => [ - 116, + 125, ], ], - 109 => [ + 117 => [ 8 => [ - 8, + 9, ], 18 => [ - 8, + 9, ], 25 => [ - 8, + 9, ], [ - 8, + 9, ], 28 => [ - 8, + 9, ], [ - 8, + 9, ], [ - 8, + 9, ], [ - 8, + 9, ], [ - 8, + 9, ], [ - 8, + 9, ], [ - 8, + 9, ], ], - 140 => [ + 151 => [ 1 => [ 0, ], @@ -1982,6 +2218,27 @@ public function __construct() 'T_DOC_COMMENT', '/[^\\s]++/', ], + messages: [ + 7 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', + 12 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', + 34 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "\\Some"', + 45 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', + 61 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', + 81 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', + 85 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', + 89 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', + 96 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', + 100 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', + 109 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', + 124 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', + 133 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', + 138 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', + 142 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', + 146 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', + 152 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', + 'Syntax error, unexpected {token}, a condition must be parted with a colon ":"', + 'Syntax error, unexpected {token}, a condition must carry the type it is false of', + ], ); } @@ -1993,6 +2250,7 @@ public function parse(\Phplrt\Contracts\Source\ReadableInterface $source): mixed private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; if (\count($children) === 1) { @@ -2000,7 +2258,7 @@ private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed } if ($this->features->conditions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset); } $condition = $children[1] @@ -2018,6 +2276,7 @@ private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed private function reduceUnionType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; if (\count($children) === 1) { @@ -2025,7 +2284,7 @@ private function reduceUnionType(\Phplrt\Parser\Context $ctx, mixed $children): } if ($this->features->unions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset); } $statement = new Type\UnionTypeNode($children); @@ -2037,6 +2296,7 @@ private function reduceUnionType(\Phplrt\Parser\Context $ctx, mixed $children): private function reduceIntersectionType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; if (\count($children) === 1) { @@ -2044,7 +2304,7 @@ private function reduceIntersectionType(\Phplrt\Parser\Context $ctx, mixed $chil } if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); } $statement = new Type\IntersectionTypeNode($children); @@ -2064,6 +2324,7 @@ private static function reduceNullableType(\Phplrt\Parser\Context $ctx, mixed $c private function reduceListOrOffsetType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; $statement = $children[0]; @@ -2075,14 +2336,14 @@ private function reduceListOrOffsetType(\Phplrt\Parser\Context $ctx, mixed $chil switch (true) { case $child === true: if ($this->features->lists === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $source, $offset); } $statement = new Type\TypesListNode($statement, $offset); break; case $child instanceof Type\TypeNode: if ($this->features->offsets === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $source, $offset); } $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset); @@ -2090,6 +2351,7 @@ private function reduceListOrOffsetType(\Phplrt\Parser\Context $ctx, mixed $chil default: throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( \get_debug_type($child), + $source, $offset, ); } @@ -2109,10 +2371,11 @@ private static function reduceThisType(\Phplrt\Parser\Context $ctx, mixed $child private function reduceLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; if ($this->features->literals === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset); } return $children; @@ -2199,6 +2462,7 @@ private static function reduceNullLiteralType(\Phplrt\Parser\Context $ctx, mixed private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; $head = $children[0]; @@ -2206,7 +2470,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): if ($head instanceof Type\ConstMaskNode) { if ($suffix !== null) { - throw Exception\ConstMaskException::becauseNothingFollowsAMask($offset); + throw Exception\ConstMaskException::becauseNothingFollowsAMask($source, $offset); } return $head; @@ -2228,7 +2492,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): // Some\Any(T, U): V case $suffix instanceof Type\Callable\CallableParameterListNode: if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); } return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); @@ -2236,7 +2500,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): // Some\Any(T): U case $suffix instanceof Type\Template\TemplateParameterListNode: if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); } return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); @@ -2348,6 +2612,7 @@ private static function reduceCallableParameters(\Phplrt\Parser\Context $ctx, mi private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; $parameter = new Type\Callable\CallableParameterNode( @@ -2379,7 +2644,7 @@ private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mix default: if ($parameter->isVariadic) { - throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); + throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset); } $parameter->isOptional = true; @@ -2403,10 +2668,11 @@ private static function reduceVariable(\Phplrt\Parser\Context $ctx, mixed $child private function reduceTemplateArguments(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; if ($this->features->generics === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $source, $offset); } return new Type\Template\TemplateArgumentListNode($children, $offset); @@ -2415,11 +2681,12 @@ private function reduceTemplateArguments(\Phplrt\Parser\Context $ctx, mixed $chi private function reduceTemplateArgument(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; if (\is_array($children)) { if ($this->features->hints === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $source, $offset); } return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset); @@ -2439,10 +2706,11 @@ private static function reduceIdentifierWithExtraSpace(\Phplrt\Parser\Context $c private function reduceTemplateParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; if ($this->features->generics === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $source, $offset); } return new Type\Template\TemplateParameterListNode($children, $offset); @@ -2451,6 +2719,7 @@ private function reduceTemplateParameters(\Phplrt\Parser\Context $ctx, mixed $ch private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; $upper = $lower = $default = null; @@ -2458,7 +2727,7 @@ private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mix foreach (\array_slice($children, 1) as $limit) { if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) { if ($default !== null) { - throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $limit->offset); + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $source, $limit->offset); } $default = $limit; @@ -2467,14 +2736,14 @@ private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mix } if ($default !== null) { - throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($limit->operator->offset); + throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($source, $limit->operator->offset); } switch ($limit->operator->value) { case 'of': case 'as': if ($upper !== null) { - throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $limit->offset); + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $source, $limit->offset); } $upper = $limit; @@ -2482,7 +2751,7 @@ private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mix case 'super': if ($lower !== null) { - throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $limit->offset); + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $source, $limit->offset); } $lower = $limit; @@ -2491,6 +2760,7 @@ private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mix default: throw Exception\TemplateBoundException::becauseOperatorIsUnknown( $limit->operator->value, + $source, $limit->operator->offset, ); } @@ -2510,6 +2780,7 @@ private static function reduceTemplateBoundEdge(\Phplrt\Parser\Context $ctx, mix private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; $count = \count($children); @@ -2519,7 +2790,7 @@ private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children) } if ($this->features->shapes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $source, $offset); } $parameters = null; @@ -2552,6 +2823,7 @@ private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children) private static function reduceShapeFieldsList(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; $explicit = false; @@ -2574,14 +2846,14 @@ private static function reduceShapeFieldsList(\Phplrt\Parser\Context $ctx, mixed $key = $field->getIndex(); if (isset($indexed[$key])) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $source, $field->offset); } $indexed[$key] = true; } if ($explicit && $implicit) { - throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); + throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset); } return new Type\Shape\FieldsListNode($children, true, $offset); @@ -2590,6 +2862,7 @@ private static function reduceShapeFieldsList(\Phplrt\Parser\Context $ctx, mixed private static function reduceExplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler + $source = $ctx->source; $offset = $ctx->begin; $count = \count($children); @@ -2610,7 +2883,7 @@ private static function reduceExplicitField(\Phplrt\Parser\Context $ctx, mixed $ $name instanceof Type\ClassConstMaskNode, $name instanceof Type\ConstMaskNode => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset), - default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($name->offset), + default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($source, $name->offset), }; } diff --git a/libs/parser/src/Internal/Executor.php b/libs/parser/src/Internal/Executor.php index e93c736..972f0a3 100644 --- a/libs/parser/src/Internal/Executor.php +++ b/libs/parser/src/Internal/Executor.php @@ -16,10 +16,10 @@ use Phplrt\Parser\Parser as ParserRuntime; use Phplrt\Position\PositionFactory; use TypeLang\Parser\Exception\InternalParseException; -use TypeLang\Parser\Exception\ParseException; -use TypeLang\Parser\Exception\SemanticException; -use TypeLang\Parser\Exception\SemanticParseException; +use TypeLang\Parser\Exception\ParserException; +use TypeLang\Parser\Exception\ParserExceptionInterface; use TypeLang\Parser\Exception\UnexpectedTokenException; +use TypeLang\Parser\Exception\UnreadableSourceException; use TypeLang\Parser\Exception\UnrecognizedSyntaxException; use TypeLang\Parser\Exception\UnrecognizedTokenException; use TypeLang\Parser\Partial\FailureParsedResult; @@ -62,7 +62,7 @@ public function __construct( /** * Reads the source whole and returns the type it is written of. * - * @throws ParseException in case of the source is no type of its own + * @throws ParserException in case of the source is no type of its own */ public function parse(ReadableInterface $source): TypeNode { @@ -79,7 +79,7 @@ public function parse(ReadableInterface $source): TypeNode * Reads as much of the source as the grammar describes and returns the * type that part is written of. * - * @throws ParseException in case of an internal error occurs + * @throws ParserException in case of an internal error occurs */ public function partial(ReadableInterface $source): ParsedResult { @@ -107,7 +107,7 @@ public function partial(ReadableInterface $source): ParsedResult * Tells whether the source is a type the grammar describes, building * nothing of it. * - * @throws ParseException in case of an internal error occurs + * @throws ParserException in case of an internal error occurs */ public function validate(ReadableInterface $source): CheckResult { @@ -140,7 +140,7 @@ public function validate(ReadableInterface $source): CheckResult * Reads the source into the type it describes. * * @return SuccessfulResult|FailureResult - * @throws ParseException in case of the grammar cannot be run + * @throws ParserException in case of the grammar cannot be run */ private function build(ReadableInterface $source): SuccessfulResult|FailureResult { @@ -157,7 +157,7 @@ private function build(ReadableInterface $source): SuccessfulResult|FailureResul * Reads the source without building anything of it. * * @return SuccessfulResult|FailureResult - * @throws ParseException in case of the grammar cannot be run + * @throws ParserException in case of the grammar cannot be run */ private function check(ReadableInterface $source): SuccessfulResult|FailureResult { @@ -174,18 +174,13 @@ private function check(ReadableInterface $source): SuccessfulResult|FailureResul * Converts whatever the grammar raises while it reads into the error of * this parser. */ - private function raised(\Throwable $e, ReadableInterface $source): ParseException + private function raised(\Throwable $e, ReadableInterface $source): ParserException { return match (true) { - $e instanceof ParseException => $e, - $e instanceof SemanticException - => SemanticParseException::becauseSemanticErrorOccurs($e, $source), + $e instanceof ParserException => $e, $e instanceof SourceExceptionInterface - => InternalParseException::becauseSourceIsUnreadable($e), - default => InternalParseException::becauseInternalErrorOccurs( - statement: $source->content, - e: $e, - ), + => UnreadableSourceException::becauseSourceIsUnreadable($source, $e), + default => InternalParseException::becauseInternalErrorOccurs($source, $e), }; } @@ -194,7 +189,7 @@ private function raised(\Throwable $e, ReadableInterface $source): ParseExceptio * has stopped at nothing ends at. * * @return int<0, max> - * @throws ParseException in case of the source cannot be read + * @throws ParserExceptionInterface in case of the source cannot be read */ private function length(ReadableInterface $source): int { @@ -207,7 +202,6 @@ private function length(ReadableInterface $source): int /** * @param int<0, max> $offset - * * @throws SourceExceptionInterface */ private function createPosition(ReadableInterface $source, int $offset): PositionInterface @@ -219,34 +213,25 @@ private function createPosition(ReadableInterface $source, int $offset): Positio * Converts the error of the grammar into the error of this parser. * * @param FailureResult|PartialResult $result - * - * @throws SourceExceptionInterface */ - private function createError(FailureResult|PartialResult $result, ReadableInterface $source): ParseException + private function createError(FailureResult|PartialResult $result, ReadableInterface $source): ParserException { $error = $result->error; if (!$error instanceof GrammarUnexpectedTokenException) { - return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( - statement: $source->content, - offset: $error->token->offset, - ); + return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized($source, $error->token); } // An input the lexer says nothing about is reported as an unrecognized // one rather than as a token in a wrong place. if ($error->token->channel === Channel::Unknown) { - return UnrecognizedTokenException::becauseTokenIsUnrecognized( - token: $error->token->value, - statement: $source->content, - offset: $error->token->offset, - ); + return UnrecognizedTokenException::becauseTokenIsUnrecognized($source, $error->token); } return UnexpectedTokenException::becauseTokenIsUnexpected( - token: $error->token->value, - statement: $source->content, - offset: $error->token->offset, + message: $error->getMessage(), + source: $source, + token: $error->token, ); } } diff --git a/libs/parser/src/Traverser/DumperVisitor.php b/libs/parser/src/Traverser/DumperVisitor.php index be5f757..4af86b3 100644 --- a/libs/parser/src/Traverser/DumperVisitor.php +++ b/libs/parser/src/Traverser/DumperVisitor.php @@ -61,7 +61,9 @@ private function printNodePropertiesSuffix(Node $node): string { $result = []; - foreach (new \ReflectionObject($node)->getProperties() as $property) { + $reflection = new \ReflectionObject($node); + + foreach ($reflection->getProperties() as $property) { // Skip readonly + static and builtin "offset" properties if ($property->isStatic() || $property->isReadOnly() || $property->getName() === 'offset') { continue; diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php index b55691a..c07138f 100644 --- a/libs/parser/src/TypeParser.php +++ b/libs/parser/src/TypeParser.php @@ -9,8 +9,6 @@ use Phplrt\Contracts\Source\ReadableInterface; use Phplrt\Contracts\Source\SourceFactoryInterface; use Phplrt\Source\SourceFactory; -use TypeLang\Parser\Exception\InternalParseException; -use TypeLang\Parser\Exception\ParseException; use TypeLang\Parser\Internal\Executor; use TypeLang\Parser\Partial\ParsedResult; use TypeLang\Parser\Validation\CheckResult; @@ -69,15 +67,12 @@ public function validate(#[Language('PHP')] mixed $source): CheckResult } /** - * @throws ParseException in case of the source cannot be read + * @throws SourceExceptionInterface in case of no source can be created out + * of the given value */ private function source(mixed $source): ReadableInterface { - try { - return $this->sources->create($source); - } catch (SourceExceptionInterface $e) { - throw InternalParseException::becauseSourceIsUnreadable($e); - } + return $this->sources->create($source); } /** diff --git a/libs/parser/src/TypeResolver.php b/libs/parser/src/TypeResolver.php index 1a2490b..13d0a40 100644 --- a/libs/parser/src/TypeResolver.php +++ b/libs/parser/src/TypeResolver.php @@ -116,8 +116,8 @@ public function withTypeImportAs(string $name, string $alias): self */ public function withTypeImportsFromClass(\ReflectionClass $class): self { - $statements = new PhpUseStatementsReader() - ->getClassUseStatements($class); + $reader = new PhpUseStatementsReader(); + $statements = $reader->getClassUseStatements($class); return new self([...$this->imports, ...$statements]); } @@ -162,8 +162,8 @@ public function withTypeImportsFromFunction(\ReflectionFunctionAbstract $functio return $this->withTypeImportsFromClass($function->getDeclaringClass()); } - $statements = new PhpUseStatementsReader() - ->getFunctionUseStatements($function); + $reader = new PhpUseStatementsReader(); + $statements = $reader->getFunctionUseStatements($function); return new self([...$this->imports, ...$statements]); } diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php index 32c9abb..1f82480 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php @@ -48,7 +48,23 @@ public function getFunctionUseStatements(\ReflectionFunctionAbstract $function): $header = ''; } - return [...$this->parse($function->getNamespaceName(), $header)]; + return [...$this->parse($this->getFunctionNamespace($function, $header), $header)]; + } + + /** + * Returns the namespace the given function is written in. + */ + private function getFunctionNamespace(\ReflectionFunctionAbstract $function, string $header): string + { + if (\PHP_VERSION_ID < 80400) { + return $function->getNamespaceName(); + } + + if ($function->isClosure()) { + return $this->namespace->findLast($this->lex($header)); + } + + return $function->getNamespaceName(); } /** diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php index f67b96d..772cdd0 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php @@ -6,6 +6,29 @@ final class NamespaceFinder { + /** + * Returns the last namespace the given source declares, which is the one + * whatever the source ends with is written in. + * + * @param \Iterator $tokens + */ + public function findLast(\Iterator $tokens): string + { + $result = ''; + + while ($tokens->valid()) { + if ($tokens->current()->id === \T_NAMESPACE) { + $result = $this->readNamespace($tokens); + + continue; + } + + $tokens->next(); + } + + return $result; + } + /** * @param \Iterator $tokens * @return \Iterator From 5d64d8f889fa1257e4ac18052ae07730c0794fbc Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 06:34:07 +0300 Subject: [PATCH 097/119] Fix parser tests --- .../tests/Exception/ParseExceptionTest.php | 237 ++++++++++-------- .../tests/Exception/SemanticExceptionTest.php | 124 ++++----- libs/parser/tests/Syntax/CallableTest.php | 14 +- libs/parser/tests/Syntax/ConditionalTest.php | 2 +- libs/parser/tests/Syntax/FloatLiteralTest.php | 2 +- libs/parser/tests/Syntax/GenericTest.php | 6 +- libs/parser/tests/Syntax/GroupingTest.php | 2 +- libs/parser/tests/Syntax/LogicalTest.php | 4 +- .../tests/Syntax/MalformedInputTest.php | 70 ++++-- libs/parser/tests/Syntax/NameTest.php | 4 +- libs/parser/tests/Syntax/OffsetAccessTest.php | 2 +- libs/parser/tests/Syntax/ShapeTest.php | 4 +- libs/parser/tests/Syntax/SyntaxTestCase.php | 6 +- libs/parser/tests/Syntax/TriviaTest.php | 2 +- libs/parser/tests/TypeParserTest.php | 4 +- .../TypeResolver/TypeResolverImportsTest.php | 11 +- 16 files changed, 278 insertions(+), 216 deletions(-) diff --git a/libs/parser/tests/Exception/ParseExceptionTest.php b/libs/parser/tests/Exception/ParseExceptionTest.php index 5d41208..ba3bd0c 100644 --- a/libs/parser/tests/Exception/ParseExceptionTest.php +++ b/libs/parser/tests/Exception/ParseExceptionTest.php @@ -5,137 +5,201 @@ namespace TypeLang\Parser\Tests\Exception; use PHPUnit\Framework\Attributes\Test; +use Phplrt\Contracts\Lexer\Channel; +use Phplrt\Contracts\Lexer\ChannelInterface; +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\ReadableInterface; +use Phplrt\Lexer\Token\Token; use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\InternalParseException; -use TypeLang\Parser\Exception\ParseException; -use TypeLang\Parser\Exception\ParserExceptionInterface; -use TypeLang\Parser\Exception\SemanticParseException; -use TypeLang\Parser\Exception\ShapeFieldDuplicationException; +use TypeLang\Parser\Exception\ParserException; use TypeLang\Parser\Exception\UnexpectedTokenException; +use TypeLang\Parser\Exception\UnreadableSourceException; use TypeLang\Parser\Exception\UnrecognizedSyntaxException; use TypeLang\Parser\Exception\UnrecognizedTokenException; use TypeLang\Parser\Tests\TestCase; final class ParseExceptionTest extends TestCase { + private static function source(string $statement): ReadableInterface + { + return SourceFactory::createDefault()->create($statement); + } + + /** + * @param int<0, max> $offset + * @param non-empty-string|null $name + */ + private static function token( + string $value, + int $offset = 0, + ?string $name = 'T_NAME', + ChannelInterface $channel = Channel::Default, + ): TokenInterface { + return new Token( + id: 0, + name: $name, + channel: $channel, + value: $value, + offset: $offset, + ); + } + + private static function unexpected(string $statement, string $message = 'Syntax error'): UnexpectedTokenException + { + return UnexpectedTokenException::becauseTokenIsUnexpected( + message: $message, + source: self::source($statement), + token: self::token('foo', 4), + ); + } + #[Test] - public function everyParseExceptionIsAParserException(): void + public function everyParseExceptionIsALogicError(): void { - $exception = InternalParseException::becauseInternalErrorOccurs('int', new \LogicException()); + $exception = InternalParseException::becauseInternalErrorOccurs( + self::source('int'), + new \LogicException(), + ); - self::assertInstanceOf(ParserExceptionInterface::class, $exception); + self::assertInstanceOf(ParserException::class, $exception); self::assertInstanceOf(\LogicException::class, $exception); } #[Test] - public function theUnexpectedTokenIsReportedWithItsLocation(): void + public function theMessageCarriesTheSourceButNoPlace(): void { - $exception = UnexpectedTokenException::becauseTokenIsUnexpected('foo', 'int|foo', 4); + $source = self::source('int|foo'); + $token = self::token('foo', 4); + + $exception = UnexpectedTokenException::becauseTokenIsUnexpected( + message: 'Syntax error, unexpected "foo" (T_NAME)', + source: $source, + token: $token, + ); self::assertSame( - 'Syntax error, unexpected "foo" in "int|foo" at column 5', + 'Syntax error, unexpected "foo" (T_NAME) in "int|foo"', $exception->getMessage(), ); - self::assertSame(ParseException::ERROR_CODE_UNEXPECTED_TOKEN, $exception->getCode()); - } - - #[Test] - public function theStatementIsOmittedInCaseOfItEqualsTheToken(): void - { - $exception = UnexpectedTokenException::becauseTokenIsUnexpected('foo', 'foo', 0); - - self::assertSame('Syntax error, unexpected "foo" at column 1', $exception->getMessage()); + self::assertSame($source, $exception->source); + self::assertSame($token, $exception->token); } #[Test] - public function theEndOfInputIsReportedInsteadOfANullToken(): void + public function theRenderedErrorCarriesTheLocation(): void { - $exception = UnexpectedTokenException::becauseTokenIsUnexpected("\0", 'int|', 4); + $exception = self::unexpected('int|foo'); - self::assertSame( - 'Syntax error, unexpected end of input in "int|" at column 5', - $exception->getMessage(), + self::assertStringContainsString( + 'Syntax error in "int|foo" on line 1 at column 5', + (string) $exception, ); } #[Test] - public function theEndOfInputIsReportedInsteadOfAnEmptyToken(): void + public function thePrintedErrorIsTheOnePhpPrints(): void { - $exception = UnexpectedTokenException::becauseTokenIsUnexpected('', 'int|', 4); + $exception = self::unexpected('int|foo'); - self::assertStringContainsString('unexpected end of input', $exception->getMessage()); + $printed = (string) $exception; + + self::assertStringStartsWith(UnexpectedTokenException::class, $printed); + self::assertStringContainsString('Syntax error in "int|foo"', $printed); + self::assertStringContainsString('Stack trace:', $printed); + self::assertStringContainsString($exception->getFile(), $printed); } #[Test] - public function theDoubleQuoteTokenIsReportedByItsName(): void + public function thePrintedErrorLeavesTheMessageAlone(): void { - $exception = UnexpectedTokenException::becauseTokenIsUnexpected('"', 'a"', 1); + $exception = self::unexpected('int|foo'); + + $printed = (string) $exception; - self::assertStringContainsString('unexpected double quote (")', $exception->getMessage()); + self::assertSame('Syntax error in "int|foo"', $exception->getMessage()); + self::assertSame($printed, (string) $exception); } #[Test] public function theMultilineStatementIsReportedUsingTheLineAndColumn(): void { - $exception = UnexpectedTokenException::becauseTokenIsUnexpected('x', "int|\nx", 5); + $exception = UnexpectedTokenException::becauseTokenIsUnexpected( + message: 'Syntax error', + source: self::source("int|\nx"), + token: self::token('x', 5), + ); - self::assertStringEndsWith('on line 2 at column 1', $exception->getMessage()); + self::assertStringContainsString('on line 2 at column 1', (string) $exception); } #[Test] public function theLongStatementIsTruncated(): void { $statement = \str_repeat('x', 100) . 'foo'; - $exception = UnexpectedTokenException::becauseTokenIsUnexpected('foo', $statement, 100); + $exception = self::unexpected($statement); - self::assertStringContainsString('…', $exception->getMessage()); - self::assertStringNotContainsString($statement, $exception->getMessage()); + self::assertStringContainsString('…', (string) $exception); + self::assertStringNotContainsString($statement, (string) $exception); } #[Test] - public function theUnrecognizedTokenIsReportedWithItsLocation(): void + public function theUnrecognizedTokenIsReportedByItsValue(): void { - $exception = UnrecognizedTokenException::becauseTokenIsUnrecognized('%', 'int|%', 4); + $exception = UnrecognizedTokenException::becauseTokenIsUnrecognized( + self::source('int|%'), + self::token('%', 4, null, Channel::Unknown), + ); self::assertSame( - 'Syntax error, unrecognized "%" in "int|%" at column 5', + 'Syntax error, unexpected "%" (unknown token) in "int|%"', $exception->getMessage(), ); - self::assertSame(ParseException::ERROR_CODE_UNRECOGNIZED_TOKEN, $exception->getCode()); + self::assertStringContainsString('on line 1 at column 5', (string) $exception); } #[Test] - public function theUnrecognizedSyntaxIsReportedWithItsLocation(): void + public function theEndOfInputIsReportedInsteadOfAToken(): void { - $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized('int|', 4); + $exception = UnrecognizedTokenException::becauseTokenIsUnrecognized( + self::source('int|'), + self::token('', 4, null, Channel::EndOfInput), + ); - self::assertSame( - 'Internal syntax error, in "int|" at column 5', - $exception->getMessage(), + self::assertStringStartsWith('Syntax error, unexpected end of input', $exception->getMessage()); + } + + #[Test] + public function theUnrecognizedSyntaxIsReportedWithItsLocation(): void + { + $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( + self::source('int|'), + self::token('', 4, null, Channel::EndOfInput), ); - self::assertSame(ParseException::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR, $exception->getCode()); + + self::assertSame('Internal syntax error in "int|"', $exception->getMessage()); + self::assertStringContainsString('on line 1 at column 5', (string) $exception); } #[Test] public function theBlankStatementIsReportedAsEmpty(): void { - $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized(' ', 0); + $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( + self::source(' '), + self::token('', 0, null, Channel::EndOfInput), + ); - self::assertStringContainsString('', $exception->getMessage()); + self::assertStringContainsString('', (string) $exception); } #[Test] public function theInternalErrorKeepsThePreviousException(): void { $previous = new \LogicException('oops'); - $exception = InternalParseException::becauseInternalErrorOccurs('int', $previous); + $exception = InternalParseException::becauseInternalErrorOccurs(self::source('int'), $previous); - self::assertSame( - 'An internal error occurred while parsing "int"', - $exception->getMessage(), - ); + self::assertSame('An internal error occurred while parsing "int"', $exception->getMessage()); self::assertSame($previous, $exception->getPrevious()); - self::assertSame(ParseException::ERROR_CODE_INTERNAL_ERROR, $exception->getCode()); } #[Test] @@ -144,65 +208,26 @@ public function theUnreadableSourceIsReportedUsingTheOriginalMessage(): void $previous = new class ('source is unreadable') extends \RuntimeException implements \Phplrt\Contracts\Source\Exception\SourceExceptionInterface {}; - $exception = InternalParseException::becauseSourceIsUnreadable($previous); + $source = self::source('int'); + $exception = UnreadableSourceException::becauseSourceIsUnreadable($source, $previous); + self::assertInstanceOf(ParserException::class, $exception); self::assertSame('source is unreadable', $exception->getMessage()); + self::assertSame($source, $exception->source); self::assertSame($previous, $exception->getPrevious()); - self::assertSame(ParseException::ERROR_CODE_INTERNAL_ERROR, $exception->getCode()); - } - - #[Test] - public function theSemanticErrorIsRebasedOntoTheWholeSource(): void - { - $exception = SemanticParseException::becauseSemanticErrorOccurs( - e: ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a', 10), - source: SourceFactory::createDefault()->create('array{a: int, a: int}'), - ); - - self::assertSame( - 'Duplicate key "a" in "array{a: int, a: int}" at column 11', - $exception->getMessage(), - ); - } - - #[Test] - public function theSemanticErrorCodeIsShiftedByTheBaseValue(): void - { - $semantic = ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a', 10); - - $exception = SemanticParseException::becauseSemanticErrorOccurs( - e: $semantic, - source: SourceFactory::createDefault()->create('array{a: int, a: int}'), - ); - - self::assertSame( - ParseException::ERROR_CODE_SEMANTIC_ERROR_BASE + $semantic->getCode(), - $exception->getCode(), - ); } + /** + * Every error is told apart by the class it is of, so none of them carries + * a code. + */ #[Test] - public function theSemanticErrorMessageIsCapitalized(): void + public function everyErrorIsToldApartByItsClassAlone(): void { - $exception = SemanticParseException::becauseSemanticErrorOccurs( - e: ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('a'), - source: SourceFactory::createDefault()->create('array{a: int, a: int}'), - ); - - self::assertStringStartsWith('Duplicate', $exception->getMessage()); - } - - #[Test] - public function everyErrorCodeIsUnique(): void - { - $codes = [ - ParseException::ERROR_CODE_UNEXPECTED_TOKEN, - ParseException::ERROR_CODE_UNRECOGNIZED_TOKEN, - ParseException::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR, - ParseException::ERROR_CODE_INTERNAL_ERROR, - ParseException::ERROR_CODE_SEMANTIC_ERROR_BASE, - ]; - - self::assertSame($codes, \array_values(\array_unique($codes))); + self::assertSame(0, self::unexpected('int|foo')->getCode()); + self::assertSame(0, InternalParseException::becauseInternalErrorOccurs( + self::source('int'), + new \LogicException(), + )->getCode()); } } diff --git a/libs/parser/tests/Exception/SemanticExceptionTest.php b/libs/parser/tests/Exception/SemanticExceptionTest.php index 22b6584..9c403f2 100644 --- a/libs/parser/tests/Exception/SemanticExceptionTest.php +++ b/libs/parser/tests/Exception/SemanticExceptionTest.php @@ -6,10 +6,13 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Phplrt\Contracts\Source\ReadableInterface; +use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\FeatureNotAllowedException; use TypeLang\Parser\Exception\InternalSemanticException; use TypeLang\Parser\Exception\InvalidConditionalOperatorException; use TypeLang\Parser\Exception\ParserExceptionInterface; +use TypeLang\Parser\Exception\ParsingException; use TypeLang\Parser\Exception\SemanticException; use TypeLang\Parser\Exception\ShapeFieldDuplicationException; use TypeLang\Parser\Exception\ShapeKeysMixingException; @@ -18,98 +21,113 @@ final class SemanticExceptionTest extends TestCase { + private static function source(string $statement = 'array{a: int, a: int}'): ReadableInterface + { + return SourceFactory::createDefault()->create($statement); + } + /** - * @return iterable + * @return iterable */ public static function factoryDataProvider(): iterable { yield 'feature not allowed' => [ - static fn(int $offset): SemanticException - => FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes', $offset), + static fn(ReadableInterface $source, int $offset): SemanticException + => FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes', $source, $offset), ]; yield 'unexpected sub-node' => [ - static fn(int $offset): SemanticException - => InternalSemanticException::becauseSubNodeIsUnexpected('Example', $offset), + static fn(ReadableInterface $source, int $offset): SemanticException + => InternalSemanticException::becauseSubNodeIsUnexpected('Example', $source, $offset), ]; yield 'invalid conditional operator' => [ - static fn(int $offset): SemanticException - => InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~', $offset), + static fn(ReadableInterface $source, int $offset): SemanticException + => InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~', $source, $offset), ]; yield 'shape field duplication' => [ - static fn(int $offset): SemanticException - => ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key', $offset), + static fn(ReadableInterface $source, int $offset): SemanticException + => ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key', $source, $offset), ]; yield 'shape keys mixing' => [ - static fn(int $offset): SemanticException - => ShapeKeysMixingException::becauseShapeKeysAreMixed($offset), + static fn(ReadableInterface $source, int $offset): SemanticException + => ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset), ]; yield 'variadic with default' => [ - static fn(int $offset): SemanticException - => VariadicWithDefaultException::becauseVariadicHasDefault($offset), + static fn(ReadableInterface $source, int $offset): SemanticException + => VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset), ]; } /** - * @param \Closure(int):SemanticException $factory + * @param \Closure(ReadableInterface,int):SemanticException $factory */ #[DataProvider('factoryDataProvider')] #[Test] - public function everySemanticExceptionIsAParserException(\Closure $factory): void + public function everySemanticExceptionIsAParseError(\Closure $factory): void { - $exception = $factory(0); + $exception = $factory(self::source(), 0); self::assertInstanceOf(ParserExceptionInterface::class, $exception); - self::assertInstanceOf(\LogicException::class, $exception); + self::assertInstanceOf(ParsingException::class, $exception); } /** - * @param \Closure(int):SemanticException $factory + * @param \Closure(ReadableInterface,int):SemanticException $factory */ #[DataProvider('factoryDataProvider')] #[Test] - public function everySemanticExceptionKeepsTheOffset(\Closure $factory): void + public function everySemanticExceptionKeepsTheSourceAndThePlace(\Closure $factory): void { - $exception = $factory(42); + $source = self::source(); + $exception = $factory($source, 14); - self::assertSame(42, $exception->offset); - self::assertSame(42, $exception->getOffset()); + self::assertSame($source, $exception->source); + self::assertSame(14, $exception->token->offset); + self::assertSame(14, $exception->getOffset()); } /** - * @param \Closure(int):SemanticException $factory + * @param \Closure(ReadableInterface,int):SemanticException $factory */ #[DataProvider('factoryDataProvider')] #[Test] - public function everySemanticExceptionHasANonEmptyMessage(\Closure $factory): void + public function everySemanticExceptionCarriesTheSourceInItsMessage(\Closure $factory): void { - self::assertNotSame('', $factory(0)->getMessage()); + $exception = $factory(self::source(), 0); + + self::assertStringEndsWith('in "array{a: int, a: int}"', $exception->getMessage()); } + /** + * @param \Closure(ReadableInterface,int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] #[Test] - public function theFeatureNameIsCapitalizedInTheMessage(): void + public function everySemanticExceptionIsPrintedWithItsPlace(\Closure $factory): void { - $exception = FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields'); + $exception = $factory(self::source(), 14); - self::assertSame('Shape fields not allowed', $exception->getMessage()); + self::assertStringContainsString('on line 1 at column 15', (string) $exception); } #[Test] - public function theFeatureNotAllowedOffsetDefaultsToZero(): void + public function theFeatureNameIsCapitalizedInTheMessage(): void { - self::assertSame(0, FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes')->offset); + $exception = FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', self::source()); + + self::assertStringStartsWith('Shape fields not allowed', $exception->getMessage()); } #[Test] public function theUnexpectedSubNodeIsReported(): void { - $exception = InternalSemanticException::becauseSubNodeIsUnexpected('Example'); + $exception = InternalSemanticException::becauseSubNodeIsUnexpected('Example', self::source()); - self::assertSame( + self::assertStringStartsWith( 'Internal error, unexpected square bracket sub-node Example', $exception->getMessage(), ); @@ -118,56 +136,42 @@ public function theUnexpectedSubNodeIsReported(): void #[Test] public function theInvalidConditionalOperatorIsReported(): void { - $exception = InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~'); + $exception = InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~', self::source()); - self::assertSame('Invalid conditional operator "~"', $exception->getMessage()); - self::assertSame(SemanticException::ERROR_CODE_INVALID_OPERATOR, $exception->getCode()); + self::assertStringStartsWith('Invalid conditional operator "~"', $exception->getMessage()); } #[Test] public function theDuplicatedShapeFieldIsReported(): void { - $exception = ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key'); + $exception = ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key', self::source()); - self::assertSame('Duplicate key "key"', $exception->getMessage()); - self::assertSame(SemanticException::ERROR_CODE_SHAPE_KEY_DUPLICATION, $exception->getCode()); + self::assertStringStartsWith('Duplicate key "key"', $exception->getMessage()); } #[Test] public function theMixedShapeKeysAreReported(): void { - $exception = ShapeKeysMixingException::becauseShapeKeysAreMixed(); + $exception = ShapeKeysMixingException::becauseShapeKeysAreMixed(self::source()); - self::assertSame('Cannot mix explicit and implicit shape keys', $exception->getMessage()); - self::assertSame(SemanticException::ERROR_CODE_SHAPE_KEY_MIX, $exception->getCode()); + self::assertStringStartsWith('Cannot mix explicit and implicit shape keys', $exception->getMessage()); } #[Test] public function theVariadicWithADefaultIsReported(): void { - $exception = VariadicWithDefaultException::becauseVariadicHasDefault(); + $exception = VariadicWithDefaultException::becauseVariadicHasDefault(self::source()); - self::assertSame('Cannot have variadic param with a default', $exception->getMessage()); - self::assertSame(SemanticException::ERROR_CODE_VARIADIC_WITH_DEFAULT, $exception->getCode()); - } - - #[Test] - public function theInternalErrorsHaveNoDedicatedCode(): void - { - self::assertSame(0, InternalSemanticException::becauseSubNodeIsUnexpected('Example')->getCode()); - self::assertSame(0, FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes')->getCode()); + self::assertStringStartsWith('Cannot have variadic param with a default', $exception->getMessage()); } + /** + * @param \Closure(ReadableInterface,int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] #[Test] - public function everyErrorCodeIsUnique(): void + public function everySemanticExceptionIsToldApartByItsClassAlone(\Closure $factory): void { - $codes = [ - SemanticException::ERROR_CODE_SHAPE_KEY_DUPLICATION, - SemanticException::ERROR_CODE_SHAPE_KEY_MIX, - SemanticException::ERROR_CODE_VARIADIC_WITH_DEFAULT, - SemanticException::ERROR_CODE_INVALID_OPERATOR, - ]; - - self::assertSame($codes, \array_values(\array_unique($codes))); + self::assertSame(0, $factory(self::source(), 0)->getCode()); } } diff --git a/libs/parser/tests/Syntax/CallableTest.php b/libs/parser/tests/Syntax/CallableTest.php index c7a137e..318c8d5 100644 --- a/libs/parser/tests/Syntax/CallableTest.php +++ b/libs/parser/tests/Syntax/CallableTest.php @@ -139,7 +139,7 @@ public function testOptionalParameter(): void public function testVariadicMarkerCannotPrecedeTheParameterType(): void { - $this->expectParsingException('unexpected "..."'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(...T)'); } @@ -158,21 +158,21 @@ public function testVariadicParameter(): void public function testNameCannotFollowTheDefaultMarker(): void { - $this->expectParsingException('unexpected "$name"'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(T= $name)'); } public function testAmpersandMustFollowParameterType(): void { - $this->expectParsingException('unexpected "&"'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(&T)'); } public function testParameterWithoutATypeIsNotAllowed(): void { - $this->expectParsingException('unexpected ")"'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo($name)'); } @@ -186,7 +186,7 @@ public function testVariadicParameterCannotHaveDefault(): void public function testLeadingCommaIsNotAllowed(): void { - $this->expectParsingException('unexpected ","'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(,T)'); } @@ -211,14 +211,14 @@ public function testReferenceAndVariadicMarkersOfATypedParameterAreOrdered(): vo public function testTypedParameterCannotPutTheReferenceAfterTheVariadic(): void { - $this->expectParsingException('unexpected "&"'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(T ...&$name)'); } public function testModifiersWithoutATypeAreNotAllowed(): void { - $this->expectParsingException('unexpected "&"'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(&...$name)'); } diff --git a/libs/parser/tests/Syntax/ConditionalTest.php b/libs/parser/tests/Syntax/ConditionalTest.php index f11a7cf..d219961 100644 --- a/libs/parser/tests/Syntax/ConditionalTest.php +++ b/libs/parser/tests/Syntax/ConditionalTest.php @@ -90,7 +90,7 @@ public function testEqualityOperatorIsNotAllowed(): void public function testInequalityOperatorIsNotAllowed(): void { - $this->expectParsingException('unrecognized "!="'); + $this->expectParsingException('unexpected "!="'); $this->parse('A != B ? C : D'); } diff --git a/libs/parser/tests/Syntax/FloatLiteralTest.php b/libs/parser/tests/Syntax/FloatLiteralTest.php index 4cc41b7..190e725 100644 --- a/libs/parser/tests/Syntax/FloatLiteralTest.php +++ b/libs/parser/tests/Syntax/FloatLiteralTest.php @@ -73,7 +73,7 @@ public function testValidFloats(string $type, float $expected): void public function testLeadingAndTrailingNumberCannotBothBeOmitted(): void { - $this->expectParsingException('unrecognized "."'); + $this->expectParsingException('unexpected "."'); $this->parse('.'); } diff --git a/libs/parser/tests/Syntax/GenericTest.php b/libs/parser/tests/Syntax/GenericTest.php index 9a7f5a2..0c570f6 100644 --- a/libs/parser/tests/Syntax/GenericTest.php +++ b/libs/parser/tests/Syntax/GenericTest.php @@ -165,21 +165,21 @@ public function testHintFollowedBySpaceAndRelativeName(): void public function testMissingTemplateArgument(): void { - $this->expectParsingException('unexpected ">"'); + $this->expectParsingException('an argument list must carry at least one argument'); $this->parse('example<>'); } public function testLeadingCommaIsNotAllowed(): void { - $this->expectParsingException('unexpected ","'); + $this->expectParsingException('an argument list must carry at least one argument'); $this->parse('example<,T>'); } public function testHintAllowsOnlyIdentifiers(): void { - $this->expectParsingException('unexpected "User"'); + $this->expectParsingException('an argument list must be closed with a bracket ">"'); $this->parse('Collection<42 User>'); } diff --git a/libs/parser/tests/Syntax/GroupingTest.php b/libs/parser/tests/Syntax/GroupingTest.php index ecfc5fe..6fc7461 100644 --- a/libs/parser/tests/Syntax/GroupingTest.php +++ b/libs/parser/tests/Syntax/GroupingTest.php @@ -308,7 +308,7 @@ public function testAnEmptyGroupIsNoType(): void public function testAGroupIsClosedByTheParenthesisItIsOpenedWith(): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException('a group must be closed with a bracket ")"'); $this->parse('(int'); } diff --git a/libs/parser/tests/Syntax/LogicalTest.php b/libs/parser/tests/Syntax/LogicalTest.php index ace03b6..b67cd54 100644 --- a/libs/parser/tests/Syntax/LogicalTest.php +++ b/libs/parser/tests/Syntax/LogicalTest.php @@ -85,7 +85,7 @@ public function testNullableQuestionMarkMustBeBeforeType(): void public function testDanglingUnionDelimiter(): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException('a union type must carry a type after the vertical bar'); $this->parse('int |'); } @@ -99,7 +99,7 @@ public function testLeadingUnionDelimiter(): void public function testDanglingIntersectionDelimiter(): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException('an intersection type must carry a type after the ampersand'); $this->parse('string &'); } diff --git a/libs/parser/tests/Syntax/MalformedInputTest.php b/libs/parser/tests/Syntax/MalformedInputTest.php index ea3a313..646dbc5 100644 --- a/libs/parser/tests/Syntax/MalformedInputTest.php +++ b/libs/parser/tests/Syntax/MalformedInputTest.php @@ -47,34 +47,68 @@ public function testAnEmptyInputIsNoType(string $type): void * A type that is cut short ends where the input does, so the reading * stops at the end of it rather than at a token. * - * @return iterable + * @return iterable */ public static function truncatedInputDataProvider(): iterable { - yield 'a dangling union' => ['int|']; - yield 'a dangling intersection' => ['int&']; - yield 'a question mark alone' => ['?']; - yield 'an unclosed shape' => ['array{']; - yield 'an unclosed field' => ['array{a:']; - yield 'an unclosed argument list' => ['Some<']; - yield 'an unclosed parameter list' => ['callable(']; - yield 'an unclosed offset' => ['int[']; - yield 'an unclosed group' => ['(int']; - yield 'a name that is a separator short' => ['Some\\']; - yield 'a class constant that is a name short' => ['Some::']; - yield 'a condition without its branches' => ['A is B ?']; - yield 'a condition of a single branch' => ['A is B ? C']; - yield 'a condition without its comparand' => ['A is']; + yield 'a condition without its comparand' => ['A is', 'unexpected end of input']; + + // The grammar describes what is missing wherever it can, and the + // message it carries is reported instead of the token that is absent. + yield 'a dangling union' => [ + 'int|', + 'a union type must carry a type after the vertical bar "|"', + ]; + yield 'a dangling intersection' => [ + 'int&', + 'an intersection type must carry a type after the ampersand "&"', + ]; + yield 'a question mark alone' => [ + '?', + 'a nullable type must carry the type it makes nullable', + ]; + yield 'a name that is a separator short' => [ + 'Some\\', + 'a name must carry a segment after the separator', + ]; + yield 'a class constant that is a name short' => [ + 'Some::', + 'a class constant must carry a name after the double colon', + ]; + yield 'an unclosed shape' => ['array{', 'a shape must be closed with a brace "}"']; + yield 'an unclosed field' => [ + 'array{a:', + 'a shape field must carry a type after the colon ":"', + ]; + yield 'an unclosed argument list' => [ + 'Some<', + 'an argument list must carry at least one argument', + ]; + yield 'an unclosed parameter list' => [ + 'callable(', + 'a parameter list must be closed with a bracket ")"', + ]; + yield 'an unclosed offset' => ['int[', 'an offset must be closed with a bracket "]"']; + yield 'an unclosed group' => ['(int', 'a group must be closed with a bracket ")"']; + yield 'a condition without its branches' => [ + 'A is B ?', + 'a condition must carry the type it is true of', + ]; + yield 'a condition of a single branch' => [ + 'A is B ? C', + 'a condition must be parted with a colon ":"', + ]; } /** * @param non-empty-string $type + * @param non-empty-string $message * @throws \Throwable */ #[DataProvider('truncatedInputDataProvider')] - public function testATruncatedTypeIsRefused(string $type): void + public function testATruncatedTypeIsRefused(string $type, string $message): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException($message); $this->parse($type); } @@ -118,7 +152,7 @@ public static function trailingTokenDataProvider(): iterable yield 'an angle bracket too many' => ['Some>', 'unexpected ">"']; yield 'a brace too many' => ['array{a: int}}', 'unexpected "}"']; yield 'a bracket too many' => ['int[]]', 'unexpected "]"']; - yield 'an empty argument list' => ['Some<>', 'unexpected ">"']; + yield 'an empty argument list' => ['Some<>', 'an argument list must carry at least one argument']; yield 'an empty group' => ['()', 'unexpected ")"']; yield 'a constant of a generic type' => ['Some::CONST', 'unexpected "::"']; yield 'a second type' => ['int string', 'unexpected "string"']; diff --git a/libs/parser/tests/Syntax/NameTest.php b/libs/parser/tests/Syntax/NameTest.php index 3b4f704..93a4fbb 100644 --- a/libs/parser/tests/Syntax/NameTest.php +++ b/libs/parser/tests/Syntax/NameTest.php @@ -153,14 +153,14 @@ public function testNameCannotStartWithDigitFollowedByLetters(): void public function testNameCannotStartWithDash(): void { - $this->expectParsingException('unrecognized "-foo"'); + $this->expectParsingException('unexpected "-foo"'); $this->parse('-foo'); } public function testNamespaceCannotEndWithDelimiter(): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException('a name must carry a segment after the separator'); $this->parse('example\\name\\'); } diff --git a/libs/parser/tests/Syntax/OffsetAccessTest.php b/libs/parser/tests/Syntax/OffsetAccessTest.php index f77880d..1c53b66 100644 --- a/libs/parser/tests/Syntax/OffsetAccessTest.php +++ b/libs/parser/tests/Syntax/OffsetAccessTest.php @@ -72,7 +72,7 @@ public function testComplexOffsetWithGenericsAndShapes(): void public function testOffsetCannotBeDoubleBracketed(): void { - $this->expectParsingException('unexpected "["'); + $this->expectParsingException('an offset must be closed with a bracket "]"'); $this->parse('Collection[[Some]]'); } diff --git a/libs/parser/tests/Syntax/ShapeTest.php b/libs/parser/tests/Syntax/ShapeTest.php index 118f2eb..49a7360 100644 --- a/libs/parser/tests/Syntax/ShapeTest.php +++ b/libs/parser/tests/Syntax/ShapeTest.php @@ -176,7 +176,7 @@ public function testDuplicateKeyIsNotAllowed(): void public function testOptionalValueSyntaxIsNotAllowed(): void { - $this->expectParsingException('unexpected "?"'); + $this->expectParsingException('a shape must be closed with a brace "}"'); $this->parse('array{key: Type?}'); } @@ -231,7 +231,7 @@ public function testKeyIsANumberAStringOrANameAlone(string $type): void */ public function testKeyDoesNotReachBeyondAPrimaryType(): void { - $this->expectParsingException('unexpected ":"'); + $this->expectParsingException('a shape must be closed with a brace "}"'); $this->parse('array{T is A ? B : C: int}'); } diff --git a/libs/parser/tests/Syntax/SyntaxTestCase.php b/libs/parser/tests/Syntax/SyntaxTestCase.php index bd876cd..616d49e 100644 --- a/libs/parser/tests/Syntax/SyntaxTestCase.php +++ b/libs/parser/tests/Syntax/SyntaxTestCase.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\Group; use TypeLang\Parser\Exception\FeatureNotAllowedException; -use TypeLang\Parser\Exception\ParseException; +use TypeLang\Parser\Exception\ParserException; use TypeLang\Parser\Exception\ParserExceptionInterface; use TypeLang\Parser\Exception\SemanticException; use TypeLang\Parser\Tests\TestCase; @@ -22,13 +22,13 @@ private function expectParserExceptionOf(string $class, ?string $message = null) $this->expectException($class); if ($message !== null) { - $this->expectExceptionMessageIsOrContains($message); + $this->expectExceptionMessage($message); } } protected function expectParsingException(?string $message = null): void { - $this->expectParserExceptionOf(ParseException::class, $message); + $this->expectParserExceptionOf(ParserException::class, $message); } protected function expectSemanticException(?string $message = null): void diff --git a/libs/parser/tests/Syntax/TriviaTest.php b/libs/parser/tests/Syntax/TriviaTest.php index b166b33..c7b7235 100644 --- a/libs/parser/tests/Syntax/TriviaTest.php +++ b/libs/parser/tests/Syntax/TriviaTest.php @@ -233,7 +233,7 @@ public function testTheWordsOfAnIsNotAreCaseSensitive(string $type): void public function testAnUnterminatedBlockCommentIsRefused(): void { - $this->expectParsingException('unrecognized "/*"'); + $this->expectParsingException('unexpected "/*"'); $this->parse('/* the comment that never ends'); } diff --git a/libs/parser/tests/TypeParserTest.php b/libs/parser/tests/TypeParserTest.php index 9c35ffc..7a7d67c 100644 --- a/libs/parser/tests/TypeParserTest.php +++ b/libs/parser/tests/TypeParserTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\Test; use Phplrt\Source\SourceFactory; -use TypeLang\Parser\Exception\ParseException; +use TypeLang\Parser\Exception\ParserException; use TypeLang\Parser\Partial\FailureParsedResult; use TypeLang\Parser\Partial\PartialParsedResult; use TypeLang\Parser\Partial\SuccessfulParsedResult; @@ -71,7 +71,7 @@ public function withFeaturesAffectsTheParsingBehaviour(): void { $parser = (new TypeParser())->withFeatures(shapes: false); - $this->expectException(ParseException::class); + $this->expectException(ParserException::class); $parser->parse('array{a: int}'); } diff --git a/libs/parser/tests/TypeResolver/TypeResolverImportsTest.php b/libs/parser/tests/TypeResolver/TypeResolverImportsTest.php index 2d62148..2b59df4 100644 --- a/libs/parser/tests/TypeResolver/TypeResolverImportsTest.php +++ b/libs/parser/tests/TypeResolver/TypeResolverImportsTest.php @@ -177,17 +177,16 @@ public function theInternalFunctionChangesNothing(): void } #[Test] - /** - * A closure is not a namespaced symbol on its own, so no imports can be - * associated with it. - */ - public function theClosureChangesNothing(): void + public function theClosureIsReadWithTheImportsOfItsFile(): void { $resolver = (new TypeResolver()) ->withTypeImportsFromFunction( new \ReflectionFunction(static fn(): int => 42), ); - self::assertSame('TypeResolver', $this->resolveName($resolver, 'TypeResolver')); + self::assertSame( + 'TypeLang\\Parser\\TypeResolver', + $this->resolveName($resolver, 'TypeResolver'), + ); } } From f25231151fdc7a6fa1795a4146cfeb574e268d76 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 06:42:36 +0300 Subject: [PATCH 098/119] Fix reader tests --- libs/reader/tests/ConstantReaderTest.php | 17 ++++++++++++----- libs/reader/tests/FunctionReaderTest.php | 4 ++++ libs/reader/tests/MethodReaderTest.php | 5 ++++- libs/reader/tests/ParameterReaderTest.php | 5 ++++- libs/reader/tests/PropertyReaderTest.php | 12 +++++++++--- libs/reader/tests/ReflectionReaderTest.php | 6 ++++-- libs/reader/tests/StaticMethodReaderTest.php | 5 ++++- libs/reader/tests/Stub/ConstantReaderStub.php | 15 --------------- libs/reader/tests/Stub/MethodReaderStub.php | 2 -- libs/reader/tests/Stub/ParameterReaderStub.php | 2 -- libs/reader/tests/Stub/PropertyReaderStub.php | 11 ----------- .../tests/Stub/StaticMethodReaderStub.php | 2 -- .../reader/tests/Stub/functions_reader_stub.php | 2 -- 13 files changed, 41 insertions(+), 47 deletions(-) diff --git a/libs/reader/tests/ConstantReaderTest.php b/libs/reader/tests/ConstantReaderTest.php index f61e287..c9fe565 100644 --- a/libs/reader/tests/ConstantReaderTest.php +++ b/libs/reader/tests/ConstantReaderTest.php @@ -6,9 +6,11 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\ConstantReaderInterface; use TypeLang\Reader\Tests\Stub\__ConstantReaderEnum; use TypeLang\Reader\Tests\Stub\ConstantReaderStub; +use TypeLang\Reader\Tests\Stub\ConstantReaderStub83; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; use TypeLang\Type\UnionTypeNode; @@ -17,20 +19,22 @@ class ConstantReaderTest extends ReaderTestCase { #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testSimpleType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'SINGLE'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'SINGLE'), ); self::assertSameType(self::builtin('int'), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testUnionType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'UNION'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'UNION'), ); self::assertSameType(new UnionTypeNode([ @@ -40,10 +44,11 @@ public function testUnionType(ConstantReaderInterface $reader): void } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testIntersectionType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'INTERSECTION'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'INTERSECTION'), ); self::assertSameType(new IntersectionTypeNode([ @@ -53,10 +58,11 @@ public function testIntersectionType(ConstantReaderInterface $reader): void } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testCompositeType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'COMPOSITE'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'COMPOSITE'), ); self::assertSameType(new UnionTypeNode([ @@ -69,10 +75,11 @@ public function testCompositeType(ConstantReaderInterface $reader): void } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testNullableType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'NULLABLE'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'NULLABLE'), ); self::assertSameType(new NullableTypeNode(self::builtin('int')), $type); diff --git a/libs/reader/tests/FunctionReaderTest.php b/libs/reader/tests/FunctionReaderTest.php index 5803705..8cf9b85 100644 --- a/libs/reader/tests/FunctionReaderTest.php +++ b/libs/reader/tests/FunctionReaderTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\FunctionReaderInterface; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; @@ -56,8 +57,11 @@ function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_intersection_t } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(FunctionReaderInterface $reader): void { + require_once __DIR__ . '/Stub/functions_reader_stub_82.php'; + $type = $reader->findFunctionType( function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_composite_type'), ); diff --git a/libs/reader/tests/MethodReaderTest.php b/libs/reader/tests/MethodReaderTest.php index e225a4c..e2955be 100644 --- a/libs/reader/tests/MethodReaderTest.php +++ b/libs/reader/tests/MethodReaderTest.php @@ -6,8 +6,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\FunctionReaderInterface; use TypeLang\Reader\Tests\Stub\MethodReaderStub; +use TypeLang\Reader\Tests\Stub\MethodReaderStub82; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\UnionTypeNode; @@ -51,10 +53,11 @@ function: new \ReflectionMethod(MethodReaderStub::class, 'getIntersectionType'), } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(FunctionReaderInterface $reader): void { $type = $reader->findFunctionType( - function: new \ReflectionMethod(MethodReaderStub::class, 'getCompositeType'), + function: new \ReflectionMethod(MethodReaderStub82::class, 'getCompositeType'), ); self::assertSameType(new UnionTypeNode([ diff --git a/libs/reader/tests/ParameterReaderTest.php b/libs/reader/tests/ParameterReaderTest.php index 2a14eca..6361a33 100644 --- a/libs/reader/tests/ParameterReaderTest.php +++ b/libs/reader/tests/ParameterReaderTest.php @@ -6,8 +6,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\ParameterReaderInterface; use TypeLang\Reader\Tests\Stub\ParameterReaderStub; +use TypeLang\Reader\Tests\Stub\ParameterReaderStub82; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; use TypeLang\Type\UnionTypeNode; @@ -52,10 +54,11 @@ public function testIntersectionType(ParameterReaderInterface $reader): void } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(ParameterReaderInterface $reader): void { $type = $reader->findParameterType( - parameter: new \ReflectionParameter(ParameterReaderStub::withCompositeType(...), 0), + parameter: new \ReflectionParameter(ParameterReaderStub82::withCompositeType(...), 0), ); self::assertSameType(new UnionTypeNode([ diff --git a/libs/reader/tests/PropertyReaderTest.php b/libs/reader/tests/PropertyReaderTest.php index aab7d7e..d249ea4 100644 --- a/libs/reader/tests/PropertyReaderTest.php +++ b/libs/reader/tests/PropertyReaderTest.php @@ -6,9 +6,12 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\PropertyAccessDirection; use TypeLang\Reader\PropertyReaderInterface; use TypeLang\Reader\Tests\Stub\PropertyReaderStub; +use TypeLang\Reader\Tests\Stub\PropertyReaderStub82; +use TypeLang\Reader\Tests\Stub\PropertyReaderStub84; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; use TypeLang\Type\UnionTypeNode; @@ -53,10 +56,11 @@ public function testIntersectionType(PropertyReaderInterface $reader): void } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(PropertyReaderInterface $reader): void { $type = $reader->findPropertyType( - property: new \ReflectionProperty(PropertyReaderStub::class, 'compositeType'), + property: new \ReflectionProperty(PropertyReaderStub82::class, 'compositeType'), ); self::assertSameType(new UnionTypeNode([ @@ -89,10 +93,11 @@ public function testUntypedPropertyHasNoType(PropertyReaderInterface $reader): v } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.4')] public function testHookedPropertyReadType(PropertyReaderInterface $reader): void { $type = $reader->findPropertyType( - property: new \ReflectionProperty(PropertyReaderStub::class, 'hookedType'), + property: new \ReflectionProperty(PropertyReaderStub84::class, 'hookedType'), access: PropertyAccessDirection::Read, ); @@ -100,10 +105,11 @@ public function testHookedPropertyReadType(PropertyReaderInterface $reader): voi } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.4')] public function testHookedPropertyWriteType(PropertyReaderInterface $reader): void { $type = $reader->findPropertyType( - property: new \ReflectionProperty(PropertyReaderStub::class, 'hookedType'), + property: new \ReflectionProperty(PropertyReaderStub84::class, 'hookedType'), access: PropertyAccessDirection::Write, ); diff --git a/libs/reader/tests/ReflectionReaderTest.php b/libs/reader/tests/ReflectionReaderTest.php index 0a7abcc..960892b 100644 --- a/libs/reader/tests/ReflectionReaderTest.php +++ b/libs/reader/tests/ReflectionReaderTest.php @@ -5,11 +5,12 @@ namespace TypeLang\Reader\Tests; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\Exception\UnrecognizedConstantTypeException; use TypeLang\Reader\Exception\UnrecognizedReflectionTypeException; use TypeLang\Reader\Exception\UnrecognizedTypeException; use TypeLang\Reader\ReflectionReader; -use TypeLang\Reader\Tests\Stub\ConstantReaderStub; +use TypeLang\Reader\Tests\Stub\ConstantReaderStub83; #[Group('type-lang/reader')] class ReflectionReaderTest extends ReaderTestCase @@ -75,11 +76,12 @@ public function testMixedTypeIsNotNullable(): void self::assertSameType(self::builtin('mixed'), $type); } + #[RequiresPhp('>= 8.3')] public function testContextualExceptionWrapsAndChainsRootCause(): void { $reader = new ReflectionReader(); - $constant = new \ReflectionClassConstant(ConstantReaderStub::class, 'SINGLE'); + $constant = new \ReflectionClassConstant(ConstantReaderStub83::class, 'SINGLE'); $previous = UnrecognizedTypeException::becauseTypeIsUnrecognized($constant->getType()); $exception = UnrecognizedConstantTypeException::becauseConstantTypeIsUnrecognized( diff --git a/libs/reader/tests/StaticMethodReaderTest.php b/libs/reader/tests/StaticMethodReaderTest.php index dab0f90..52acddd 100644 --- a/libs/reader/tests/StaticMethodReaderTest.php +++ b/libs/reader/tests/StaticMethodReaderTest.php @@ -6,8 +6,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\FunctionReaderInterface; use TypeLang\Reader\Tests\Stub\StaticMethodReaderStub; +use TypeLang\Reader\Tests\Stub\StaticMethodReaderStub82; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\UnionTypeNode; @@ -51,10 +53,11 @@ function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getIntersectionT } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(FunctionReaderInterface $reader): void { $type = $reader->findFunctionType( - function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getCompositeType'), + function: new \ReflectionMethod(StaticMethodReaderStub82::class, 'getCompositeType'), ); self::assertSameType(new UnionTypeNode([ diff --git a/libs/reader/tests/Stub/ConstantReaderStub.php b/libs/reader/tests/Stub/ConstantReaderStub.php index 258011b..87fa941 100644 --- a/libs/reader/tests/Stub/ConstantReaderStub.php +++ b/libs/reader/tests/Stub/ConstantReaderStub.php @@ -4,22 +4,7 @@ namespace TypeLang\Reader\Tests\Stub; -enum __ConstantReaderEnum: int -{ - case EXAMPLE = 0; -} - final class ConstantReaderStub { - public const int SINGLE = 0xDEAD_BEEF; - - public const int|string UNION = 0xDEAD_BEEF; - - public const __ConstantReaderEnum&\BackedEnum INTERSECTION = __ConstantReaderEnum::EXAMPLE; - - public const (__ConstantReaderEnum&\BackedEnum)|array COMPOSITE = __ConstantReaderEnum::EXAMPLE; - - public const ?int NULLABLE = null; - public const UNTYPED = 0xDEAD_BEEF; } diff --git a/libs/reader/tests/Stub/MethodReaderStub.php b/libs/reader/tests/Stub/MethodReaderStub.php index ae28e59..2f435b5 100644 --- a/libs/reader/tests/Stub/MethodReaderStub.php +++ b/libs/reader/tests/Stub/MethodReaderStub.php @@ -11,6 +11,4 @@ public function getSingleType(): int; public function getUnionType(): int|string; public function getIntersectionType(): \ArrayAccess&\Traversable; - - public function getCompositeType(): (\ArrayAccess&\Traversable)|array; } diff --git a/libs/reader/tests/Stub/ParameterReaderStub.php b/libs/reader/tests/Stub/ParameterReaderStub.php index 4666a4a..de54d0f 100644 --- a/libs/reader/tests/Stub/ParameterReaderStub.php +++ b/libs/reader/tests/Stub/ParameterReaderStub.php @@ -12,8 +12,6 @@ public static function withUnionType(int|string $param): void {} public static function withIntersectionType(\ArrayAccess&\Traversable $param): void {} - public static function withCompositeType((\ArrayAccess&\Traversable)|array $param): void {} - public static function withNullableType(?int $param): void {} public static function withoutType($param): void {} diff --git a/libs/reader/tests/Stub/PropertyReaderStub.php b/libs/reader/tests/Stub/PropertyReaderStub.php index 36d9c1a..cf5e9fa 100644 --- a/libs/reader/tests/Stub/PropertyReaderStub.php +++ b/libs/reader/tests/Stub/PropertyReaderStub.php @@ -12,18 +12,7 @@ final class PropertyReaderStub public \ArrayAccess&\Traversable $intersectionType; - public (\ArrayAccess&\Traversable)|array $compositeType; - public ?int $nullableType; public $untypedType; - - /** - * An asymmetric property hook: reads as "string", but accepts a wider - * "string|\Stringable" on write. - */ - public string $hookedType { - get => $this->hookedType; - set(string|\Stringable $value) => (string) $value; - } } diff --git a/libs/reader/tests/Stub/StaticMethodReaderStub.php b/libs/reader/tests/Stub/StaticMethodReaderStub.php index bd59b7c..67a5907 100644 --- a/libs/reader/tests/Stub/StaticMethodReaderStub.php +++ b/libs/reader/tests/Stub/StaticMethodReaderStub.php @@ -11,6 +11,4 @@ public static function getSingleType(): int; public static function getUnionType(): int|string; public static function getIntersectionType(): \ArrayAccess&\Traversable; - - public static function getCompositeType(): (\ArrayAccess&\Traversable)|array; } diff --git a/libs/reader/tests/Stub/functions_reader_stub.php b/libs/reader/tests/Stub/functions_reader_stub.php index d813b5b..2e6852d 100644 --- a/libs/reader/tests/Stub/functions_reader_stub.php +++ b/libs/reader/tests/Stub/functions_reader_stub.php @@ -10,8 +10,6 @@ function get_union_type(): int|string {} function get_intersection_type(): \ArrayAccess&\Traversable {} -function get_composite_type(): (\ArrayAccess&\Traversable)|array {} - function get_nullable_type(): ?int {} function get_void_type(): void {} From c27c9dd81990b9ed75bdfa139b11149b8d100f5b Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 06:43:41 +0300 Subject: [PATCH 099/119] Add missing reader stubs --- .../reader/tests/Stub/ConstantReaderStub83.php | 18 ++++++++++++++++++ libs/reader/tests/Stub/MethodReaderStub82.php | 10 ++++++++++ .../tests/Stub/ParameterReaderStub82.php | 10 ++++++++++ .../reader/tests/Stub/PropertyReaderStub82.php | 10 ++++++++++ .../reader/tests/Stub/PropertyReaderStub84.php | 17 +++++++++++++++++ .../tests/Stub/StaticMethodReaderStub82.php | 10 ++++++++++ .../reader/tests/Stub/__ConstantReaderEnum.php | 10 ++++++++++ .../tests/Stub/functions_reader_stub_82.php | 7 +++++++ 8 files changed, 92 insertions(+) create mode 100644 libs/reader/tests/Stub/ConstantReaderStub83.php create mode 100644 libs/reader/tests/Stub/MethodReaderStub82.php create mode 100644 libs/reader/tests/Stub/ParameterReaderStub82.php create mode 100644 libs/reader/tests/Stub/PropertyReaderStub82.php create mode 100644 libs/reader/tests/Stub/PropertyReaderStub84.php create mode 100644 libs/reader/tests/Stub/StaticMethodReaderStub82.php create mode 100644 libs/reader/tests/Stub/__ConstantReaderEnum.php create mode 100644 libs/reader/tests/Stub/functions_reader_stub_82.php diff --git a/libs/reader/tests/Stub/ConstantReaderStub83.php b/libs/reader/tests/Stub/ConstantReaderStub83.php new file mode 100644 index 0000000..d1867ae --- /dev/null +++ b/libs/reader/tests/Stub/ConstantReaderStub83.php @@ -0,0 +1,18 @@ + $this->hookedType; + set(string|\Stringable $value) => (string) $value; + } +} diff --git a/libs/reader/tests/Stub/StaticMethodReaderStub82.php b/libs/reader/tests/Stub/StaticMethodReaderStub82.php new file mode 100644 index 0000000..4421978 --- /dev/null +++ b/libs/reader/tests/Stub/StaticMethodReaderStub82.php @@ -0,0 +1,10 @@ + Date: Tue, 8 Sep 2026 06:43:53 +0300 Subject: [PATCH 100/119] Add missing parser exceptions --- .../ParserRuntimeExceptionInterface.php | 15 +++ .../parser/src/Exception/ParsingException.php | 94 +++++++++++++++++++ .../Exception/UnreadableSourceException.php | 21 +++++ 3 files changed, 130 insertions(+) create mode 100644 libs/parser/src/Exception/ParserRuntimeExceptionInterface.php create mode 100644 libs/parser/src/Exception/ParsingException.php create mode 100644 libs/parser/src/Exception/UnreadableSourceException.php diff --git a/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php b/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php new file mode 100644 index 0000000..d3f4706 --- /dev/null +++ b/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php @@ -0,0 +1,15 @@ +getMessage(); + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" + * + * (string) $e; + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" on line 1 at column 7 + * ``` + */ +abstract class ParsingException extends ParserException implements ParserRuntimeExceptionInterface +{ + public PositionInterface $position; + + public function __construct( + string $message, + ReadableInterface $source, + public readonly TokenInterface $token, + int $code = 0, + ?\Throwable $previous = null, + ) { + $this->position = $this->createPosition($source, $token); + + if ($source instanceof FileInterface) { + $pathname = \realpath($source->pathname); + + if ($pathname === false) { + $pathname = \str_replace('\\', '/', $source->pathname); + } + + $this->file = $pathname; + $this->line = $this->position->line; + } + + parent::__construct($message, $source, $code, $previous); + } + + private function createPosition(ReadableInterface $source, TokenInterface $token): PositionInterface + { + try { + return (new PositionFactory()) + ->createFromOffset($source, $token->offset); + } catch (\Throwable) { + return new Position(); + } + } + + /** + * @param int<0, max> $offset + */ + protected static function createToken(ReadableInterface $source, int $offset, int $length = 0): TokenInterface + { + return new Token( + id: 0, + name: null, + channel: Channel::Default, + value: \substr($source->content, $offset, $length), + offset: $offset, + ); + } + + private function printPosition(): string + { + return \sprintf('on line %d at column %d', $this->position->line, $this->position->column); + } + + public function __toString(): string + { + $message = $this->message; + $this->message = \sprintf('%s %s', $message, $this->printPosition()); + + try { + return parent::__toString(); + } finally { + $this->message = $message; + } + } +} diff --git a/libs/parser/src/Exception/UnreadableSourceException.php b/libs/parser/src/Exception/UnreadableSourceException.php new file mode 100644 index 0000000..b646707 --- /dev/null +++ b/libs/parser/src/Exception/UnreadableSourceException.php @@ -0,0 +1,21 @@ +getMessage(), $source, 0, $e); + } +} From 936621302de5af1bfac24c4b9f4cae451c892cb1 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Tue, 8 Sep 2026 06:44:10 +0300 Subject: [PATCH 101/119] Fix php-cs-fixer config --- .php-cs-fixer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 69aa1a9..5c5d69d 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -24,7 +24,7 @@ strict: true, )); -return new Config() +return (new Config()) ->setUnsupportedPhpVersionAllowed(true) ->setParallelConfig(ParallelConfigFactory::detect()) ->setRules([ From d6ee2242b66e90355e5b8a1a6526307011e5ae3d Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 01:58:14 +0300 Subject: [PATCH 102/119] Upgrade parser runtime to v4.1 --- composer.json | 2 +- libs/parser/composer.json | 6 +- libs/parser/src/Internal/CompiledExecutor.php | 3871 +++++++++-------- libs/parser/src/Internal/Executor.php | 9 + libs/parser/src/TypeParser.php | 15 +- 5 files changed, 2016 insertions(+), 1887 deletions(-) diff --git a/composer.json b/composer.json index 3b74f8f..773d18a 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "jetbrains/phpstorm-attributes": "^1.2", "phpbench/phpbench": "^1.2", "phpdocumentor/reflection-docblock": "^6.0", - "phplrt/compiler": "^4.0", + "phplrt/compiler": "^4.1", "phpstan/phpdoc-parser": "^2.3", "phpstan/phpstan": "^2.2", "phpstan/phpstan-deprecation-rules": "^2.0", diff --git a/libs/parser/composer.json b/libs/parser/composer.json index 6d77faf..2813faa 100644 --- a/libs/parser/composer.json +++ b/libs/parser/composer.json @@ -11,9 +11,7 @@ "require": { "php": "^8.1", "type-lang/types": "^2.0", - "phplrt/lexer": "^4.0", - "phplrt/parser": "^4.0", - "phplrt/source": "^4.0" + "phplrt/runtime": "^4.1" }, "autoload": { "psr-4": { @@ -22,7 +20,7 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", - "phplrt/compiler": "^4.0", + "phplrt/compiler": "^4.1", "phpstan/phpstan": "^2.2", "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, diff --git a/libs/parser/src/Internal/CompiledExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php index 0ef89d2..6426518 100644 --- a/libs/parser/src/Internal/CompiledExecutor.php +++ b/libs/parser/src/Internal/CompiledExecutor.php @@ -1,7 +1,7 @@ */ + private const PARSER_EXPECTATIONS = [ + 'T_WHITESPACE', + 'T_EQ', + 'T_NOT', + 'T_TRUE_LITERAL', + 'T_FALSE_LITERAL', + 'T_NULL_LITERAL', + 'T_NAME_WITH_SPACE', + 'T_NAME', + 'T_NS_DELIMITER', + 'T_OR', + 'T_COMMA', + 'T_DOUBLE_COLON', + 'T_COLON', + 'T_ANGLE_BRACKET_OPEN', + 'T_ANGLE_BRACKET_CLOSE', + 'T_QMARK', + 'T_BRACE_OPEN', + 'T_BRACE_CLOSE', + 'T_PARENTHESIS_OPEN', + 'T_PARENTHESIS_CLOSE', + 'T_SQUARE_BRACKET_OPEN', + 'T_SQUARE_BRACKET_CLOSE', + 'T_AMP', + 'T_ELLIPSIS', + 'T_ASSIGN', + 'T_ASTERISK', + 'T_THIS', + 'T_VARIABLE', + 'T_DQ_STRING_LITERAL', + 'T_SQ_STRING_LITERAL', + 'T_FLOAT_LITERAL', + 'T_BIN_INT_LITERAL', + 'T_OCT_INT_LITERAL', + 'T_HEX_INT_LITERAL', + 'T_DEC_INT_LITERAL', + 'T_COMMENT', + 'T_DOC_COMMENT', + '/[^\\s]++/', + ]; + + /** @var array */ + private const PARSER_MESSAGES = [ + 7 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', + 12 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', + 34 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "\\Some"', + 45 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', + 61 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', + 81 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', + 85 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', + 89 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', + 96 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', + 100 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', + 109 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', + 124 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', + 133 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', + 138 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', + 142 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', + 146 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', + 152 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', + 'Syntax error, unexpected {token}, a condition must be parted with a colon ":"', + 'Syntax error, unexpected {token}, a condition must carry the type it is false of', + ]; + + /** @var array */ + private const PARSER_KEPT_TABLE = [ + true, + true, + true, + true, + false, + true, + false, + false, + true, + false, + false, + false, + false, + true, + true, + false, + false, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + false, + true, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + true, + ]; + + /** @var array|null> */ + private const PARSER_START_PREDICTION_TABLE = [ + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 15 => true, + ], + [ + 15 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 18 => true, + ], + [ + 18 => true, + ], + [ + 19 => true, + ], + [ + 26 => true, + ], + [ + 3 => true, + true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 3 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 28 => true, + true, + ], + [ + 28 => true, + ], + [ + 29 => true, + ], + [ + 30 => true, + ], + [ + 31 => true, + true, + true, + true, + ], + [ + 31 => true, + ], + [ + 32 => true, + ], + [ + 33 => true, + ], + [ + 34 => true, + ], + [ + 3 => true, + ], + [ + 4 => true, + ], + [ + 5 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 25 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 25 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 8 => true, + ], + [ + 8 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 7 => true, + ], + [ + 6 => true, + ], + [ + 1 => true, + ], + [ + 2 => true, + ], + [ + 3 => true, + ], + [ + 4 => true, + ], + [ + 5 => true, + ], + null, + [ + 8 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 8 => true, + 25 => true, + ], + [ + 25 => true, + ], + [ + 25 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 8 => true, + ], + [ + 25 => true, + ], + null, + null, + [ + 11 => true, + 13 => true, + 16 => true, + 18 => true, + ], + [ + 11 => true, + ], + [ + 11 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + 25 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 18 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 22 => true, + ], + null, + [ + 23 => true, + ], + null, + [ + 26 => true, + true, + ], + [ + 27 => true, + ], + [ + 26 => true, + ], + null, + [ + 24 => true, + ], + null, + [ + 10 => true, + ], + [ + 10 => true, + ], + null, + [ + 19 => true, + ], + null, + [ + 12 => true, + ], + [ + 12 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 13 => true, + ], + [ + 13 => true, + ], + [ + 13 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 6 => true, + ], + [ + 6 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 10 => true, + ], + [ + 14 => true, + ], + null, + [ + 13 => true, + ], + [ + 13 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + 24 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 24 => true, + ], + [ + 24 => true, + ], + null, + [ + 10 => true, + ], + [ + 14 => true, + ], + [ + 16 => true, + ], + [ + 16 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 12 => true, + 15 => true, + ], + null, + null, + [ + 15 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 10 => true, + ], + null, + [ + 10 => true, + ], + [ + 23 => true, + ], + null, + null, + [ + 17 => true, + ], + null, + [ + 20 => true, + ], + [ + 20 => true, + ], + null, + [ + 21 => true, + ], + null, + [ + 22 => true, + ], + [ + 22 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 9 => true, + ], + [ + 9 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 1 => true, + ], + [ + 1 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 12 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 26 => true, + true, + ], + ]; + + /** @var array>> */ + private const PARSER_CHOICE_PREDICTION_TABLE = [ + [ + 1 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + 15 => [ + 1, + ], + 18 => [ + 1, + ], + 25 => [ + 1, + ], + 28 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + 27 => [ + 155, + ], + ], + 4 => [ + 15 => [ + 5, + ], + 1 => [ + 8, + ], + 2 => [ + 8, + ], + 3 => [ + 8, + ], + 4 => [ + 8, + ], + 5 => [ + 8, + ], + 6 => [ + 8, + ], + 7 => [ + 8, + ], + 8 => [ + 8, + ], + 18 => [ + 8, + ], + 25 => [ + 8, + ], + [ + 8, + ], + 28 => [ + 8, + ], + [ + 8, + ], + [ + 8, + ], + [ + 8, + ], + [ + 8, + ], + [ + 8, + ], + [ + 8, + ], + ], + 9 => [ + 18 => [ + 10, + ], + 26 => [ + -14, + ], + 3 => [ + 14, + 28, + ], + 4 => [ + 14, + 28, + ], + 5 => [ + 14, + 28, + ], + 28 => [ + 14, + ], + [ + 14, + ], + [ + 14, + ], + [ + 14, + ], + [ + 14, + ], + [ + 14, + ], + [ + 14, + ], + 1 => [ + 28, + ], + 2 => [ + 28, + ], + 6 => [ + 28, + ], + 7 => [ + 28, + ], + 8 => [ + 28, + ], + 25 => [ + 28, + ], + ], + 14 => [ + 3 => [ + 15, + ], + [ + 15, + ], + 28 => [ + 15, + ], + [ + 15, + ], + [ + 15, + ], + [ + 15, + ], + [ + 15, + ], + [ + 15, + ], + [ + 15, + ], + 5 => [ + -28, + ], + ], + [ + 28 => [ + 16, + ], + [ + 16, + ], + [ + -20, + ], + [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + 3 => [ + -26, + ], + 4 => [ + -27, + ], + ], + [ + 28 => [ + -18, + ], + [ + -19, + ], + ], + 20 => [ + 31 => [ + -22, + ], + [ + -23, + ], + [ + -24, + ], + [ + -25, + ], + ], + 29 => [ + 1 => [ + 30, + ], + [ + 30, + ], + [ + 30, + ], + [ + 30, + ], + [ + 30, + ], + [ + 30, + ], + [ + 30, + ], + [ + 30, + ], + 25 => [ + 55, + ], + ], + 31 => [ + 8 => [ + 32, + ], + 1 => [ + 46, + ], + 2 => [ + 46, + ], + 3 => [ + 46, + ], + 4 => [ + 46, + ], + 5 => [ + 46, + ], + 6 => [ + 46, + ], + 7 => [ + 46, + ], + ], + 35 => [ + 7 => [ + -37, + ], + 6 => [ + -38, + ], + 1 => [ + -39, + ], + 2 => [ + -40, + ], + 3 => [ + -41, + ], + 4 => [ + -42, + ], + 5 => [ + -43, + ], + ], + 48 => [ + 25 => [ + 49, + ], + 8 => [ + 54, + ], + ], + 58 => [ + 11 => [ + 59, + ], + 18 => [ + 63, + ], + 13 => [ + 86, + 98, + ], + 16 => [ + 110, + ], + ], + 61 => [ + 1 => [ + 62, + ], + [ + 62, + ], + [ + 62, + ], + [ + 62, + ], + [ + 62, + ], + [ + 62, + ], + [ + 62, + ], + 25 => [ + 49, + ], + ], + 72 => [ + 27 => [ + -74, + ], + 26 => [ + -75, + ], + ], + 90 => [ + 1 => [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + 7 => [ + 93, + ], + [ + 93, + ], + 15 => [ + 93, + ], + 18 => [ + 93, + ], + 25 => [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + [ + 93, + ], + ], + 93 => [ + 25 => [ + -51, + 0, + ], + 1 => [ + 0, + ], + 2 => [ + 0, + ], + 3 => [ + 0, + ], + 4 => [ + 0, + ], + 5 => [ + 0, + ], + 6 => [ + 0, + ], + 7 => [ + 0, + ], + 8 => [ + 0, + ], + 15 => [ + 0, + ], + 18 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + ], + 103 => [ + 1 => [ + 104, + ], + [ + 104, + ], + [ + 104, + ], + [ + 104, + ], + [ + 104, + ], + [ + 104, + ], + [ + 104, + ], + 24 => [ + 105, + ], + ], + 115 => [ + 15 => [ + 125, + ], + 27 => [ + 125, + ], + ], + 117 => [ + 8 => [ + 9, + ], + 18 => [ + 9, + ], + 25 => [ + 9, + ], + [ + 9, + ], + 28 => [ + 9, + ], + [ + 9, + ], + [ + 9, + ], + [ + 9, + ], + [ + 9, + ], + [ + 9, + ], + [ + 9, + ], + ], + 151 => [ + 1 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + 15 => [ + 0, + ], + 18 => [ + 0, + ], + 25 => [ + 0, + ], + 28 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + ], + ]; + + /** @var array> */ + private const PARSER_SEQUENCE_PREDICTION_TABLE = [ + 1 => [ + 2, + -149, + ], + 28 => [ + 29, + -59, + ], + 30 => [ + 31, + -49, + ], + 49 => [ + 50, + 51, + -36, + ], + 55 => [ + 50, + 35, + -50, + ], + 62 => [ + 35, + -50, + ], + [ + 11, + 64, + 81, + -84, + ], + 65 => [ + 66, + 77, + -80, + ], + [ + 0, + -69, + -71, + -73, + -77, + ], + 87 => [ + 88, + 89, + 94, + -80, + 96, + ], + 99 => [ + 88, + 100, + 107, + -80, + 109, + ], + 110 => [ + 111, + 112, + -80, + 133, + ], + 113 => [ + 114, + -130, + ], + 116 => [ + 117, + -124, + 84, + 124, + ], + 120 => [ + -7, + 84, + ], + 130 => [ + 70, + -88, + ], + 135 => [ + 136, + -1, + 138, + ], + 149 => [ + 38, + -40, + ], + ]; + /** - * @var \Phplrt\Contracts\Parser\ParserInterface + * @var \Phplrt\Parser\Parser */ - protected readonly \Phplrt\Contracts\Parser\ParserInterface $parser; + protected readonly \Phplrt\Parser\Parser $parser; protected readonly \Phplrt\Contracts\Lexer\LexerInterface $lexer; @@ -364,1881 +2348,12 @@ public function __construct() 135 => self::reduceListOrOffsetSuffix(...), 149 => self::reduceTernaryExpressionOperator(...), ], - lookahead: [ - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 15 => true, - ], - [ - 15 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 18 => true, - ], - [ - 18 => true, - ], - [ - 19 => true, - ], - [ - 26 => true, - ], - [ - 3 => true, - true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 3 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 28 => true, - true, - ], - [ - 28 => true, - ], - [ - 29 => true, - ], - [ - 30 => true, - ], - [ - 31 => true, - true, - true, - true, - ], - [ - 31 => true, - ], - [ - 32 => true, - ], - [ - 33 => true, - ], - [ - 34 => true, - ], - [ - 3 => true, - ], - [ - 4 => true, - ], - [ - 5 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 25 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 25 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 8 => true, - ], - [ - 8 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 7 => true, - ], - [ - 6 => true, - ], - [ - 1 => true, - ], - [ - 2 => true, - ], - [ - 3 => true, - ], - [ - 4 => true, - ], - [ - 5 => true, - ], - null, - [ - 8 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 8 => true, - 25 => true, - ], - [ - 25 => true, - ], - [ - 25 => true, - ], - null, - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 8 => true, - ], - [ - 25 => true, - ], - null, - null, - [ - 11 => true, - 13 => true, - 16 => true, - 18 => true, - ], - [ - 11 => true, - ], - [ - 11 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - 25 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 18 => true, - ], - null, - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 22 => true, - ], - null, - [ - 23 => true, - ], - null, - [ - 26 => true, - true, - ], - [ - 27 => true, - ], - [ - 26 => true, - ], - null, - [ - 24 => true, - ], - null, - [ - 10 => true, - ], - [ - 10 => true, - ], - null, - [ - 19 => true, - ], - null, - [ - 12 => true, - ], - [ - 12 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 13 => true, - ], - [ - 13 => true, - ], - [ - 13 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 6 => true, - ], - [ - 6 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 10 => true, - ], - [ - 14 => true, - ], - null, - [ - 13 => true, - ], - [ - 13 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 1 => true, - true, - true, - true, - true, - true, - true, - 24 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 24 => true, - ], - [ - 24 => true, - ], - null, - [ - 10 => true, - ], - [ - 14 => true, - ], - [ - 16 => true, - ], - [ - 16 => true, - ], - null, - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 12 => true, - 15 => true, - ], - null, - null, - [ - 15 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 10 => true, - ], - null, - [ - 10 => true, - ], - [ - 23 => true, - ], - null, - null, - [ - 17 => true, - ], - null, - [ - 20 => true, - ], - [ - 20 => true, - ], - null, - [ - 21 => true, - ], - null, - [ - 22 => true, - ], - [ - 22 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 9 => true, - ], - [ - 9 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - null, - [ - 1 => true, - ], - [ - 1 => true, - ], - null, - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 12 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 26 => true, - true, - ], - ], - kept: [ - true, - true, - true, - true, - false, - true, - false, - false, - true, - false, - false, - false, - false, - true, - true, - false, - false, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - false, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - true, - true, - true, - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - true, - ], - choicePrediction: [ - [ - 1 => [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - 15 => [ - 1, - ], - 18 => [ - 1, - ], - 25 => [ - 1, - ], - 28 => [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - 27 => [ - 155, - ], - ], - 4 => [ - 15 => [ - 5, - ], - 1 => [ - 8, - ], - 2 => [ - 8, - ], - 3 => [ - 8, - ], - 4 => [ - 8, - ], - 5 => [ - 8, - ], - 6 => [ - 8, - ], - 7 => [ - 8, - ], - 8 => [ - 8, - ], - 18 => [ - 8, - ], - 25 => [ - 8, - ], - [ - 8, - ], - 28 => [ - 8, - ], - [ - 8, - ], - [ - 8, - ], - [ - 8, - ], - [ - 8, - ], - [ - 8, - ], - [ - 8, - ], - ], - 9 => [ - 18 => [ - 10, - ], - 26 => [ - 13, - ], - 3 => [ - 14, - 28, - ], - 4 => [ - 14, - 28, - ], - 5 => [ - 14, - 28, - ], - 28 => [ - 14, - ], - [ - 14, - ], - [ - 14, - ], - [ - 14, - ], - [ - 14, - ], - [ - 14, - ], - [ - 14, - ], - 1 => [ - 28, - ], - 2 => [ - 28, - ], - 6 => [ - 28, - ], - 7 => [ - 28, - ], - 8 => [ - 28, - ], - 25 => [ - 28, - ], - ], - 14 => [ - 3 => [ - 15, - ], - [ - 15, - ], - 28 => [ - 15, - ], - [ - 15, - ], - [ - 15, - ], - [ - 15, - ], - [ - 15, - ], - [ - 15, - ], - [ - 15, - ], - 5 => [ - 27, - ], - ], - [ - 28 => [ - 16, - ], - [ - 16, - ], - [ - 19, - ], - [ - 20, - ], - [ - 20, - ], - [ - 20, - ], - [ - 20, - ], - 3 => [ - 25, - ], - 4 => [ - 26, - ], - ], - [ - 28 => [ - 17, - ], - [ - 18, - ], - ], - 20 => [ - 31 => [ - 21, - ], - [ - 22, - ], - [ - 23, - ], - [ - 24, - ], - ], - 29 => [ - 1 => [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - 25 => [ - 55, - ], - ], - 31 => [ - 8 => [ - 32, - ], - 1 => [ - 46, - ], - 2 => [ - 46, - ], - 3 => [ - 46, - ], - 4 => [ - 46, - ], - 5 => [ - 46, - ], - 6 => [ - 46, - ], - 7 => [ - 46, - ], - ], - 35 => [ - 7 => [ - 36, - ], - 6 => [ - 37, - ], - 1 => [ - 38, - ], - 2 => [ - 39, - ], - 3 => [ - 40, - ], - 4 => [ - 41, - ], - 5 => [ - 42, - ], - ], - 48 => [ - 25 => [ - 49, - ], - 8 => [ - 54, - ], - ], - 58 => [ - 11 => [ - 59, - ], - 18 => [ - 63, - ], - 13 => [ - 86, - 98, - ], - 16 => [ - 110, - ], - ], - 61 => [ - 1 => [ - 62, - ], - [ - 62, - ], - [ - 62, - ], - [ - 62, - ], - [ - 62, - ], - [ - 62, - ], - [ - 62, - ], - 25 => [ - 49, - ], - ], - 72 => [ - 27 => [ - 73, - ], - 26 => [ - 74, - ], - ], - 90 => [ - 1 => [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - 7 => [ - 93, - ], - [ - 93, - ], - 15 => [ - 93, - ], - 18 => [ - 93, - ], - 25 => [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - [ - 93, - ], - ], - 93 => [ - 1 => [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - 15 => [ - 0, - ], - 18 => [ - 0, - ], - 26 => [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - ], - 103 => [ - 1 => [ - 104, - ], - [ - 104, - ], - [ - 104, - ], - [ - 104, - ], - [ - 104, - ], - [ - 104, - ], - [ - 104, - ], - 24 => [ - 105, - ], - ], - 115 => [ - 15 => [ - 125, - ], - 27 => [ - 125, - ], - ], - 117 => [ - 8 => [ - 9, - ], - 18 => [ - 9, - ], - 25 => [ - 9, - ], - [ - 9, - ], - 28 => [ - 9, - ], - [ - 9, - ], - [ - 9, - ], - [ - 9, - ], - [ - 9, - ], - [ - 9, - ], - [ - 9, - ], - ], - 151 => [ - 1 => [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - 15 => [ - 0, - ], - 18 => [ - 0, - ], - 25 => [ - 0, - ], - 28 => [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - [ - 0, - ], - ], - ], - expectations: [ - 'T_WHITESPACE', - 'T_EQ', - 'T_NOT', - 'T_TRUE_LITERAL', - 'T_FALSE_LITERAL', - 'T_NULL_LITERAL', - 'T_NAME_WITH_SPACE', - 'T_NAME', - 'T_NS_DELIMITER', - 'T_OR', - 'T_COMMA', - 'T_DOUBLE_COLON', - 'T_COLON', - 'T_ANGLE_BRACKET_OPEN', - 'T_ANGLE_BRACKET_CLOSE', - 'T_QMARK', - 'T_BRACE_OPEN', - 'T_BRACE_CLOSE', - 'T_PARENTHESIS_OPEN', - 'T_PARENTHESIS_CLOSE', - 'T_SQUARE_BRACKET_OPEN', - 'T_SQUARE_BRACKET_CLOSE', - 'T_AMP', - 'T_ELLIPSIS', - 'T_ASSIGN', - 'T_ASTERISK', - 'T_THIS', - 'T_VARIABLE', - 'T_DQ_STRING_LITERAL', - 'T_SQ_STRING_LITERAL', - 'T_FLOAT_LITERAL', - 'T_BIN_INT_LITERAL', - 'T_OCT_INT_LITERAL', - 'T_HEX_INT_LITERAL', - 'T_DEC_INT_LITERAL', - 'T_COMMENT', - 'T_DOC_COMMENT', - '/[^\\s]++/', - ], - messages: [ - 7 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', - 12 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', - 34 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "\\Some"', - 45 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', - 61 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', - 81 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', - 85 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', - 89 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', - 96 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', - 100 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', - 109 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', - 124 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', - 133 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', - 138 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', - 142 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', - 146 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', - 152 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', - 'Syntax error, unexpected {token}, a condition must be parted with a colon ":"', - 'Syntax error, unexpected {token}, a condition must carry the type it is false of', - ], + expectations: self::PARSER_EXPECTATIONS, + messages: self::PARSER_MESSAGES, + kept: self::PARSER_KEPT_TABLE, + startPrediction: self::PARSER_START_PREDICTION_TABLE, + choicePrediction: self::PARSER_CHOICE_PREDICTION_TABLE, + sequencePrediction: self::PARSER_SEQUENCE_PREDICTION_TABLE, ); } diff --git a/libs/parser/src/Internal/Executor.php b/libs/parser/src/Internal/Executor.php index 972f0a3..71fc2e0 100644 --- a/libs/parser/src/Internal/Executor.php +++ b/libs/parser/src/Internal/Executor.php @@ -5,6 +5,7 @@ namespace TypeLang\Parser\Internal; use Phplrt\Contracts\Lexer\Channel; +use Phplrt\Contracts\Lexer\TokenInterface; use Phplrt\Contracts\Position\PositionInterface; use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; use Phplrt\Contracts\Source\ReadableInterface; @@ -59,6 +60,14 @@ public function __construct( parent::__construct(); } + /** + * @return iterable + */ + public function lex(ReadableInterface $source): iterable + { + return $this->lexer->lex($source); + } + /** * Reads the source whole and returns the type it is written of. * diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php index c07138f..6a6cab3 100644 --- a/libs/parser/src/TypeParser.php +++ b/libs/parser/src/TypeParser.php @@ -45,32 +45,39 @@ public function withFeatures(bool ...$features): self ); } + public function lex(#[Language('PHP')] mixed $source): iterable + { + $executor = $this->getExecutor(); + + return $executor->lex($this->toSource($source)); + } + public function parse(#[Language('PHP')] mixed $source): TypeNode { $executor = $this->getExecutor(); - return $executor->parse($this->source($source)); + return $executor->parse($this->toSource($source)); } public function partial(#[Language('PHP')] mixed $source): ParsedResult { $executor = $this->getExecutor(); - return $executor->partial($this->source($source)); + return $executor->partial($this->toSource($source)); } public function validate(#[Language('PHP')] mixed $source): CheckResult { $executor = $this->getExecutor(); - return $executor->validate($this->source($source)); + return $executor->validate($this->toSource($source)); } /** * @throws SourceExceptionInterface in case of no source can be created out * of the given value */ - private function source(mixed $source): ReadableInterface + private function toSource(mixed $source): ReadableInterface { return $this->sources->create($source); } From 7308d719b9ce0006eb48c2c64aeea44e95e525d0 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 01:58:40 +0300 Subject: [PATCH 103/119] Actualize phpdoc benchmarks --- bench/PhpDoc/Tools/DocBlockParserBench.php | 473 +++++++++--------- bench/PhpDoc/Tools/PHPStanParserBench.php | 15 +- .../PhpDoc/Tools/PhpDocumentorParserBench.php | 13 +- bench/PhpDoc/Tools/TypeLangParserBench.php | 13 +- bench/var/.gitignore | 2 + 5 files changed, 264 insertions(+), 252 deletions(-) create mode 100644 bench/var/.gitignore diff --git a/bench/PhpDoc/Tools/DocBlockParserBench.php b/bench/PhpDoc/Tools/DocBlockParserBench.php index b249fa5..b8fe41b 100644 --- a/bench/PhpDoc/Tools/DocBlockParserBench.php +++ b/bench/PhpDoc/Tools/DocBlockParserBench.php @@ -4,287 +4,272 @@ namespace TypeLang\Bench\PhpDoc\Tools; +use FilesystemIterator; +use PhpToken; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\Process; + abstract class DocBlockParserBench { /** - * @var int<1, max> + * A sandbox directory containing the third-party packages that are used + * as a real-world DocBlock corpus. + * + * The whole directory is disposable: It is created, filled and installed + * by the benchmark itself and can be removed at any time. + * + * @var non-empty-string + */ + protected const CORPUS_DIRECTORY = __DIR__ . '/../../var/corpus'; + + /** + * The packages to extract the DocBlocks from. + * + * Each package is a separate benchmark set, so the list should contain + * recognizable packages of a different size and documentation style. + * + * @var non-empty-array */ - private const REPEATS = 16; + protected const CORPUS_PACKAGES = [ + 'doctrine/collections' => '*', + 'guzzlehttp/guzzle' => '*', + 'illuminate/support' => '*', + 'monolog/monolog' => '*', + 'nikic/php-parser' => '*', + 'phpdocumentor/reflection-docblock' => '*', + 'phpstan/phpdoc-parser' => '*', + 'psr/log' => '*', + 'symfony/console' => '*', + 'symfony/http-foundation' => '*', + 'twig/twig' => '*', + 'webmozart/assert' => '*', + ]; /** - * @return iterable + * Each set contains every DocBlock of a single package of the corpus. + * + * The corpus is installed by the provider itself, because it is executed + * in a separate process before any benchmark is launched. + * + * @return iterable}> */ public static function docBlocksDataProvider(): iterable { - yield 'empty' => ['docblock' => '/** */']; - - yield 'inline description' => ['docblock' => '/** An example of the inline description. */']; - - yield 'inline tag' => ['docblock' => '/** @return void */']; - - yield 'description' => ['docblock' => <<<'DOC' - /** - * The `@link` tag can be used to define a relation, or link, between - * the element, or part of the long description when used inline, to a URI. - */ - DOC]; - - yield 'description long' => ['docblock' => self::createDescription(self::REPEATS)]; - - yield 'scalar types' => ['docblock' => <<<'DOC' - /** - * @param int $offset - * @param string $name - * @param bool $strict - * @param float $ratio - * @param mixed $context - * @return void - */ - DOC]; - - yield 'union types' => ['docblock' => <<<'DOC' - /** - * @param int|string $key - * @param string|null $name - * @param ?iterable $items - * @param bool|int|float|string|null $scalar - * @return array|false - */ - DOC]; - - yield 'intersection types' => ['docblock' => <<<'DOC' - /** - * @param \Traversable&\Countable $items - * @param (\Stringable&\JsonSerializable)|null $value - * @param \IteratorAggregate&\ArrayAccess&\Countable $collection - * @return \Traversable&\Countable - */ - DOC]; - - yield 'named types' => ['docblock' => <<<'DOC' - /** - * @param \TypeLang\PhpDoc\DocBlockParserInterface $parser - * @param DocBlock\Tag\TagInterface $tag - * @param self $self - * @param static $static - * @return $this - */ - DOC]; - - yield 'generic types' => ['docblock' => <<<'DOC' - /** - * @param array $list - * @param iterable $items - * @param \Traversable>> $nested - * @param class-string<\Throwable> $exception - * @return \Generator> - */ - DOC]; - - yield 'shape types' => ['docblock' => <<<'DOC' - /** - * @param array{int, string, bool} $tuple - * @param array{name: string, age?: int<0, max>} $struct - * @param array{ - * id: positive-int, - * meta: array{ - * created: \DateTimeInterface, - * tags: list, - * ... - * } - * } $nested - * @param object{name: string, value?: mixed} $object - * @return array{} - */ - DOC]; - - yield 'callable types' => ['docblock' => <<<'DOC' - /** - * @param callable $any - * @param callable(): void $empty - * @param callable(int, string): bool $simple - * @param \Closure(non-empty-string, mixed...): (int|string) $variadic - * @param callable(callable(int): string): callable(string): int $nested - * @return \Closure(): \Generator - */ - DOC]; - - yield 'literal types' => ['docblock' => <<<'DOC' - /** - * @param 'read'|'write'|'append' $mode - * @param 1|2|3|42 $version - * @param 0.1|-0.5 $ratio - * @param true|false $flag - * @param "double \"quoted\" literal" $quoted - * @return 'ok' - */ - DOC]; - - yield 'const types' => ['docblock' => <<<'DOC' - /** - * @param \PDO::FETCH_* $mode - * @param self::STATUS_ACTIVE|self::STATUS_INACTIVE $status - * @param \PHP_INT_MAX $max - * @param int<0, \PHP_INT_MAX> $range - * @return static::DEFAULT_VALUE - */ - DOC]; - - yield 'conditional types' => ['docblock' => <<<'DOC' - /** - * @template T - * @param T $value - * @return (T is int ? string : (T is string ? int : bool)) - */ - DOC]; - - yield 'template tags' => ['docblock' => <<<'DOC' - /** - * @template TKey of array-key - * @template TValue of object - * @template-covariant TResult - * @extends \IteratorAggregate - * @implements \ArrayAccess - * @param TKey $key - * @param TValue $value - * @return static - */ - DOC]; - - yield 'inline tags' => ['docblock' => <<<'DOC' - /** - * {@inheritDoc} - * - * See the {@see \TypeLang\PhpDoc\DocBlockParser::parse()} method and - * the {@link https://www.ietf.org/rfc/rfc2396.txt RFC2396} document. - * - * @return void - */ - DOC]; - - yield 'metadata tags' => ['docblock' => <<<'DOC' - /** - * @author Nesmeyanov Kirill - * @copyright 2024 TypeLang - * @license MIT - * @since 1.0 - * @deprecated since 2.0, use something else instead - * @internal - * @api - * @final - * @todo Remove this method - */ - DOC]; - - yield 'unknown tags' => ['docblock' => <<<'DOC' - /** - * @some-vendor-tag with an arbitrary payload - * @another_tag(with, parens) - * @x-custom {"json": "like", "payload": [1, 2, 3]} - * @return void - */ - DOC]; - - yield 'real world' => ['docblock' => <<<'DOC' - /** - * The `@link` tag can be used to define a relation, or link, between - * the element, or part of the long description when used inline, to a URI. - * - * ``` - * "@link" [ | ] [] - * ``` - * - * @link https://www.ietf.org/rfc/rfc2396.txt RFC2396 - * @return iterable }> - * }> - */ - DOC]; - - yield 'many params' => ['docblock' => self::createTags(self::REPEATS, [ - '@param int<0, max> $offset%d', - '@param non-empty-string $name%d', - '@param list $items%d', - ])]; - - yield 'many throws' => ['docblock' => self::createTags(self::REPEATS, [ - '@throws \RuntimeException in case of a %d error', - '@throws \InvalidArgumentException in case of an invalid #%d argument', - ])]; - - yield 'many mixed tags' => ['docblock' => self::createTags(self::REPEATS, [ - '@param \Closure(int, string...): (bool|null) $callback%d', - '@param array{a: int, b: array{c: string, d?: list}} $shape%d', - '@var iterable> $iterable%d', - '@throws \LogicException on the %d failure', - '@see \TypeLang\PhpDoc\DocBlockParser::parse() the #%d reference', - '@deprecated since %d.0', - ])]; - - yield 'huge' => ['docblock' => self::createHuge()]; + $metadata = self::getCorpusMetadata(); + + foreach (\array_keys(self::CORPUS_PACKAGES) as $package) { + $directory = \realpath($metadata['versions'][$package]['install_path'] ?? ''); + + if ($directory === false) { + throw new \RuntimeException(\sprintf( + 'The "%s" package of the benchmark corpus is not installed', + $package, + )); + } + + $docblocks = self::extractDocBlocks($directory); + + if ($docblocks === []) { + continue; + } + + yield $package => ['docblocks' => $docblocks]; + } + } + + /** + * Installs the corpus (if required) and returns the Composer metadata + * of the installed packages. + * + * @return array{versions: array} + */ + private static function getCorpusMetadata(): array + { + $installed = self::CORPUS_DIRECTORY . '/vendor/composer/installed.php'; + + // The manifest is generated from the {@see CORPUS_PACKAGES} list, so + // that an edit of the list is enough to update the corpus. + $outdated = self::updateCorpusManifest(); + + if ($outdated || !\is_file(self::CORPUS_DIRECTORY . '/composer.lock')) { + self::composer('update'); + } elseif (!\is_file($installed)) { + self::composer('install'); + } + + /** @var array{versions: array} */ + return require $installed; } /** - * @param int<1, max> $repeats - * @return non-empty-string + * Writes the "composer.json" of the corpus and returns {@see true} in + * case of the manifest has been changed (and therefore the installed + * packages are outdated). */ - private static function createDescription(int $repeats): string + private static function updateCorpusManifest(): bool { - $lines = ['/**']; + $pathname = self::CORPUS_DIRECTORY . '/composer.json'; + + $expected = \json_encode([ + 'name' => 'type-lang/bench-corpus', + 'description' => 'A disposable sandbox of the third-party packages that are ' + . 'used as a real-world DocBlock corpus. The directory is generated by ' + . 'the benchmarks and can be safely removed.', + 'type' => 'project', + 'license' => 'MIT', + 'require' => self::CORPUS_PACKAGES, + 'config' => [ + 'preferred-install' => ['*' => 'dist'], + 'allow-plugins' => false, + ], + 'minimum-stability' => 'stable', + 'prefer-stable' => true, + ], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR); + + if (@\file_get_contents($pathname) === $expected) { + return false; + } - for ($i = 0; $i < $repeats; ++$i) { - $lines[] = ' * Lorem ipsum dolor sit amet, consectetur adipiscing elit,'; - $lines[] = ' * sed do eiusmod tempor incididunt ut labore et dolore magna.'; - $lines[] = ' *'; + if (!\is_dir(self::CORPUS_DIRECTORY) && !@\mkdir(self::CORPUS_DIRECTORY, recursive: true)) { + throw new \RuntimeException(\sprintf( + 'Could not create the benchmark corpus directory "%s"', + self::CORPUS_DIRECTORY, + )); } - $lines[] = ' */'; + if (\file_put_contents($pathname, $expected) === false) { + throw new \RuntimeException(\sprintf( + 'Could not write the benchmark corpus manifest "%s"', + $pathname, + )); + } - return \implode("\n", $lines); + return true; } /** - * @param int<1, max> $repeats - * @param non-empty-list $templates sprintf templates of the tag lines - * @return non-empty-string + * @param non-empty-string $command */ - private static function createTags(int $repeats, array $templates): string + private static function composer(string $command): void { - $lines = ['/**']; + $process = new Process( + command: [ + ...self::getComposerBinary(), + $command, + '--working-dir=' . self::CORPUS_DIRECTORY, + '--no-interaction', + '--no-progress', + '--no-audit', + '--no-plugins', + '--no-scripts', + '--prefer-dist', + ], + // The installation of the corpus may take a few minutes on the + // first run, so it should not be interrupted by a timeout. + timeout: null, + ); + + if ($process->run() !== 0) { + throw new \RuntimeException(\sprintf( + "Could not install the benchmark corpus:\n%s", + \trim($process->getErrorOutput() . "\n" . $process->getOutput()), + )); + } + } - for ($i = 0; $i < $repeats; ++$i) { - foreach ($templates as $template) { - $lines[] = ' * ' . \sprintf($template, $i); + /** + * @return non-empty-list + */ + private static function getComposerBinary(): array + { + $finder = new ExecutableFinder(); + + foreach (['composer', 'composer.phar'] as $name) { + $pathname = $finder->find($name); + + if ($pathname === null || $pathname === '') { + continue; } - } - $lines[] = ' */'; + // A phar is not executable on all platforms, so it is passed + // to the PHP binary that runs the benchmarks. + return \str_ends_with($pathname, '.phar') + ? [\PHP_BINARY, $pathname] + : [$pathname]; + } - return \implode("\n", $lines); + throw new \RuntimeException(\sprintf( + 'The "composer" executable is required to install the benchmark corpus, ' + . 'but it could not be found. Please install the corpus manually ' + . 'using the "composer install --working-dir=%s" command', + self::CORPUS_DIRECTORY, + )); } /** - * @return non-empty-string + * Extracts every DocBlock of every PHP file of the given directory. + * + * The PHP tokenizer is used instead of a full-blown parser (like the + * {@link https://github.com/nikic/PHP-Parser} package) because the + * benchmark requires the DocBlock texts only and not the elements + * they are attached to. + * + * A naive regular expression is not an option either: It also matches + * the DocBlocks that occur inside string literals and heredocs, which + * the test suites and fixtures of the corpus packages are full of. + * + * @param non-empty-string $directory + * @return list */ - private static function createHuge(): string + private static function extractDocBlocks(string $directory): array { - // Removes the trailing " */" line terminator of the description block. - $description = \substr(self::createDescription(self::REPEATS), 0, -3); + $result = []; + + foreach (self::getSourceFiles($directory) as $pathname) { + $source = @\file_get_contents($pathname); - // Removes the leading "/**" line opener of the tags block. - $tags = \substr(self::createTags(self::REPEATS, [ - '@param \Closure(int, string...): (bool|null) $callback%d', - '@param array{a: int, b: array{c: string, d?: list}} $shape%d', - '@throws \LogicException on the %d failure', - ]), 4); + if ($source === false || $source === '') { + continue; + } + + foreach (PhpToken::tokenize($source) as $token) { + if ($token->is(\T_DOC_COMMENT) && $token->text !== '') { + $result[] = $token->text; + } + } + } + + return $result; + } - return $description . $tags; + /** + * @param non-empty-string $directory + * @return iterable + */ + private static function getSourceFiles(string $directory): iterable + { + $files = new RecursiveIteratorIterator( + iterator: new RecursiveDirectoryIterator( + directory: $directory, + flags: FilesystemIterator::SKIP_DOTS + | FilesystemIterator::CURRENT_AS_FILEINFO, + ), + ); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + $pathname = $file->getPathname(); + + if ($file->isFile() && $file->getExtension() === 'php' && $pathname !== '') { + yield $pathname; + } + } } /** - * @param array{docblock: non-empty-string} $params + * @param array{docblocks: list} $params */ abstract public function benchParseDocBlock(array $params): void; } diff --git a/bench/PhpDoc/Tools/PHPStanParserBench.php b/bench/PhpDoc/Tools/PHPStanParserBench.php index 7494d0c..4152f28 100644 --- a/bench/PhpDoc/Tools/PHPStanParserBench.php +++ b/bench/PhpDoc/Tools/PHPStanParserBench.php @@ -18,7 +18,8 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\ParserConfig; -#[Groups(['phpstan']), Revs(500), Warmup(50), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] +#[Groups(['phpstan']), Revs(1), Warmup(1), Iterations(10)] +#[BeforeMethods('prepare'), RetryThreshold(5)] final class PHPStanParserBench extends DocBlockParserBench { private Lexer $lexer; @@ -42,8 +43,14 @@ public function prepare(): void #[ParamProviders('docBlocksDataProvider')] public function benchParseDocBlock(array $params): void { - $iterator = new TokenIterator($this->lexer->tokenize($params['docblock'])); - - $this->parser->parse($iterator); + foreach ($params['docblocks'] as $docblock) { + try { + $this->parser->parse(new TokenIterator($this->lexer->tokenize($docblock))); + } catch (\Throwable) { + // A real-world corpus contains DocBlocks that some of the tools + // are not able to parse. They are skipped so that every tool + // is measured on the same corpus. + } + } } } diff --git a/bench/PhpDoc/Tools/PhpDocumentorParserBench.php b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php index 8a9fe5a..4a39799 100644 --- a/bench/PhpDoc/Tools/PhpDocumentorParserBench.php +++ b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php @@ -14,7 +14,8 @@ use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; -#[Groups(['phpdocumentor']), Revs(500), Warmup(50), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] +#[Groups(['phpdocumentor']), Revs(1), Warmup(1), Iterations(10)] +#[BeforeMethods('prepare'), RetryThreshold(5)] final class PhpDocumentorParserBench extends DocBlockParserBench { private DocBlockFactoryInterface $parser; @@ -27,6 +28,14 @@ public function prepare(): void #[ParamProviders('docBlocksDataProvider')] public function benchParseDocBlock(array $params): void { - $this->parser->create($params['docblock']); + foreach ($params['docblocks'] as $docblock) { + try { + $this->parser->create($docblock); + } catch (\Throwable) { + // A real-world corpus contains DocBlocks that some of the tools + // are not able to parse. They are skipped so that every tool + // is measured on the same corpus. + } + } } } diff --git a/bench/PhpDoc/Tools/TypeLangParserBench.php b/bench/PhpDoc/Tools/TypeLangParserBench.php index 97069cc..4d0bdc7 100644 --- a/bench/PhpDoc/Tools/TypeLangParserBench.php +++ b/bench/PhpDoc/Tools/TypeLangParserBench.php @@ -14,7 +14,8 @@ use TypeLang\PhpDoc\DocBlockParser; use TypeLang\PhpDoc\DocBlockParserInterface; -#[Groups(['typelang', 'baseline']), Revs(500), Warmup(50), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] +#[Groups(['typelang', 'baseline']), Revs(1), Warmup(1), Iterations(10)] +#[BeforeMethods('prepare'), RetryThreshold(5)] final class TypeLangParserBench extends DocBlockParserBench { private DocBlockParserInterface $parser; @@ -27,6 +28,14 @@ public function prepare(): void #[ParamProviders('docBlocksDataProvider')] public function benchParseDocBlock(array $params): void { - $this->parser->parse($params['docblock']); + foreach ($params['docblocks'] as $docblock) { + try { + $this->parser->parse($docblock); + } catch (\Throwable) { + // A real-world corpus contains DocBlocks that some of the tools + // are not able to parse. They are skipped so that every tool + // is measured on the same corpus. + } + } } } diff --git a/bench/var/.gitignore b/bench/var/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bench/var/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From 04df19f6d8908cac12e2d9e6f2acd7a5855af3b2 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 02:44:22 +0300 Subject: [PATCH 104/119] Optimize grammar rules (union/intersection, variable, name and some other rules) --- libs/parser/resources/grammar.pp3 | 128 +- libs/parser/resources/grammar/common.pp3 | 25 +- libs/parser/resources/grammar/named-type.pp3 | 71 +- libs/parser/src/Internal/CompiledExecutor.php | 1285 +++++++---------- 4 files changed, 658 insertions(+), 851 deletions(-) diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3 index 8d8c43b..b28c2fc 100644 --- a/libs/parser/resources/grammar.pp3 +++ b/libs/parser/resources/grammar.pp3 @@ -25,28 +25,91 @@ * ----------------------------------------------------------------------------- */ +// The logical operators and the condition are read by this very rule rather +// than by a level of their own: every type the reading goes through is read +// through each of those levels, while an operator stands in only a few of +// them. Type -> { if (\count($children) === 1) { return $children[0]; } + // The suffix of a condition is the only thing the rule reads that carries + // a flag of its own, which is what the operands are told from + $condition = null; + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + if (\is_bool($children[$i])) { + $condition = \array_splice($children, $i); + + break; + } + } + + // The operands joined by an ampersand bind tighter, so they are collected + // into a group of their own, and the groups are what a union is made of + $groups = []; + $group = [$children[0]]; + $isUnion = false; + $isIntersection = false; + + for ($i = 1, $count = \count($children); $i < $count; $i += 2) { + if ($children[$i]->id === self::T_OR) { + $isUnion = true; + $groups[] = $group; + $group = [$children[$i + 1]]; + + continue; + } + + $isIntersection = true; + $group[] = $children[$i + 1]; + } + + $groups[] = $group; + + // The operand that binds tighter is the one reported first, the way the + // reading would have reached it + if ($isIntersection && $this->features->intersections === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); + } + + if ($isUnion && $this->features->unions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset); + } + + $statements = []; + + foreach ($groups as $group) { + $statements[] = \count($group) === 1 + ? $group[0] + : new Type\IntersectionTypeNode($group, $group[0]->offset); + } + + $statement = \count($statements) === 1 + ? $statements[0] + : new Type\UnionTypeNode($statements, $offset); + + if ($condition === null) { + return $statement; + } + if ($this->features->conditions === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset); } - $condition = $children[1] - ? new Type\Condition\NotEqualConditionNode($children[0], $children[2], $offset) - : new Type\Condition\EqualConditionNode($children[0], $children[2], $offset); + $comparison = $condition[0] + ? new Type\Condition\NotEqualConditionNode($statement, $condition[1], $offset) + : new Type\Condition\EqualConditionNode($statement, $condition[1], $offset); return new Type\TernaryExpressionNode( - $condition, - $children[3], - $children[4], + $comparison, + $condition[2], + $condition[3], $offset, ); } - : LogicalType() TernaryExpressionSuffix()? - | Variable() TernaryExpressionSuffix() + : UnaryType() LogicalTail()* TernaryExpressionSuffix()? ; TernaryExpressionSuffix @@ -69,50 +132,9 @@ TernaryExpressionOperator -> { * ----------------------------------------------------------------------------- */ -LogicalType - : UnionType() - ; - -UnionType -> { - if (\count($children) === 1) { - return $children[0]; - } - - if ($this->features->unions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset); - } - - $statement = new Type\UnionTypeNode($children); - $statement->offset = $offset; - - return $statement; -} - : IntersectionType() UnionTail()* - ; - -UnionTail - : ::T_OR:: IntersectionType() @error("Syntax error, unexpected {token}, a union type must carry a type after the vertical bar \"|\", for example \"T|U\"") - ; - -IntersectionType -> { - if (\count($children) === 1) { - return $children[0]; - } - - if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); - } - - $statement = new Type\IntersectionTypeNode($children); - $statement->offset = $offset; - - return $statement; -} - : UnaryType() IntersectionTail()* - ; - -IntersectionTail - : ::T_AMP:: UnaryType() @error("Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand \"&\", for example \"T&U\"") +LogicalTail + : UnaryType() @error("Syntax error, unexpected {token}, a union type must carry a type after the vertical bar \"|\", for example \"T|U\"") + | UnaryType() @error("Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand \"&\", for example \"T&U\"") ; /** @@ -182,4 +204,6 @@ PrimaryType | ThisType() | LiteralType() | NamedType() + | LeadingMaskType() + | Variable() &::T_EQ:: ; diff --git a/libs/parser/resources/grammar/common.pp3 b/libs/parser/resources/grammar/common.pp3 index 6cfc663..657138a 100644 --- a/libs/parser/resources/grammar/common.pp3 +++ b/libs/parser/resources/grammar/common.pp3 @@ -1,23 +1,18 @@ // "Some\Any" or "\Some\Any" -Name - : FullQualifiedName() - | RelativeName() - ; +Name -> { + // "Some\Any" starts at a segment of its own + if ($children[0] instanceof Type\Identifier) { + return new Type\Name($children, false, $offset); + } -// "\Some\Any" -FullQualifiedName -> { - return new Type\Name($children, true, $offset); -} - : ::T_NS_DELIMITER:: Identifier() @error("Syntax error, unexpected {token}, a name must carry a segment after the separator, for example \"\Some\"") - NameTail()* - ; + // "\Some\Any" carries the separator in front of its segments, and the + // name itself is the part that follows it + $segments = \array_slice($children, 1); -// "Some\Any" -RelativeName -> { - return new Type\Name($children, false, $offset); + return new Type\Name($segments, true, $segments[0]->offset); } - : Identifier() NameTail()* + : ? Identifier() NameTail()* ; NameTail diff --git a/libs/parser/resources/grammar/named-type.pp3 b/libs/parser/resources/grammar/named-type.pp3 index 4825450..a76e73e 100644 --- a/libs/parser/resources/grammar/named-type.pp3 +++ b/libs/parser/resources/grammar/named-type.pp3 @@ -5,16 +5,36 @@ * ----------------------------------------------------------------------------- */ +// A name is read by this very rule rather than by one of its own: a type is +// named far more often than anything else is read, so every rule the reading +// goes through on its way here is paid for by the whole grammar. NamedType -> { $head = $children[0]; $suffix = $children[1] ?? null; - if ($head instanceof Type\ConstMaskNode) { - if ($suffix !== null) { - throw Exception\ConstMaskException::becauseNothingFollowsAMask($source, $offset); + // "Some\Any\*" and "Some\Any\SOME_*" + // + // A mask closes the type it is written at the end of, so it is read as a + // suffix of a name and never carries a suffix of its own. + if ($suffix instanceof Type\MaskNode || $suffix instanceof Type\WildcardNode) { + $parts = $head->parts; + + if ($suffix instanceof Type\MaskNode) { + // "Some\Any\*" + $mask = $suffix; + } else { + // "Some\Any\SOME_*" + $segment = \array_pop($parts); + $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); } - return $head; + return new Type\ConstMaskNode( + $mask, + $parts === [] + ? $head->isFullyQualified + : new Type\Name($parts, $head->isFullyQualified, $head->offset), + $offset, + ); } switch (true) { @@ -59,7 +79,7 @@ NamedType -> { ); } } - : NameOrMask() NamedTypeSuffix()? + : Name() NamedTypeSuffix()? ; NamedTypeSuffix @@ -68,45 +88,14 @@ NamedTypeSuffix | TemplateArguments() !::T_PARENTHESIS_OPEN:: | TemplateParameters() CallableSuffix() | ShapeFields() + | MaskTail() + | NamespacedMask() ; -NameOrMask -> { - $head = $children[0]; - - // "*_SOME" - if (!$head instanceof Type\Name) { - return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); - } - - // "Some\Any" - if (!isset($children[1])) { - return $head; - } - - $parts = $head->parts; - - if ($children[1] instanceof Type\MaskNode) { - // "Some\Any\*" - $mask = $children[1]; - } else { - // "Some\Any\SOME_*" - $segment = \array_pop($parts); - $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); - } - - return new Type\ConstMaskNode( - $mask, - $parts === [] - ? $head->isFullyQualified - : new Type\Name($parts, $head->isFullyQualified, $head->offset), - $offset, - ); +// "*_SOME" — a mask that carries no name in front of it +LeadingMaskType -> { + return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); } - : Name() ( MaskTail() | NamespacedMask() )? - | LeadingMask() - ; - -LeadingMask : WildcardType() Identifier() MaskTail()? ; diff --git a/libs/parser/src/Internal/CompiledExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php index 6426518..2e95711 100644 --- a/libs/parser/src/Internal/CompiledExecutor.php +++ b/libs/parser/src/Internal/CompiledExecutor.php @@ -151,32 +151,28 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf /** @var array */ private const PARSER_MESSAGES = [ - 7 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', - 12 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', - 34 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "\\Some"', - 45 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', - 61 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', - 81 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', - 85 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', - 89 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', - 96 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', - 100 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', - 109 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', - 124 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', - 133 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', - 138 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', - 142 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', - 146 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', - 152 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', + 4 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', + 9 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', + 40 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', + 45 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', + 71 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', + 75 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', + 79 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', + 86 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', + 90 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', + 99 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', + 114 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', + 123 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', + 133 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', + 138 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', + 140 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', + 146 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', 'Syntax error, unexpected {token}, a condition must be parted with a colon ":"', 'Syntax error, unexpected {token}, a condition must carry the type it is false of', ]; /** @var array */ private const PARSER_KEPT_TABLE = [ - true, - true, - true, true, false, true, @@ -204,9 +200,6 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, true, - true, - false, - true, false, false, true, @@ -220,20 +213,16 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf false, false, false, - true, - false, false, false, - true, false, false, false, - true, - true, false, false, false, false, + true, false, false, false, @@ -308,12 +297,16 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf false, false, false, - false, + true, true, false, false, false, false, + true, + false, + false, + false, false, false, false, @@ -329,7 +322,6 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf false, false, false, - true, ]; /** @var array|null> */ @@ -369,73 +361,11 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, true, true, true, true, true, - 15 => true, - 18 => true, - 25 => true, - true, - 28 => true, - true, - true, - true, true, true, true, @@ -458,7 +388,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, + true, + true, true, true, true, @@ -478,7 +409,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, + true, + true, true, true, true, @@ -498,7 +430,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, + true, + true, true, true, true, @@ -590,7 +523,6 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, true, - 25 => true, ], [ 1 => true, @@ -601,31 +533,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, true, - 25 => true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 8 => true, ], + null, [ 8 => true, ], @@ -638,15 +547,6 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, ], - [ - 1 => true, - true, - true, - true, - true, - true, - true, - ], [ 7 => true, ], @@ -673,13 +573,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 8 => true, ], [ - 1 => true, - true, - true, - true, - true, - true, - true, + 8 => true, ], [ 1 => true, @@ -693,15 +587,18 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf null, [ 8 => true, + 11 => true, + 13 => true, + 16 => true, + 18 => true, 25 => true, ], [ - 25 => true, + 11 => true, ], [ - 25 => true, + 11 => true, ], - null, [ 1 => true, true, @@ -710,28 +607,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, true, - ], - null, - [ - 8 => true, - ], - [ 25 => true, ], - null, - null, - [ - 11 => true, - 13 => true, - 16 => true, - 18 => true, - ], - [ - 11 => true, - ], - [ - 11 => true, - ], [ 1 => true, true, @@ -740,8 +617,15 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, true, + ], + null, + [ + 25 => true, + ], + [ 25 => true, ], + null, [ 1 => true, true, @@ -751,6 +635,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, ], + null, [ 18 => true, ], @@ -1093,7 +978,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, + true, + true, true, true, true, @@ -1113,7 +999,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, + true, + true, true, true, true, @@ -1200,6 +1087,20 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf [ 17 => true, ], + [ + 8 => true, + ], + [ + 25 => true, + ], + [ + 26 => true, + true, + ], + null, + [ + 1 => true, + ], null, [ 20 => true, @@ -1213,10 +1114,14 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf ], null, [ + 9 => true, 22 => true, ], [ - 22 => true, + 9 => true, + ], + [ + 9 => true, ], [ 1 => true, @@ -1231,20 +1136,17 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, true, true, true, true, true, true, - ], - null, - [ - 9 => true, + true, + true, ], [ - 9 => true, + 22 => true, ], [ 1 => true, @@ -1259,7 +1161,8 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - 28 => true, + true, + true, true, true, true, @@ -1344,471 +1247,358 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, ], - [ - 26 => true, - true, - ], ]; /** @var array>> */ private const PARSER_CHOICE_PREDICTION_TABLE = [ - [ - 1 => [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - 15 => [ - 1, - ], - 18 => [ - 1, - ], - 25 => [ - 1, - ], - 28 => [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - [ - 1, - ], - 27 => [ - 155, - ], - ], - 4 => [ + 1 => [ 15 => [ - 5, + 2, ], 1 => [ - 8, + 5, ], 2 => [ - 8, + 5, ], 3 => [ - 8, + 5, ], 4 => [ - 8, + 5, ], 5 => [ - 8, + 5, ], 6 => [ - 8, + 5, ], 7 => [ - 8, + 5, ], 8 => [ - 8, + 5, ], 18 => [ - 8, + 5, ], 25 => [ - 8, + 5, ], [ - 8, + 5, ], - 28 => [ - 8, + [ + 5, ], [ - 8, + 5, ], [ - 8, + 5, ], [ - 8, + 5, ], [ - 8, + 5, ], [ - 8, + 5, ], [ - 8, + 5, + ], + [ + 5, ], ], - 9 => [ + 6 => [ 18 => [ - 10, + 7, ], 26 => [ - -14, + -11, + 126, ], 3 => [ - 14, - 28, + 11, + 25, ], 4 => [ - 14, - 28, + 11, + 25, ], 5 => [ - 14, - 28, + 11, + 25, ], 28 => [ - 14, + 11, ], [ - 14, + 11, ], [ - 14, + 11, ], [ - 14, + 11, ], [ - 14, + 11, ], [ - 14, + 11, ], [ - 14, + 11, ], 1 => [ - 28, + 25, ], 2 => [ - 28, + 25, ], 6 => [ - 28, + 25, ], 7 => [ - 28, + 25, ], 8 => [ - 28, + 25, ], 25 => [ - 28, + 125, + ], + 27 => [ + 126, ], ], - 14 => [ + 11 => [ 3 => [ - 15, + 12, ], [ - 15, + 12, ], 28 => [ - 15, + 12, ], [ - 15, + 12, ], [ - 15, + 12, ], [ - 15, + 12, ], [ - 15, + 12, ], [ - 15, + 12, ], [ - 15, + 12, ], 5 => [ - -28, + -25, ], ], [ 28 => [ - 16, + 13, ], [ - 16, + 13, ], [ - -20, + -17, ], [ - 20, + 17, ], [ - 20, + 17, ], [ - 20, + 17, ], [ - 20, + 17, ], 3 => [ - -26, + -23, ], 4 => [ - -27, + -24, ], ], [ 28 => [ - -18, + -15, ], [ - -19, + -16, ], ], - 20 => [ + 17 => [ 31 => [ - -22, + -19, ], [ - -23, + -20, ], [ - -24, + -21, ], [ - -25, + -22, ], ], 29 => [ - 1 => [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - [ - 30, - ], - 25 => [ - 55, - ], - ], - 31 => [ - 8 => [ - 32, - ], - 1 => [ - 46, - ], - 2 => [ - 46, - ], - 3 => [ - 46, - ], - 4 => [ - 46, - ], - 5 => [ - 46, - ], - 6 => [ - 46, - ], 7 => [ - 46, - ], - ], - 35 => [ - 7 => [ - -37, + -31, ], 6 => [ - -38, + -32, ], 1 => [ - -39, + -33, ], 2 => [ - -40, + -34, ], 3 => [ - -41, + -35, ], 4 => [ - -42, + -36, ], 5 => [ - -43, - ], - ], - 48 => [ - 25 => [ - 49, - ], - 8 => [ - 54, + -37, ], ], - 58 => [ + 42 => [ 11 => [ - 59, + 43, ], 18 => [ - 63, + 53, ], 13 => [ - 86, - 98, + 76, + 88, ], 16 => [ - 110, + 100, + ], + 25 => [ + 48, + ], + 8 => [ + 124, ], ], - 61 => [ + 45 => [ 1 => [ - 62, + 46, ], [ - 62, + 46, ], [ - 62, + 46, ], [ - 62, + 46, ], [ - 62, + 46, ], [ - 62, + 46, ], [ - 62, + 46, ], 25 => [ - 49, + 48, ], ], - 72 => [ + 62 => [ 27 => [ - -74, + -64, ], 26 => [ - -75, + -65, ], ], - 90 => [ + 80 => [ 1 => [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], 7 => [ - 93, + 83, ], [ - 93, + 83, ], 15 => [ - 93, + 83, ], 18 => [ - 93, + 83, ], 25 => [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], [ - 93, + 83, ], ], - 93 => [ + 83 => [ 25 => [ - -51, + -50, 0, ], 1 => [ @@ -1869,76 +1659,84 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 0, ], ], - 103 => [ + 93 => [ 1 => [ - 104, + 94, ], [ - 104, + 94, ], [ - 104, + 94, ], [ - 104, + 94, ], [ - 104, + 94, ], [ - 104, + 94, ], [ - 104, + 94, ], 24 => [ - 105, + 95, ], ], - 115 => [ + 105 => [ 15 => [ - 125, - ], - 27 => [ - 125, + 115, ], ], - 117 => [ + 107 => [ 8 => [ - 9, + 6, ], 18 => [ - 9, + 6, ], 25 => [ - 9, + 6, ], [ - 9, + 6, ], - 28 => [ - 9, + [ + 6, + ], + [ + 6, ], [ - 9, + 6, ], [ - 9, + 6, ], [ - 9, + 6, ], [ - 9, + 6, ], [ - 9, + 6, ], [ - 9, + 6, ], ], - 151 => [ + 135 => [ + 9 => [ + 136, + ], + 22 => [ + 139, + ], + ], + 145 => [ 1 => [ 0, ], @@ -1998,96 +1796,98 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf /** @var array> */ private const PARSER_SEQUENCE_PREDICTION_TABLE = [ - 1 => [ - 2, - -149, + [ + 1, + 134, + -143, ], - 28 => [ + 25 => [ + 26, + -43, + ], + [ + -29, 29, - -59, + 37, ], - 30 => [ - 31, + 46 => [ + 29, -49, ], - 49 => [ - 50, - 51, - -36, - ], - 55 => [ + 48 => [ + 49, 50, - 35, - -50, + -30, ], - 62 => [ - 35, - -50, + 53 => [ + 8, + 54, + 71, + -74, ], - [ - 11, - 64, - 81, - -84, - ], - 65 => [ - 66, - 77, - -80, + 55 => [ + 56, + 67, + -70, ], [ 0, - -69, - -71, - -73, - -77, - ], - 87 => [ - 88, - 89, - 94, - -80, - 96, - ], - 99 => [ - 88, - 100, - 107, - -80, - 109, + -59, + -61, + -63, + -67, ], - 110 => [ - 111, - 112, - -80, - 133, + 77 => [ + 78, + 79, + 84, + -70, + 86, + ], + 89 => [ + 78, + 90, + 97, + -70, + 99, + ], + 100 => [ + 101, + 102, + -70, + 123, ], - 113 => [ + 103 => [ + 104, + -120, + ], + 106 => [ + 107, + -114, + 74, 114, - -130, ], - 116 => [ - 117, - -124, - 84, - 124, + 110 => [ + -4, + 74, ], 120 => [ - -7, - 84, + 60, + -78, ], - 130 => [ - 70, - -88, + 125 => [ + 49, + 29, + -49, ], - 135 => [ - 136, + 130 => [ + 131, -1, - 138, + 133, ], - 149 => [ - 38, - -40, + 143 => [ + 32, + -34, ], ]; @@ -2152,27 +1952,24 @@ public function __construct() $this->parser = new \Phplrt\Parser\Parser( lexer: $this->lexer, grammar: [ - new \Phplrt\Parser\Grammar\Alternation([1, 155]), - new \Phplrt\Parser\Grammar\Concatenation([2, 147]), - new \Phplrt\Parser\Grammar\Concatenation([3, 143]), - new \Phplrt\Parser\Grammar\Concatenation([4, 139]), - new \Phplrt\Parser\Grammar\Alternation([5, 8]), - new \Phplrt\Parser\Grammar\Concatenation([6, 7]), + new \Phplrt\Parser\Grammar\Concatenation([1, 134, 141]), + new \Phplrt\Parser\Grammar\Alternation([2, 5]), + new \Phplrt\Parser\Grammar\Concatenation([3, 4]), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false), - new \Phplrt\Parser\Grammar\Concatenation([8]), - new \Phplrt\Parser\Grammar\Concatenation([9, 134]), - new \Phplrt\Parser\Grammar\Alternation([10, 13, 14, 28]), - new \Phplrt\Parser\Grammar\Concatenation([11, 0, 12]), + new \Phplrt\Parser\Grammar\Concatenation([5]), + new \Phplrt\Parser\Grammar\Concatenation([6, 129]), + new \Phplrt\Parser\Grammar\Alternation([7, 10, 11, 25, 125, 126]), + new \Phplrt\Parser\Grammar\Concatenation([8, 0, 9]), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Alternation([15, 27]), - new \Phplrt\Parser\Grammar\Alternation([16, 19, 20, 25, 26]), - new \Phplrt\Parser\Grammar\Alternation([17, 18]), + new \Phplrt\Parser\Grammar\Alternation([12, 24]), + new \Phplrt\Parser\Grammar\Alternation([13, 16, 17, 22, 23]), + new \Phplrt\Parser\Grammar\Alternation([14, 15]), new \Phplrt\Parser\Grammar\Lexeme(self::T_DQ_STRING_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQ_STRING_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FLOAT_LITERAL, true), - new \Phplrt\Parser\Grammar\Alternation([21, 22, 23, 24]), + new \Phplrt\Parser\Grammar\Alternation([18, 19, 20, 21]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BIN_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_OCT_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_HEX_INT_LITERAL, true), @@ -2180,14 +1977,11 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Concatenation([29, 57]), - new \Phplrt\Parser\Grammar\Alternation([30, 55]), - new \Phplrt\Parser\Grammar\Concatenation([31, 47]), - new \Phplrt\Parser\Grammar\Alternation([32, 46]), - new \Phplrt\Parser\Grammar\Concatenation([33, 34, 43]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), - new \Phplrt\Parser\Grammar\Concatenation([35]), - new \Phplrt\Parser\Grammar\Alternation([36, 37, 38, 39, 40, 41, 42]), + new \Phplrt\Parser\Grammar\Concatenation([26, 41]), + new \Phplrt\Parser\Grammar\Concatenation([27, 29, 37]), + new \Phplrt\Parser\Grammar\Optional(28), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, true), + new \Phplrt\Parser\Grammar\Alternation([30, 31, 32, 33, 34, 35, 36]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true), @@ -2195,158 +1989,154 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Repetition(44, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([33, 45]), - new \Phplrt\Parser\Grammar\Concatenation([35]), - new \Phplrt\Parser\Grammar\Concatenation([35, 43]), + new \Phplrt\Parser\Grammar\Repetition(38, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([39, 40]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), + new \Phplrt\Parser\Grammar\Concatenation([29]), + new \Phplrt\Parser\Grammar\Optional(42), + new \Phplrt\Parser\Grammar\Alternation([43, 53, 76, 88, 100, 48, 124]), + new \Phplrt\Parser\Grammar\Concatenation([44, 45]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true), + new \Phplrt\Parser\Grammar\Alternation([46, 48]), + new \Phplrt\Parser\Grammar\Concatenation([29, 47]), new \Phplrt\Parser\Grammar\Optional(48), - new \Phplrt\Parser\Grammar\Alternation([49, 54]), - new \Phplrt\Parser\Grammar\Concatenation([50, 51, 53]), + new \Phplrt\Parser\Grammar\Concatenation([49, 50, 52]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), - new \Phplrt\Parser\Grammar\Repetition(52, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([35, 50]), - new \Phplrt\Parser\Grammar\Optional(35), - new \Phplrt\Parser\Grammar\Concatenation([33, 49]), - new \Phplrt\Parser\Grammar\Concatenation([50, 35, 56]), - new \Phplrt\Parser\Grammar\Optional(49), + new \Phplrt\Parser\Grammar\Repetition(51, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([29, 49]), + new \Phplrt\Parser\Grammar\Optional(29), + new \Phplrt\Parser\Grammar\Concatenation([8, 54, 71, 72]), + new \Phplrt\Parser\Grammar\Optional(55), + new \Phplrt\Parser\Grammar\Concatenation([56, 67, 70]), + new \Phplrt\Parser\Grammar\Concatenation([0, 57, 59, 61, 65]), new \Phplrt\Parser\Grammar\Optional(58), - new \Phplrt\Parser\Grammar\Alternation([59, 63, 86, 98, 110]), - new \Phplrt\Parser\Grammar\Concatenation([60, 61]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true), - new \Phplrt\Parser\Grammar\Alternation([62, 49]), - new \Phplrt\Parser\Grammar\Concatenation([35, 56]), - new \Phplrt\Parser\Grammar\Concatenation([11, 64, 81, 82]), - new \Phplrt\Parser\Grammar\Optional(65), - new \Phplrt\Parser\Grammar\Concatenation([66, 77, 80]), - new \Phplrt\Parser\Grammar\Concatenation([0, 67, 69, 71, 75]), - new \Phplrt\Parser\Grammar\Optional(68), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true), - new \Phplrt\Parser\Grammar\Optional(70), + new \Phplrt\Parser\Grammar\Optional(60), new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true), - new \Phplrt\Parser\Grammar\Optional(72), - new \Phplrt\Parser\Grammar\Alternation([73, 74]), + new \Phplrt\Parser\Grammar\Optional(62), + new \Phplrt\Parser\Grammar\Alternation([63, 64]), new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Optional(76), + new \Phplrt\Parser\Grammar\Optional(66), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true), - new \Phplrt\Parser\Grammar\Repetition(78, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([79, 66]), + new \Phplrt\Parser\Grammar\Repetition(68, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([69, 56]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false), - new \Phplrt\Parser\Grammar\Optional(79), + new \Phplrt\Parser\Grammar\Optional(69), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), - new \Phplrt\Parser\Grammar\Optional(83), - new \Phplrt\Parser\Grammar\Concatenation([84, 85]), + new \Phplrt\Parser\Grammar\Optional(73), + new \Phplrt\Parser\Grammar\Concatenation([74, 75]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Concatenation([87, 97]), - new \Phplrt\Parser\Grammar\Concatenation([88, 89, 94, 80, 96]), + new \Phplrt\Parser\Grammar\Concatenation([77, 87]), + new \Phplrt\Parser\Grammar\Concatenation([78, 79, 84, 70, 86]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false), - new \Phplrt\Parser\Grammar\Concatenation([90]), - new \Phplrt\Parser\Grammar\Alternation([91, 93]), - new \Phplrt\Parser\Grammar\Concatenation([92, 93]), + new \Phplrt\Parser\Grammar\Concatenation([80]), + new \Phplrt\Parser\Grammar\Alternation([81, 83]), + new \Phplrt\Parser\Grammar\Concatenation([82, 83]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), - new \Phplrt\Parser\Grammar\Alternation([50, 0]), - new \Phplrt\Parser\Grammar\Repetition(95, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([79, 90]), + new \Phplrt\Parser\Grammar\Alternation([49, 0]), + new \Phplrt\Parser\Grammar\Repetition(85, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([69, 80]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Predicate(11, false), - new \Phplrt\Parser\Grammar\Concatenation([99, 63]), - new \Phplrt\Parser\Grammar\Concatenation([88, 100, 107, 80, 109]), - new \Phplrt\Parser\Grammar\Concatenation([101]), - new \Phplrt\Parser\Grammar\Concatenation([35, 102]), - new \Phplrt\Parser\Grammar\Repetition(103, 0, \INF), - new \Phplrt\Parser\Grammar\Alternation([104, 105]), - new \Phplrt\Parser\Grammar\Concatenation([35, 0]), - new \Phplrt\Parser\Grammar\Concatenation([106, 0]), + new \Phplrt\Parser\Grammar\Predicate(8, false), + new \Phplrt\Parser\Grammar\Concatenation([89, 53]), + new \Phplrt\Parser\Grammar\Concatenation([78, 90, 97, 70, 99]), + new \Phplrt\Parser\Grammar\Concatenation([91]), + new \Phplrt\Parser\Grammar\Concatenation([29, 92]), + new \Phplrt\Parser\Grammar\Repetition(93, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([94, 95]), + new \Phplrt\Parser\Grammar\Concatenation([29, 0]), + new \Phplrt\Parser\Grammar\Concatenation([96, 0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, false), - new \Phplrt\Parser\Grammar\Repetition(108, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([79, 101]), + new \Phplrt\Parser\Grammar\Repetition(98, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([69, 91]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Concatenation([111, 112, 80, 133]), + new \Phplrt\Parser\Grammar\Concatenation([101, 102, 70, 123]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([113, 132]), - new \Phplrt\Parser\Grammar\Concatenation([114, 128]), - new \Phplrt\Parser\Grammar\Concatenation([115, 126]), - new \Phplrt\Parser\Grammar\Alternation([116, 125]), - new \Phplrt\Parser\Grammar\Concatenation([117, 122, 84, 124]), - new \Phplrt\Parser\Grammar\Alternation([118, 9]), - new \Phplrt\Parser\Grammar\Concatenation([35, 119]), - new \Phplrt\Parser\Grammar\Predicate(120, true), - new \Phplrt\Parser\Grammar\Concatenation([121, 84]), - new \Phplrt\Parser\Grammar\Optional(6), - new \Phplrt\Parser\Grammar\Optional(123), + new \Phplrt\Parser\Grammar\Alternation([103, 122]), + new \Phplrt\Parser\Grammar\Concatenation([104, 118]), + new \Phplrt\Parser\Grammar\Concatenation([105, 116]), + new \Phplrt\Parser\Grammar\Alternation([106, 115]), + new \Phplrt\Parser\Grammar\Concatenation([107, 112, 74, 114]), + new \Phplrt\Parser\Grammar\Alternation([108, 6]), + new \Phplrt\Parser\Grammar\Concatenation([29, 109]), + new \Phplrt\Parser\Grammar\Predicate(110, true), + new \Phplrt\Parser\Grammar\Concatenation([111, 74]), + new \Phplrt\Parser\Grammar\Optional(3), + new \Phplrt\Parser\Grammar\Optional(113), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true), new \Phplrt\Parser\Grammar\Concatenation([0]), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Repetition(127, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([79, 115]), - new \Phplrt\Parser\Grammar\Optional(129), - new \Phplrt\Parser\Grammar\Concatenation([79, 130]), - new \Phplrt\Parser\Grammar\Concatenation([70, 131]), - new \Phplrt\Parser\Grammar\Optional(87), - new \Phplrt\Parser\Grammar\Optional(130), + new \Phplrt\Parser\Grammar\Repetition(117, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([69, 105]), + new \Phplrt\Parser\Grammar\Optional(119), + new \Phplrt\Parser\Grammar\Concatenation([69, 120]), + new \Phplrt\Parser\Grammar\Concatenation([60, 121]), + new \Phplrt\Parser\Grammar\Optional(77), + new \Phplrt\Parser\Grammar\Optional(120), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(135, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([136, 137, 138]), + new \Phplrt\Parser\Grammar\Concatenation([39, 48]), + new \Phplrt\Parser\Grammar\Concatenation([49, 29, 47]), + new \Phplrt\Parser\Grammar\Concatenation([62, 127]), + new \Phplrt\Parser\Grammar\Predicate(128, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, false), + new \Phplrt\Parser\Grammar\Repetition(130, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([131, 132, 133]), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false), new \Phplrt\Parser\Grammar\Optional(0), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(140, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([141, 142]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, false), - new \Phplrt\Parser\Grammar\Concatenation([4]), - new \Phplrt\Parser\Grammar\Repetition(144, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([145, 146]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, false), - new \Phplrt\Parser\Grammar\Concatenation([3]), - new \Phplrt\Parser\Grammar\Optional(148), - new \Phplrt\Parser\Grammar\Concatenation([149, 151, 6, 152, 153, 154]), - new \Phplrt\Parser\Grammar\Concatenation([38, 150]), - new \Phplrt\Parser\Grammar\Optional(39), - new \Phplrt\Parser\Grammar\Alternation([0, 72]), + new \Phplrt\Parser\Grammar\Repetition(135, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([136, 139]), + new \Phplrt\Parser\Grammar\Concatenation([137, 138]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, true), + new \Phplrt\Parser\Grammar\Concatenation([1]), + new \Phplrt\Parser\Grammar\Concatenation([58, 140]), + new \Phplrt\Parser\Grammar\Concatenation([1]), + new \Phplrt\Parser\Grammar\Optional(142), + new \Phplrt\Parser\Grammar\Concatenation([143, 145, 3, 146, 147, 148]), + new \Phplrt\Parser\Grammar\Concatenation([32, 144]), + new \Phplrt\Parser\Grammar\Optional(33), + new \Phplrt\Parser\Grammar\Alternation([0, 62]), new \Phplrt\Parser\Grammar\Concatenation([0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Concatenation([72, 148]), ], initial: 0, reducers: [ 0 => $this->reduceType(...), - 2 => $this->reduceUnionType(...), - 3 => $this->reduceIntersectionType(...), - 5 => self::reduceNullableType(...), - 8 => $this->reduceListOrOffsetType(...), - 13 => self::reduceThisType(...), - 14 => $this->reduceLiteralType(...), - 17 => self::reduceDoubleQuotedStringLiteralType(...), - 18 => self::reduceSingleQuotedStringLiteralType(...), - 19 => self::reduceFloatLiteralType(...), - 20 => self::reduceIntLiteralType(...), - 25 => self::reduceTrueLiteralType(...), - 26 => self::reduceFalseLiteralType(...), - 27 => self::reduceNullLiteralType(...), - 28 => $this->reduceNamedType(...), - 29 => self::reduceNameOrMask(...), - 32 => self::reduceFullQualifiedName(...), - 35 => self::reduceIdentifier(...), - 46 => self::reduceRelativeName(...), - 50 => self::reduceWildcardType(...), - 54 => self::reduceNamespacedMask(...), - 64 => self::reduceCallableParameters(...), - 66 => self::reduceCallableParameter(...), - 72 => self::reduceVariable(...), - 87 => $this->reduceTemplateArguments(...), - 90 => $this->reduceTemplateArgument(...), - 92 => self::reduceIdentifierWithExtraSpace(...), - 99 => $this->reduceTemplateParameters(...), - 101 => self::reduceTemplateParameter(...), - 104 => self::reduceTemplateBoundEdge(...), - 110 => $this->reduceShapeFields(...), - 114 => self::reduceShapeFieldsList(...), - 116 => self::reduceExplicitField(...), - 125 => self::reduceImplicitField(...), - 135 => self::reduceListOrOffsetSuffix(...), - 149 => self::reduceTernaryExpressionOperator(...), + 2 => self::reduceNullableType(...), + 5 => $this->reduceListOrOffsetType(...), + 10 => self::reduceThisType(...), + 11 => $this->reduceLiteralType(...), + 14 => self::reduceDoubleQuotedStringLiteralType(...), + 15 => self::reduceSingleQuotedStringLiteralType(...), + 16 => self::reduceFloatLiteralType(...), + 17 => self::reduceIntLiteralType(...), + 22 => self::reduceTrueLiteralType(...), + 23 => self::reduceFalseLiteralType(...), + 24 => self::reduceNullLiteralType(...), + 25 => $this->reduceNamedType(...), + 26 => self::reduceName(...), + 29 => self::reduceIdentifier(...), + 49 => self::reduceWildcardType(...), + 54 => self::reduceCallableParameters(...), + 56 => self::reduceCallableParameter(...), + 62 => self::reduceVariable(...), + 77 => $this->reduceTemplateArguments(...), + 80 => $this->reduceTemplateArgument(...), + 82 => self::reduceIdentifierWithExtraSpace(...), + 89 => $this->reduceTemplateParameters(...), + 91 => self::reduceTemplateParameter(...), + 94 => self::reduceTemplateBoundEdge(...), + 100 => $this->reduceShapeFields(...), + 104 => self::reduceShapeFieldsList(...), + 106 => self::reduceExplicitField(...), + 115 => self::reduceImplicitField(...), + 124 => self::reduceNamespacedMask(...), + 125 => self::reduceLeadingMaskType(...), + 130 => self::reduceListOrOffsetSuffix(...), + 143 => self::reduceTernaryExpressionOperator(...), ], expectations: self::PARSER_EXPECTATIONS, messages: self::PARSER_MESSAGES, @@ -2372,60 +2162,80 @@ private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed return $children[0]; } - if ($this->features->conditions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset); + // The suffix of a condition is the only thing the rule reads that carries + // a flag of its own, which is what the operands are told from + $condition = null; + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + if (\is_bool($children[$i])) { + $condition = \array_splice($children, $i); + + break; + } } - $condition = $children[1] - ? new Type\Condition\NotEqualConditionNode($children[0], $children[2], $offset) - : new Type\Condition\EqualConditionNode($children[0], $children[2], $offset); + // The operands joined by an ampersand bind tighter, so they are collected + // into a group of their own, and the groups are what a union is made of + $groups = []; + $group = [$children[0]]; + $isUnion = false; + $isIntersection = false; - return new Type\TernaryExpressionNode( - $condition, - $children[3], - $children[4], - $offset, - ); - } + for ($i = 1, $count = \count($children); $i < $count; $i += 2) { + if ($children[$i]->id === self::T_OR) { + $isUnion = true; + $groups[] = $group; + $group = [$children[$i + 1]]; - private function reduceUnionType(\Phplrt\Parser\Context $ctx, mixed $children): mixed - { - // The variables below are declared by the compiler - $source = $ctx->source; - $offset = $ctx->begin; + continue; + } - if (\count($children) === 1) { - return $children[0]; + $isIntersection = true; + $group[] = $children[$i + 1]; + } + + $groups[] = $group; + + // The operand that binds tighter is the one reported first, the way the + // reading would have reached it + if ($isIntersection && $this->features->intersections === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); } - if ($this->features->unions === false) { + if ($isUnion && $this->features->unions === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset); } - $statement = new Type\UnionTypeNode($children); - $statement->offset = $offset; + $statements = []; - return $statement; - } + foreach ($groups as $group) { + $statements[] = \count($group) === 1 + ? $group[0] + : new Type\IntersectionTypeNode($group, $group[0]->offset); + } - private function reduceIntersectionType(\Phplrt\Parser\Context $ctx, mixed $children): mixed - { - // The variables below are declared by the compiler - $source = $ctx->source; - $offset = $ctx->begin; + $statement = \count($statements) === 1 + ? $statements[0] + : new Type\UnionTypeNode($statements, $offset); - if (\count($children) === 1) { - return $children[0]; + if ($condition === null) { + return $statement; } - if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); + if ($this->features->conditions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset); } - $statement = new Type\IntersectionTypeNode($children); - $statement->offset = $offset; + $comparison = $condition[0] + ? new Type\Condition\NotEqualConditionNode($statement, $condition[1], $offset) + : new Type\Condition\EqualConditionNode($statement, $condition[1], $offset); - return $statement; + return new Type\TernaryExpressionNode( + $comparison, + $condition[2], + $condition[3], + $offset, + ); } private static function reduceNullableType(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -2583,12 +2393,29 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): $head = $children[0]; $suffix = $children[1] ?? null; - if ($head instanceof Type\ConstMaskNode) { - if ($suffix !== null) { - throw Exception\ConstMaskException::becauseNothingFollowsAMask($source, $offset); + // "Some\Any\*" and "Some\Any\SOME_*" + // + // A mask closes the type it is written at the end of, so it is read as a + // suffix of a name and never carries a suffix of its own. + if ($suffix instanceof Type\MaskNode || $suffix instanceof Type\WildcardNode) { + $parts = $head->parts; + + if ($suffix instanceof Type\MaskNode) { + // "Some\Any\*" + $mask = $suffix; + } else { + // "Some\Any\SOME_*" + $segment = \array_pop($parts); + $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); } - return $head; + return new Type\ConstMaskNode( + $mask, + $parts === [] + ? $head->isFullyQualified + : new Type\Name($parts, $head->isFullyQualified, $head->offset), + $offset, + ); } switch (true) { @@ -2634,49 +2461,21 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): } } - private static function reduceNameOrMask(\Phplrt\Parser\Context $ctx, mixed $children): mixed + private static function reduceName(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler $offset = $ctx->begin; - $head = $children[0]; - - // "*_SOME" - if (!$head instanceof Type\Name) { - return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); + // "Some\Any" starts at a segment of its own + if ($children[0] instanceof Type\Identifier) { + return new Type\Name($children, false, $offset); } - // "Some\Any" - if (!isset($children[1])) { - return $head; - } - - $parts = $head->parts; + // "\Some\Any" carries the separator in front of its segments, and the + // name itself is the part that follows it + $segments = \array_slice($children, 1); - if ($children[1] instanceof Type\MaskNode) { - // "Some\Any\*" - $mask = $children[1]; - } else { - // "Some\Any\SOME_*" - $segment = \array_pop($parts); - $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); - } - - return new Type\ConstMaskNode( - $mask, - $parts === [] - ? $head->isFullyQualified - : new Type\Name($parts, $head->isFullyQualified, $head->offset), - $offset, - ); - } - - private static function reduceFullQualifiedName(\Phplrt\Parser\Context $ctx, mixed $children): mixed - { - // The variables below are declared by the compiler - $offset = $ctx->begin; - - return new Type\Name($children, true, $offset); + return new Type\Name($segments, true, $segments[0]->offset); } private static function reduceIdentifier(\Phplrt\Parser\Context $ctx, mixed $children): mixed @@ -2692,14 +2491,6 @@ private static function reduceIdentifier(\Phplrt\Parser\Context $ctx, mixed $chi ); } - private static function reduceRelativeName(\Phplrt\Parser\Context $ctx, mixed $children): mixed - { - // The variables below are declared by the compiler - $offset = $ctx->begin; - - return new Type\Name($children, false, $offset); - } - private static function reduceWildcardType(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler @@ -2708,14 +2499,6 @@ private static function reduceWildcardType(\Phplrt\Parser\Context $ctx, mixed $c return new Type\WildcardNode($offset); } - private static function reduceNamespacedMask(\Phplrt\Parser\Context $ctx, mixed $children): mixed - { - // The variables below are declared by the compiler - $offset = $ctx->begin; - - return new Type\MaskNode($children, $offset); - } - private static function reduceCallableParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed { // The variables below are declared by the compiler @@ -3010,6 +2793,22 @@ private static function reduceImplicitField(\Phplrt\Parser\Context $ctx, mixed $ return new Type\Shape\ImplicitFieldNode($children[0], false, $offset); } + private static function reduceNamespacedMask(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\MaskNode($children, $offset); + } + + private static function reduceLeadingMaskType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); + } + private static function reduceListOrOffsetSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed { return $children[0] ?? true; From 89e6b6c4ee15077fc863730b1b69279cb7ab8749 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 15:55:36 +0300 Subject: [PATCH 105/119] Actualize phpdoc benchmarks --- bench/PhpDoc/Tools/DocBlockParserBench.php | 54 ++++++++++++---------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/bench/PhpDoc/Tools/DocBlockParserBench.php b/bench/PhpDoc/Tools/DocBlockParserBench.php index b8fe41b..0037f43 100644 --- a/bench/PhpDoc/Tools/DocBlockParserBench.php +++ b/bench/PhpDoc/Tools/DocBlockParserBench.php @@ -4,10 +4,6 @@ namespace TypeLang\Bench\PhpDoc\Tools; -use FilesystemIterator; -use PhpToken; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -17,9 +13,6 @@ abstract class DocBlockParserBench * A sandbox directory containing the third-party packages that are used * as a real-world DocBlock corpus. * - * The whole directory is disposable: It is created, filled and installed - * by the benchmark itself and can be removed at any time. - * * @var non-empty-string */ protected const CORPUS_DIRECTORY = __DIR__ . '/../../var/corpus'; @@ -27,22 +20,42 @@ abstract class DocBlockParserBench /** * The packages to extract the DocBlocks from. * - * Each package is a separate benchmark set, so the list should contain - * recognizable packages of a different size and documentation style. - * * @var non-empty-array */ protected const CORPUS_PACKAGES = [ 'doctrine/collections' => '*', + 'doctrine/lexer' => '*', 'guzzlehttp/guzzle' => '*', + 'guzzlehttp/promises' => '*', + 'guzzlehttp/psr7' => '*', 'illuminate/support' => '*', 'monolog/monolog' => '*', 'nikic/php-parser' => '*', 'phpdocumentor/reflection-docblock' => '*', 'phpstan/phpdoc-parser' => '*', + 'phpunit/php-code-coverage' => '*', + 'phpunit/php-file-iterator' => '*', + 'phpunit/php-text-template' => '*', + 'phpunit/php-timer' => '*', + 'phpunit/phpunit' => '*', + 'psr/container' => '*', + 'psr/http-message' => '*', 'psr/log' => '*', + 'ralouphie/getallheaders' => '*', + 'sebastian/comparator' => '*', + 'sebastian/diff' => '*', + 'sebastian/environment' => '*', + 'sebastian/exporter' => '*', + 'sebastian/recursion-context' => '*', 'symfony/console' => '*', + 'symfony/event-dispatcher' => '*', + 'symfony/finder' => '*', 'symfony/http-foundation' => '*', + 'symfony/polyfill-ctype' => '*', + 'symfony/polyfill-mbstring' => '*', + 'symfony/process' => '*', + 'symfony/service-contracts' => '*', + 'symfony/var-dumper' => '*', 'twig/twig' => '*', 'webmozart/assert' => '*', ]; @@ -54,6 +67,7 @@ abstract class DocBlockParserBench * in a separate process before any benchmark is launched. * * @return iterable}> + * @throws \JsonException */ public static function docBlocksDataProvider(): iterable { @@ -84,6 +98,7 @@ public static function docBlocksDataProvider(): iterable * of the installed packages. * * @return array{versions: array} + * @throws \JsonException */ private static function getCorpusMetadata(): array { @@ -211,15 +226,6 @@ private static function getComposerBinary(): array /** * Extracts every DocBlock of every PHP file of the given directory. * - * The PHP tokenizer is used instead of a full-blown parser (like the - * {@link https://github.com/nikic/PHP-Parser} package) because the - * benchmark requires the DocBlock texts only and not the elements - * they are attached to. - * - * A naive regular expression is not an option either: It also matches - * the DocBlocks that occur inside string literals and heredocs, which - * the test suites and fixtures of the corpus packages are full of. - * * @param non-empty-string $directory * @return list */ @@ -234,7 +240,7 @@ private static function extractDocBlocks(string $directory): array continue; } - foreach (PhpToken::tokenize($source) as $token) { + foreach (\PhpToken::tokenize($source) as $token) { if ($token->is(\T_DOC_COMMENT) && $token->text !== '') { $result[] = $token->text; } @@ -250,11 +256,11 @@ private static function extractDocBlocks(string $directory): array */ private static function getSourceFiles(string $directory): iterable { - $files = new RecursiveIteratorIterator( - iterator: new RecursiveDirectoryIterator( + $files = new \RecursiveIteratorIterator( + iterator: new \RecursiveDirectoryIterator( directory: $directory, - flags: FilesystemIterator::SKIP_DOTS - | FilesystemIterator::CURRENT_AS_FILEINFO, + flags: \FilesystemIterator::SKIP_DOTS + | \FilesystemIterator::CURRENT_AS_FILEINFO, ), ); From f8f2476e04d8d37d214e949684129ada7d41421d Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 17:00:02 +0300 Subject: [PATCH 106/119] Fix variables in conditions --- libs/parser/resources/grammar.pp3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3 index b28c2fc..6b17c1e 100644 --- a/libs/parser/resources/grammar.pp3 +++ b/libs/parser/resources/grammar.pp3 @@ -110,6 +110,7 @@ Type -> { ); } : UnaryType() LogicalTail()* TernaryExpressionSuffix()? + | Variable() TernaryExpressionSuffix() ; TernaryExpressionSuffix @@ -205,5 +206,4 @@ PrimaryType | LiteralType() | NamedType() | LeadingMaskType() - | Variable() &::T_EQ:: ; From 418d3255a2a48bd6f97e210a530125fb435db3a9 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 17:00:25 +0300 Subject: [PATCH 107/119] Add conditions tests --- libs/parser/src/Internal/CompiledExecutor.php | 866 ++++++++++-------- libs/parser/tests/Syntax/VariableTest.php | 35 + 2 files changed, 497 insertions(+), 404 deletions(-) diff --git a/libs/parser/src/Internal/CompiledExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php index 2e95711..839476b 100644 --- a/libs/parser/src/Internal/CompiledExecutor.php +++ b/libs/parser/src/Internal/CompiledExecutor.php @@ -1,15 +1,15 @@ */ private const PARSER_MESSAGES = [ - 4 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', - 9 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', - 40 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', - 45 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', - 71 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', - 75 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', - 79 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', - 86 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', - 90 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', - 99 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', - 114 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', - 123 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', - 133 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', - 138 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', - 140 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', - 146 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', + 5 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', + 10 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', + 41 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', + 46 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', + 72 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', + 76 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', + 80 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', + 87 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', + 91 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', + 100 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', + 115 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', + 124 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', + 131 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', + 136 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', + 138 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', + 144 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', 'Syntax error, unexpected {token}, a condition must be parted with a colon ":"', 'Syntax error, unexpected {token}, a condition must carry the type it is false of', ]; /** @var array */ private const PARSER_KEPT_TABLE = [ + true, true, false, true, @@ -300,9 +301,6 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, false, - false, - false, - false, true, false, false, @@ -322,6 +320,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf false, false, false, + true, ]; /** @var array|null> */ @@ -361,8 +360,28 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, + 28 => true, + true, + true, + true, + true, true, true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, true, true, true, @@ -388,8 +407,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - true, - true, + 28 => true, true, true, true, @@ -409,8 +427,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - true, - true, + 28 => true, true, true, true, @@ -430,8 +447,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - true, - true, + 28 => true, true, true, true, @@ -978,8 +994,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - true, - true, + 28 => true, true, true, true, @@ -999,8 +1014,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - true, - true, + 28 => true, true, true, true, @@ -1093,14 +1107,6 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf [ 25 => true, ], - [ - 26 => true, - true, - ], - null, - [ - 1 => true, - ], null, [ 20 => true, @@ -1136,8 +1142,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - true, - true, + 28 => true, true, true, true, @@ -1161,8 +1166,7 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 18 => true, 25 => true, true, - true, - true, + 28 => true, true, true, true, @@ -1247,209 +1251,262 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf true, true, ], + [ + 26 => true, + true, + ], ]; /** @var array>> */ private const PARSER_CHOICE_PREDICTION_TABLE = [ - 1 => [ + [ + 1 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], 15 => [ - 2, + 1, + ], + 18 => [ + 1, + ], + 25 => [ + 1, + ], + 28 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + 27 => [ + 147, + ], + ], + 2 => [ + 15 => [ + 3, ], 1 => [ - 5, + 6, ], 2 => [ - 5, + 6, ], 3 => [ - 5, + 6, ], 4 => [ - 5, + 6, ], 5 => [ - 5, + 6, ], 6 => [ - 5, + 6, ], 7 => [ - 5, + 6, ], 8 => [ - 5, + 6, ], 18 => [ - 5, + 6, ], 25 => [ - 5, - ], - [ - 5, + 6, ], [ - 5, + 6, ], - [ - 5, + 28 => [ + 6, ], [ - 5, + 6, ], [ - 5, + 6, ], [ - 5, + 6, ], [ - 5, + 6, ], [ - 5, + 6, ], [ - 5, + 6, ], ], - 6 => [ + 7 => [ 18 => [ - 7, + 8, ], 26 => [ - -11, - 126, + -12, ], 3 => [ - 11, - 25, + 12, + 26, ], 4 => [ - 11, - 25, + 12, + 26, ], 5 => [ - 11, - 25, + 12, + 26, ], 28 => [ - 11, + 12, ], [ - 11, + 12, ], [ - 11, + 12, ], [ - 11, + 12, ], [ - 11, + 12, ], [ - 11, + 12, ], [ - 11, + 12, ], 1 => [ - 25, + 26, ], 2 => [ - 25, + 26, ], 6 => [ - 25, + 26, ], 7 => [ - 25, + 26, ], 8 => [ - 25, + 26, ], 25 => [ - 125, - ], - 27 => [ 126, ], ], - 11 => [ + 12 => [ 3 => [ - 12, + 13, ], [ - 12, + 13, ], 28 => [ - 12, + 13, ], [ - 12, + 13, ], [ - 12, + 13, ], [ - 12, + 13, ], [ - 12, + 13, ], [ - 12, + 13, ], [ - 12, + 13, ], 5 => [ - -25, + -26, ], ], [ 28 => [ - 13, + 14, ], [ - 13, + 14, ], [ - -17, + -18, ], [ - 17, + 18, ], [ - 17, + 18, ], [ - 17, + 18, ], [ - 17, + 18, ], 3 => [ - -23, + -24, ], 4 => [ - -24, + -25, ], ], [ 28 => [ - -15, + -16, ], [ - -16, + -17, ], ], - 17 => [ + 18 => [ 31 => [ - -19, - ], - [ -20, ], [ @@ -1458,147 +1515,150 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf [ -22, ], + [ + -23, + ], ], - 29 => [ + 30 => [ 7 => [ - -31, + -32, ], 6 => [ - -32, + -33, ], 1 => [ - -33, + -34, ], 2 => [ - -34, + -35, ], 3 => [ - -35, + -36, ], 4 => [ - -36, + -37, ], 5 => [ - -37, + -38, ], ], - 42 => [ + 43 => [ 11 => [ - 43, + 44, ], 18 => [ - 53, + 54, ], 13 => [ - 76, - 88, + 77, + 89, ], 16 => [ - 100, + 101, ], 25 => [ - 48, + 49, ], 8 => [ - 124, + 125, ], ], - 45 => [ + 46 => [ 1 => [ - 46, + 47, ], [ - 46, + 47, ], [ - 46, + 47, ], [ - 46, + 47, ], [ - 46, + 47, ], [ - 46, + 47, ], [ - 46, + 47, ], 25 => [ - 48, + 49, ], ], - 62 => [ + 63 => [ 27 => [ - -64, + -65, ], 26 => [ - -65, + -66, ], ], - 80 => [ + 81 => [ 1 => [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], 7 => [ - 83, + 84, ], [ - 83, + 84, ], 15 => [ - 83, + 84, ], 18 => [ - 83, + 84, ], 25 => [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], [ - 83, + 84, ], ], - 83 => [ + 84 => [ 25 => [ - -50, + -51, 0, ], 1 => [ @@ -1659,84 +1719,84 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf 0, ], ], - 93 => [ + 94 => [ 1 => [ - 94, + 95, ], [ - 94, + 95, ], [ - 94, + 95, ], [ - 94, + 95, ], [ - 94, + 95, ], [ - 94, + 95, ], [ - 94, + 95, ], 24 => [ - 95, + 96, ], ], - 105 => [ + 106 => [ 15 => [ - 115, + 116, + ], + 27 => [ + 116, ], ], - 107 => [ + 108 => [ 8 => [ - 6, + 7, ], 18 => [ - 6, + 7, ], 25 => [ - 6, - ], - [ - 6, + 7, ], [ - 6, + 7, ], - [ - 6, + 28 => [ + 7, ], [ - 6, + 7, ], [ - 6, + 7, ], [ - 6, + 7, ], [ - 6, + 7, ], [ - 6, + 7, ], [ - 6, + 7, ], ], - 135 => [ + 133 => [ 9 => [ - 136, + 134, ], 22 => [ - 139, + 137, ], ], - 145 => [ + 143 => [ 1 => [ 0, ], @@ -1796,98 +1856,98 @@ abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterf /** @var array> */ private const PARSER_SEQUENCE_PREDICTION_TABLE = [ - [ - 1, - 134, - -143, + 1 => [ + 2, + 132, + -141, ], - 25 => [ - 26, - -43, + 26 => [ + 27, + -44, ], [ - -29, - 29, - 37, + -30, + 30, + 38, ], - 46 => [ - 29, - -49, + 47 => [ + 30, + -50, ], - 48 => [ - 49, + 49 => [ 50, - -30, + 51, + -31, ], - 53 => [ - 8, - 54, - 71, - -74, + 54 => [ + 9, + 55, + 72, + -75, ], - 55 => [ - 56, - 67, - -70, + 56 => [ + 57, + 68, + -71, ], [ 0, - -59, - -61, - -63, - -67, + -60, + -62, + -64, + -68, + ], + 78 => [ + 79, + 80, + 85, + -71, + 87, ], - 77 => [ - 78, + 90 => [ 79, - 84, - -70, - 86, - ], - 89 => [ - 78, - 90, - 97, - -70, - 99, - ], - 100 => [ - 101, + 91, + 98, + -71, + 100, + ], + 101 => [ 102, - -70, - 123, + 103, + -71, + 124, ], - 103 => [ - 104, - -120, + 104 => [ + 105, + -121, ], - 106 => [ - 107, - -114, - 74, - 114, + 107 => [ + 108, + -115, + 75, + 115, ], - 110 => [ - -4, - 74, + 111 => [ + -5, + 75, ], - 120 => [ - 60, - -78, + 121 => [ + 61, + -79, ], - 125 => [ - 49, - 29, - -49, + 126 => [ + 50, + 30, + -50, ], - 130 => [ - 131, + 128 => [ + 129, -1, - 133, + 131, ], - 143 => [ - 32, - -34, + 141 => [ + 33, + -35, ], ]; @@ -1952,24 +2012,25 @@ public function __construct() $this->parser = new \Phplrt\Parser\Parser( lexer: $this->lexer, grammar: [ - new \Phplrt\Parser\Grammar\Concatenation([1, 134, 141]), - new \Phplrt\Parser\Grammar\Alternation([2, 5]), - new \Phplrt\Parser\Grammar\Concatenation([3, 4]), + new \Phplrt\Parser\Grammar\Alternation([1, 147]), + new \Phplrt\Parser\Grammar\Concatenation([2, 132, 139]), + new \Phplrt\Parser\Grammar\Alternation([3, 6]), + new \Phplrt\Parser\Grammar\Concatenation([4, 5]), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false), - new \Phplrt\Parser\Grammar\Concatenation([5]), - new \Phplrt\Parser\Grammar\Concatenation([6, 129]), - new \Phplrt\Parser\Grammar\Alternation([7, 10, 11, 25, 125, 126]), - new \Phplrt\Parser\Grammar\Concatenation([8, 0, 9]), + new \Phplrt\Parser\Grammar\Concatenation([6]), + new \Phplrt\Parser\Grammar\Concatenation([7, 127]), + new \Phplrt\Parser\Grammar\Alternation([8, 11, 12, 26, 126]), + new \Phplrt\Parser\Grammar\Concatenation([9, 0, 10]), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Alternation([12, 24]), - new \Phplrt\Parser\Grammar\Alternation([13, 16, 17, 22, 23]), - new \Phplrt\Parser\Grammar\Alternation([14, 15]), + new \Phplrt\Parser\Grammar\Alternation([13, 25]), + new \Phplrt\Parser\Grammar\Alternation([14, 17, 18, 23, 24]), + new \Phplrt\Parser\Grammar\Alternation([15, 16]), new \Phplrt\Parser\Grammar\Lexeme(self::T_DQ_STRING_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQ_STRING_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FLOAT_LITERAL, true), - new \Phplrt\Parser\Grammar\Alternation([18, 19, 20, 21]), + new \Phplrt\Parser\Grammar\Alternation([19, 20, 21, 22]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BIN_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_OCT_INT_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_HEX_INT_LITERAL, true), @@ -1977,11 +2038,11 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Concatenation([26, 41]), - new \Phplrt\Parser\Grammar\Concatenation([27, 29, 37]), - new \Phplrt\Parser\Grammar\Optional(28), + new \Phplrt\Parser\Grammar\Concatenation([27, 42]), + new \Phplrt\Parser\Grammar\Concatenation([28, 30, 38]), + new \Phplrt\Parser\Grammar\Optional(29), new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, true), - new \Phplrt\Parser\Grammar\Alternation([30, 31, 32, 33, 34, 35, 36]), + new \Phplrt\Parser\Grammar\Alternation([31, 32, 33, 34, 35, 36, 37]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true), @@ -1989,154 +2050,152 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), - new \Phplrt\Parser\Grammar\Repetition(38, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([39, 40]), + new \Phplrt\Parser\Grammar\Repetition(39, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([40, 41]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), - new \Phplrt\Parser\Grammar\Concatenation([29]), - new \Phplrt\Parser\Grammar\Optional(42), - new \Phplrt\Parser\Grammar\Alternation([43, 53, 76, 88, 100, 48, 124]), - new \Phplrt\Parser\Grammar\Concatenation([44, 45]), + new \Phplrt\Parser\Grammar\Concatenation([30]), + new \Phplrt\Parser\Grammar\Optional(43), + new \Phplrt\Parser\Grammar\Alternation([44, 54, 77, 89, 101, 49, 125]), + new \Phplrt\Parser\Grammar\Concatenation([45, 46]), new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true), - new \Phplrt\Parser\Grammar\Alternation([46, 48]), - new \Phplrt\Parser\Grammar\Concatenation([29, 47]), - new \Phplrt\Parser\Grammar\Optional(48), - new \Phplrt\Parser\Grammar\Concatenation([49, 50, 52]), + new \Phplrt\Parser\Grammar\Alternation([47, 49]), + new \Phplrt\Parser\Grammar\Concatenation([30, 48]), + new \Phplrt\Parser\Grammar\Optional(49), + new \Phplrt\Parser\Grammar\Concatenation([50, 51, 53]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), - new \Phplrt\Parser\Grammar\Repetition(51, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([29, 49]), - new \Phplrt\Parser\Grammar\Optional(29), - new \Phplrt\Parser\Grammar\Concatenation([8, 54, 71, 72]), - new \Phplrt\Parser\Grammar\Optional(55), - new \Phplrt\Parser\Grammar\Concatenation([56, 67, 70]), - new \Phplrt\Parser\Grammar\Concatenation([0, 57, 59, 61, 65]), - new \Phplrt\Parser\Grammar\Optional(58), + new \Phplrt\Parser\Grammar\Repetition(52, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([30, 50]), + new \Phplrt\Parser\Grammar\Optional(30), + new \Phplrt\Parser\Grammar\Concatenation([9, 55, 72, 73]), + new \Phplrt\Parser\Grammar\Optional(56), + new \Phplrt\Parser\Grammar\Concatenation([57, 68, 71]), + new \Phplrt\Parser\Grammar\Concatenation([0, 58, 60, 62, 66]), + new \Phplrt\Parser\Grammar\Optional(59), new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true), - new \Phplrt\Parser\Grammar\Optional(60), + new \Phplrt\Parser\Grammar\Optional(61), new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true), - new \Phplrt\Parser\Grammar\Optional(62), - new \Phplrt\Parser\Grammar\Alternation([63, 64]), + new \Phplrt\Parser\Grammar\Optional(63), + new \Phplrt\Parser\Grammar\Alternation([64, 65]), new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), - new \Phplrt\Parser\Grammar\Optional(66), + new \Phplrt\Parser\Grammar\Optional(67), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true), - new \Phplrt\Parser\Grammar\Repetition(68, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([69, 56]), + new \Phplrt\Parser\Grammar\Repetition(69, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 57]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false), - new \Phplrt\Parser\Grammar\Optional(69), + new \Phplrt\Parser\Grammar\Optional(70), new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), - new \Phplrt\Parser\Grammar\Optional(73), - new \Phplrt\Parser\Grammar\Concatenation([74, 75]), + new \Phplrt\Parser\Grammar\Optional(74), + new \Phplrt\Parser\Grammar\Concatenation([75, 76]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Concatenation([77, 87]), - new \Phplrt\Parser\Grammar\Concatenation([78, 79, 84, 70, 86]), + new \Phplrt\Parser\Grammar\Concatenation([78, 88]), + new \Phplrt\Parser\Grammar\Concatenation([79, 80, 85, 71, 87]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false), - new \Phplrt\Parser\Grammar\Concatenation([80]), - new \Phplrt\Parser\Grammar\Alternation([81, 83]), - new \Phplrt\Parser\Grammar\Concatenation([82, 83]), + new \Phplrt\Parser\Grammar\Concatenation([81]), + new \Phplrt\Parser\Grammar\Alternation([82, 84]), + new \Phplrt\Parser\Grammar\Concatenation([83, 84]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), - new \Phplrt\Parser\Grammar\Alternation([49, 0]), - new \Phplrt\Parser\Grammar\Repetition(85, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([69, 80]), + new \Phplrt\Parser\Grammar\Alternation([50, 0]), + new \Phplrt\Parser\Grammar\Repetition(86, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 81]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Predicate(8, false), - new \Phplrt\Parser\Grammar\Concatenation([89, 53]), - new \Phplrt\Parser\Grammar\Concatenation([78, 90, 97, 70, 99]), - new \Phplrt\Parser\Grammar\Concatenation([91]), - new \Phplrt\Parser\Grammar\Concatenation([29, 92]), - new \Phplrt\Parser\Grammar\Repetition(93, 0, \INF), - new \Phplrt\Parser\Grammar\Alternation([94, 95]), - new \Phplrt\Parser\Grammar\Concatenation([29, 0]), - new \Phplrt\Parser\Grammar\Concatenation([96, 0]), + new \Phplrt\Parser\Grammar\Predicate(9, false), + new \Phplrt\Parser\Grammar\Concatenation([90, 54]), + new \Phplrt\Parser\Grammar\Concatenation([79, 91, 98, 71, 100]), + new \Phplrt\Parser\Grammar\Concatenation([92]), + new \Phplrt\Parser\Grammar\Concatenation([30, 93]), + new \Phplrt\Parser\Grammar\Repetition(94, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([95, 96]), + new \Phplrt\Parser\Grammar\Concatenation([30, 0]), + new \Phplrt\Parser\Grammar\Concatenation([97, 0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, false), - new \Phplrt\Parser\Grammar\Repetition(98, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([69, 91]), + new \Phplrt\Parser\Grammar\Repetition(99, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 92]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Concatenation([101, 102, 70, 123]), + new \Phplrt\Parser\Grammar\Concatenation([102, 103, 71, 124]), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false), - new \Phplrt\Parser\Grammar\Alternation([103, 122]), - new \Phplrt\Parser\Grammar\Concatenation([104, 118]), - new \Phplrt\Parser\Grammar\Concatenation([105, 116]), - new \Phplrt\Parser\Grammar\Alternation([106, 115]), - new \Phplrt\Parser\Grammar\Concatenation([107, 112, 74, 114]), - new \Phplrt\Parser\Grammar\Alternation([108, 6]), - new \Phplrt\Parser\Grammar\Concatenation([29, 109]), - new \Phplrt\Parser\Grammar\Predicate(110, true), - new \Phplrt\Parser\Grammar\Concatenation([111, 74]), - new \Phplrt\Parser\Grammar\Optional(3), - new \Phplrt\Parser\Grammar\Optional(113), + new \Phplrt\Parser\Grammar\Alternation([104, 123]), + new \Phplrt\Parser\Grammar\Concatenation([105, 119]), + new \Phplrt\Parser\Grammar\Concatenation([106, 117]), + new \Phplrt\Parser\Grammar\Alternation([107, 116]), + new \Phplrt\Parser\Grammar\Concatenation([108, 113, 75, 115]), + new \Phplrt\Parser\Grammar\Alternation([109, 7]), + new \Phplrt\Parser\Grammar\Concatenation([30, 110]), + new \Phplrt\Parser\Grammar\Predicate(111, true), + new \Phplrt\Parser\Grammar\Concatenation([112, 75]), + new \Phplrt\Parser\Grammar\Optional(4), + new \Phplrt\Parser\Grammar\Optional(114), new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true), new \Phplrt\Parser\Grammar\Concatenation([0]), new \Phplrt\Parser\Grammar\Concatenation([0]), - new \Phplrt\Parser\Grammar\Repetition(117, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([69, 105]), - new \Phplrt\Parser\Grammar\Optional(119), - new \Phplrt\Parser\Grammar\Concatenation([69, 120]), - new \Phplrt\Parser\Grammar\Concatenation([60, 121]), - new \Phplrt\Parser\Grammar\Optional(77), + new \Phplrt\Parser\Grammar\Repetition(118, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 106]), new \Phplrt\Parser\Grammar\Optional(120), + new \Phplrt\Parser\Grammar\Concatenation([70, 121]), + new \Phplrt\Parser\Grammar\Concatenation([61, 122]), + new \Phplrt\Parser\Grammar\Optional(78), + new \Phplrt\Parser\Grammar\Optional(121), new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false), - new \Phplrt\Parser\Grammar\Concatenation([39, 48]), - new \Phplrt\Parser\Grammar\Concatenation([49, 29, 47]), - new \Phplrt\Parser\Grammar\Concatenation([62, 127]), - new \Phplrt\Parser\Grammar\Predicate(128, true), - new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, false), - new \Phplrt\Parser\Grammar\Repetition(130, 0, \INF), - new \Phplrt\Parser\Grammar\Concatenation([131, 132, 133]), + new \Phplrt\Parser\Grammar\Concatenation([40, 49]), + new \Phplrt\Parser\Grammar\Concatenation([50, 30, 48]), + new \Phplrt\Parser\Grammar\Repetition(128, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([129, 130, 131]), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false), new \Phplrt\Parser\Grammar\Optional(0), new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false), - new \Phplrt\Parser\Grammar\Repetition(135, 0, \INF), - new \Phplrt\Parser\Grammar\Alternation([136, 139]), - new \Phplrt\Parser\Grammar\Concatenation([137, 138]), + new \Phplrt\Parser\Grammar\Repetition(133, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([134, 137]), + new \Phplrt\Parser\Grammar\Concatenation([135, 136]), new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, true), - new \Phplrt\Parser\Grammar\Concatenation([1]), - new \Phplrt\Parser\Grammar\Concatenation([58, 140]), - new \Phplrt\Parser\Grammar\Concatenation([1]), - new \Phplrt\Parser\Grammar\Optional(142), - new \Phplrt\Parser\Grammar\Concatenation([143, 145, 3, 146, 147, 148]), - new \Phplrt\Parser\Grammar\Concatenation([32, 144]), - new \Phplrt\Parser\Grammar\Optional(33), - new \Phplrt\Parser\Grammar\Alternation([0, 62]), + new \Phplrt\Parser\Grammar\Concatenation([2]), + new \Phplrt\Parser\Grammar\Concatenation([59, 138]), + new \Phplrt\Parser\Grammar\Concatenation([2]), + new \Phplrt\Parser\Grammar\Optional(140), + new \Phplrt\Parser\Grammar\Concatenation([141, 143, 4, 144, 145, 146]), + new \Phplrt\Parser\Grammar\Concatenation([33, 142]), + new \Phplrt\Parser\Grammar\Optional(34), + new \Phplrt\Parser\Grammar\Alternation([0, 63]), new \Phplrt\Parser\Grammar\Concatenation([0]), new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Concatenation([63, 140]), ], initial: 0, reducers: [ 0 => $this->reduceType(...), - 2 => self::reduceNullableType(...), - 5 => $this->reduceListOrOffsetType(...), - 10 => self::reduceThisType(...), - 11 => $this->reduceLiteralType(...), - 14 => self::reduceDoubleQuotedStringLiteralType(...), - 15 => self::reduceSingleQuotedStringLiteralType(...), - 16 => self::reduceFloatLiteralType(...), - 17 => self::reduceIntLiteralType(...), - 22 => self::reduceTrueLiteralType(...), - 23 => self::reduceFalseLiteralType(...), - 24 => self::reduceNullLiteralType(...), - 25 => $this->reduceNamedType(...), - 26 => self::reduceName(...), - 29 => self::reduceIdentifier(...), - 49 => self::reduceWildcardType(...), - 54 => self::reduceCallableParameters(...), - 56 => self::reduceCallableParameter(...), - 62 => self::reduceVariable(...), - 77 => $this->reduceTemplateArguments(...), - 80 => $this->reduceTemplateArgument(...), - 82 => self::reduceIdentifierWithExtraSpace(...), - 89 => $this->reduceTemplateParameters(...), - 91 => self::reduceTemplateParameter(...), - 94 => self::reduceTemplateBoundEdge(...), - 100 => $this->reduceShapeFields(...), - 104 => self::reduceShapeFieldsList(...), - 106 => self::reduceExplicitField(...), - 115 => self::reduceImplicitField(...), - 124 => self::reduceNamespacedMask(...), - 125 => self::reduceLeadingMaskType(...), - 130 => self::reduceListOrOffsetSuffix(...), - 143 => self::reduceTernaryExpressionOperator(...), + 3 => self::reduceNullableType(...), + 6 => $this->reduceListOrOffsetType(...), + 11 => self::reduceThisType(...), + 12 => $this->reduceLiteralType(...), + 15 => self::reduceDoubleQuotedStringLiteralType(...), + 16 => self::reduceSingleQuotedStringLiteralType(...), + 17 => self::reduceFloatLiteralType(...), + 18 => self::reduceIntLiteralType(...), + 23 => self::reduceTrueLiteralType(...), + 24 => self::reduceFalseLiteralType(...), + 25 => self::reduceNullLiteralType(...), + 26 => $this->reduceNamedType(...), + 27 => self::reduceName(...), + 30 => self::reduceIdentifier(...), + 50 => self::reduceWildcardType(...), + 55 => self::reduceCallableParameters(...), + 57 => self::reduceCallableParameter(...), + 63 => self::reduceVariable(...), + 78 => $this->reduceTemplateArguments(...), + 81 => $this->reduceTemplateArgument(...), + 83 => self::reduceIdentifierWithExtraSpace(...), + 90 => $this->reduceTemplateParameters(...), + 92 => self::reduceTemplateParameter(...), + 95 => self::reduceTemplateBoundEdge(...), + 101 => $this->reduceShapeFields(...), + 105 => self::reduceShapeFieldsList(...), + 107 => self::reduceExplicitField(...), + 116 => self::reduceImplicitField(...), + 125 => self::reduceNamespacedMask(...), + 126 => self::reduceLeadingMaskType(...), + 128 => self::reduceListOrOffsetSuffix(...), + 141 => self::reduceTernaryExpressionOperator(...), ], expectations: self::PARSER_EXPECTATIONS, messages: self::PARSER_MESSAGES, @@ -2302,7 +2361,6 @@ private function reduceLiteralType(\Phplrt\Parser\Context $ctx, mixed $children) if ($this->features->literals === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset); } - return $children; } @@ -2423,15 +2481,15 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): case $suffix === null: return new Type\NamedTypeNode($children[0], null, null, $offset); - // Some\Any + // Some\Any case $suffix instanceof Type\Template\TemplateArgumentListNode: return new Type\NamedTypeNode($children[0], $suffix, null, $offset); - // Some\Any{name: T, ...} + // Some\Any{name: T, ...} case $suffix instanceof Type\Shape\FieldsListNode: return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); - // Some\Any(T, U): V + // Some\Any(T, U): V case $suffix instanceof Type\Callable\CallableParameterListNode: if ($this->features->callables === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); @@ -2439,7 +2497,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); - // Some\Any(T): U + // Some\Any(T): U case $suffix instanceof Type\Template\TemplateParameterListNode: if ($this->features->callables === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); @@ -2447,7 +2505,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); - // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* + // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* default: if (\count($children) === 3 && $children[2] instanceof Type\Identifier) { return new Type\ClassConstNode($children[0], $children[2], $offset); diff --git a/libs/parser/tests/Syntax/VariableTest.php b/libs/parser/tests/Syntax/VariableTest.php index 99cd224..2b35b54 100644 --- a/libs/parser/tests/Syntax/VariableTest.php +++ b/libs/parser/tests/Syntax/VariableTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use TypeLang\Parser\Exception\UnexpectedTokenException; use TypeLang\Type\CallableTypeNode; use TypeLang\Type\TernaryExpressionNode; use TypeLang\Type\ThisNode; @@ -162,6 +163,40 @@ public function testAVariableIsNoTemplateArgument(): void $this->parse('Some<$value>'); } + /** + * A variable carries no type the rest of the reading could go on with, + * so it stands nowhere a type is wrapped, joined or indexed. The subject + * of a condition is the one place a variable is read. + * + * @return iterable + */ + public static function variableBesideATypeDataProvider(): iterable + { + yield 'a nullable' => ['?$value is B ? C : D']; + yield 'a union' => ['int|$value is B ? C : D']; + yield 'an intersection' => ['int&$value is B ? C : D']; + yield 'a union it opens' => ['$value|int is B ? C : D']; + yield 'a list' => ['$value[] is B ? C : D']; + yield 'an offset' => ['$value[0] is B ? C : D']; + yield 'a group' => ['($value)']; + yield 'a shape value' => ['array{a: $value}']; + yield 'a return type' => ['callable(): $value']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('variableBesideATypeDataProvider')] + public function testAVariableStandsInNoTypePosition(string $type): void + { + // The token is reported the way any other unexpected one is, rather + // than raised as an error of the parser itself. + $this->expectException(UnexpectedTokenException::class); + + $this->parse($type); + } + public function testThisCarriesNoClassConstant(): void { $this->expectParsingException('unexpected "::"'); From 0c1e4ee85c8e2856254c61da49645534181867a5 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 17:00:35 +0300 Subject: [PATCH 108/119] Upgrade specs --- .../Appendix B -- Grammar Summary.md | 106 +++++++-- .../Section 2 -- Lexical Grammar.md | 202 +++++++++++++----- docs/Specification/Section 3 -- Types.md | 164 +++++++++----- .../Section 4 -- Structural Types.md | 118 +++++++++- 4 files changed, 451 insertions(+), 139 deletions(-) diff --git a/docs/Specification/Appendix B -- Grammar Summary.md b/docs/Specification/Appendix B -- Grammar Summary.md index 42248ac..db3afdb 100644 --- a/docs/Specification/Appendix B -- Grammar Summary.md +++ b/docs/Specification/Appendix B -- Grammar Summary.md @@ -16,7 +16,7 @@ Letter :: one of - `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z` - `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m` - `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z` -- "Any byte from U+0080 to U+00FF" +- "Any byte from 0x80 to 0xFF" Digit :: one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -27,7 +27,14 @@ Ignored :: - Whitespace - Comment -Whitespace :: "Any Unicode whitespace character" +Whitespace :: one of + +- "Horizontal Tab (U+0009)" +- "Line Feed (U+000A)" +- "Vertical Tab (U+000B)" +- "Form Feed (U+000C)" +- "Carriage Return (U+000D)" +- "Space (U+0020)" LineTerminator :: one of "New Line (U+000A)" "Carriage Return (U+000D)" @@ -63,7 +70,7 @@ Punctuator :: one of - `?` `|` `&` `*` `,` `:` `=` - `(` `)` `[` `]` `{` `}` -- `<` `>` `<=` `>=` +- `<` `>` - `::` `\` `...` NameToken :: NameStart NameContinue\* [lookahead != NameContinue] @@ -80,11 +87,13 @@ NameContinue :: - `_` - `-` -ReservedWord :: one of `true` `false` `null` `is` +ReservedWord :: one of `true` `false` `null` `is` `not` + +Variable :: `$` NameStart VariableContinue\* -Variable :: `$` NameStart NameContinue\* +VariableContinue :: NameContinue but not `-` -ThisVariable :: `$this` [lookahead != NameContinue] +ThisVariable :: `$this` [lookahead != VariableContinue] **Literal Tokens** @@ -94,8 +103,12 @@ NullLiteral :: `null` [lookahead != NameContinue] NegativeSign :: `-` +Sign :: one of `-` `+` + DigitSeparator :: `_` +Digits :: Digit (DigitSeparator? Digit)\* + IntLiteral :: - BinaryIntLiteral @@ -103,24 +116,38 @@ IntLiteral :: - HexIntLiteral - DecimalIntLiteral -DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\* +DecimalIntLiteral :: -BinaryIntLiteral :: NegativeSign? `0` BinaryIndicator BinaryDigit (BinaryDigit | DigitSeparator)\* +- Sign? NonZeroDigit (DigitSeparator? Digit)\* +- Sign? `0` + +NonZeroDigit :: one of `1` `2` `3` `4` `5` `6` `7` `8` `9` + +BinaryIntLiteral :: Sign? `0` BinaryIndicator BinaryDigits BinaryIndicator :: one of `b` `B` +BinaryDigits :: BinaryDigit (DigitSeparator? BinaryDigit)\* + BinaryDigit :: one of `0` `1` -OctalIntLiteral :: NegativeSign? `0` OctalIndicator OctalDigit (OctalDigit | DigitSeparator)\* +OctalIntLiteral :: + +- Sign? `0` OctalIndicator OctalDigits +- Sign? `0` (DigitSeparator? OctalDigit)+ OctalIndicator :: one of `o` `O` +OctalDigits :: OctalDigit (DigitSeparator? OctalDigit)\* + OctalDigit :: one of `0` `1` `2` `3` `4` `5` `6` `7` -HexIntLiteral :: NegativeSign? `0` HexIndicator HexDigit (HexDigit | DigitSeparator)\* +HexIntLiteral :: Sign? `0` HexIndicator HexDigits HexIndicator :: one of `x` `X` +HexDigits :: HexDigit (DigitSeparator? HexDigit)\* + HexDigit :: one of - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -133,13 +160,13 @@ FloatLiteral :: - TrailingFloatLiteral - ExponentFloatLiteral -LeadingFloatLiteral :: NegativeSign? Digit+ `.` Digit\* ExponentPart? +LeadingFloatLiteral :: Sign? Digits `.` Digits? ExponentPart? -TrailingFloatLiteral :: NegativeSign? Digit\* `.` Digit+ ExponentPart? +TrailingFloatLiteral :: Sign? `.` Digits ExponentPart? -ExponentFloatLiteral :: NegativeSign? Digit+ ExponentPart +ExponentFloatLiteral :: Sign? Digits ExponentPart -ExponentPart :: ExponentIndicator NegativeSign? Digit+ +ExponentPart :: ExponentIndicator Sign? Digits ExponentIndicator :: one of `e` `E` @@ -176,6 +203,8 @@ UnicodeEscape :: `u` `{` HexDigit+ `}` **Types** +Document : Type + Type : Expression Expression : ConditionalType @@ -192,7 +221,7 @@ ConditionalOperand : - Type - Variable -ConditionalOperator : one of `is` `is not` `>=` `<=` `<` `>` +ConditionalOperator : one of `is` `is not` **Logical Types** @@ -251,11 +280,13 @@ NamedType : Name (TemplateArguments | ShapeFields)? TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>` -TemplateArgument : TemplateArgumentHint | TemplateArgumentType +TemplateArgument : TemplateArgumentHint | TemplateArgumentValue + +TemplateArgumentHint : NameToken TemplateArgumentValue -TemplateArgumentHint : Identifier Type +TemplateArgumentValue : Wildcard | Type -TemplateArgumentType : Type +Wildcard : `*` **Literal and Constant Types** @@ -273,9 +304,23 @@ ClassConstant : Name `::` Identifier ConstantMask : -- Name `*` -- Name `::` Identifier `*` -- Name `::` `*` +- GlobalConstantMask +- ClassConstantMask + +GlobalConstantMask : + +- Name MaskTail +- Name `\` MaskTail +- LeadingMask + +ClassConstantMask : + +- Name `::` Identifier MaskTail? +- Name `::` MaskTail + +MaskTail : Wildcard (Identifier Wildcard)\* Identifier? + +LeadingMask : Wildcard Identifier MaskTail? **Shape Types** @@ -308,7 +353,24 @@ UnsealedShape : `...` TemplateArguments? **Callable Types** -CallableType : Name `(` CallableParameters? `)` CallableReturnType? +CallableType : Name TemplateParameters? `(` CallableParameters? `)` CallableReturnType? + +TemplateParameters : `<` TemplateParameter (`,` TemplateParameter)\* `,`? `>` + +TemplateParameter : Identifier TemplateBound\* TemplateDefault? + +TemplateBound : + +- UpperBound +- LowerBound + +UpperBound : UpperBoundOperator Type + +UpperBoundOperator : one of `of` `as` + +LowerBound : `super` Type + +TemplateDefault : `=` Type CallableParameters : CallableParameter (`,` CallableParameter)\* `,`? diff --git a/docs/Specification/Section 2 -- Lexical Grammar.md b/docs/Specification/Section 2 -- Lexical Grammar.md index 73c6454..f9d3506 100644 --- a/docs/Specification/Section 2 -- Lexical Grammar.md +++ b/docs/Specification/Section 2 -- Lexical Grammar.md @@ -56,7 +56,7 @@ Letter :: one of - `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z` - `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m` - `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z` -- "Any byte from U+0080 to U+00FF" +- "Any byte from 0x80 to 0xFF" Digit :: one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -72,23 +72,36 @@ otherwise insignificant. Any amount of {Ignored} MAY appear before and after every lexical {Token}. Note: Although {Ignored} between two tokens is never itself significant, the -_presence_ of separating whitespace is what distinguishes a {NameToken} that -is immediately followed by another token from a {NameToken} that introduces a -template argument hint (see +_presence_ of {Whitespace} standing directly behind a {NameToken} is what +distinguishes a {NameToken} that is immediately followed by another token +from a {NameToken} that introduces a template argument hint (see [Template Argument Hints](#sec-Template-Argument-Hints)). This is the only -place in the grammar where the presence of separating whitespace is -observable to the syntactic grammar. +place in the grammar where {Ignored} is observable to the syntactic grammar, +and the one place a {Comment} does not stand for the whitespace it is written +among: a comment MAY follow that whitespace, but MUST NOT stand in its stead. ### White Space -Whitespace :: "Any Unicode whitespace character" +Whitespace :: one of -Whitespace separates tokens and improves the legibility of the source text. It -includes, at minimum, the space (U+0020), horizontal tab (U+0009), line feed -(U+000A) and carriage return (U+000D) characters. TypeLang does not -distinguish between horizontal whitespace and line terminators, except within -the multi-word `is not` operator (see -[Conditional Types](#sec-Conditional-Types)). +- "Horizontal Tab (U+0009)" +- "Line Feed (U+000A)" +- "Vertical Tab (U+000B)" +- "Form Feed (U+000C)" +- "Carriage Return (U+000D)" +- "Space (U+0020)" + +Whitespace separates tokens and improves the legibility of the source text. +TypeLang does not distinguish between horizontal whitespace and line +terminators, and a {Comment} stands wherever whitespace stands, save for the +one place the presence of the whitespace itself is read (see +[Ignored Tokens](#sec-Ignored-Tokens)). + +Note: The six characters above are the whole of it. A source text is read as +a sequence of bytes (see [Source Text](#sec-Source-Text)), so a character that +parts words in a script of its own — a no-break space (U+00A0), say — is no +whitespace here: every byte it is written of falls in the range a {Letter} +covers, and it is read as a part of the name it stands in. ### Comments @@ -140,12 +153,12 @@ Punctuator :: one of - `?` `|` `&` `*` `,` `:` `=` - `(` `)` `[` `]` `{` `}` -- `<` `>` `<=` `>=` +- `<` `>` - `::` `\` `...` TypeLang documents use punctuation to describe structure. Several punctuators -share a leading character (for example, `<` and `<=`; `:` and `::`; `.` in -`...`); in every such case, the longest matching punctuator is taken. +share a leading character (for example, `:` and `::`; `.` in `...`); in every +such case, the longest matching punctuator is taken. ### Names @@ -173,6 +186,14 @@ the [PHP identifier grammar](https://www.php.net/manual/en/language.variables.ba is that the dash character is additionally permitted in any non-leading position. +A source text is read as a sequence of bytes rather than of codepoints, and +every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a +{Letter} covers. A name MAY therefore be written in any script. + +```typescript +Проект\Тип +``` + A {NameToken} is always the longest possible valid sequence; it MUST NOT be followed by a {NameContinue} character. @@ -207,41 +228,57 @@ ParseException: Syntax error, unexpected "type" ### Reserved Words -ReservedWord :: one of `true` `false` `null` `is` +ReservedWord :: one of `true` `false` `null` `is` `not` The words `true`, `false` and `null` are _literal_ keywords (see -[Literal Tokens](#sec-Literal-Tokens)), and `is` is the conditional operator -keyword (see [Conditional Types](#sec-Conditional-Types)). All four are -matched case-insensitively, and only when not immediately followed by a -{NameContinue} character. +[Literal Tokens](#sec-Literal-Tokens)), and `is` and `not` are the +conditional operator keywords (see +[Conditional Types](#sec-Conditional-Types)). Each is matched only when not +immediately followed by a {NameContinue} character. The three literal +keywords are matched case-insensitively, while `is` and `not` are matched +the one way they are spelled. + +Wherever the source contains one of these words, it is scanned as the +corresponding literal or operator token and MUST NOT be scanned as a +{NameToken}. It remains an {Identifier} all the same (see +[Names and Namespaces](#sec-Names-and-Namespaces)), so a reserved word is +shadowed rather than forbidden: it stands as a name wherever the token it is +scanned as denotes no type of its own. + +A literal keyword standing alone, in a position where a type is expected, is +read as the {LiteralType} it denotes and not as the {NamedType} it would +otherwise be (see [Primary Types](#sec-Primary-Types)): -When the source contains one of these words standing alone, in a position -where a type is expected, it is scanned as the corresponding literal or -operator token and MUST NOT be scanned as a {NameToken}. Consequently, a bare -reserved word cannot be used as a type name. +```typescript +TrUe +``` -**Counter-example.** A bare reserved word is scanned as a literal, not a type -name. +Behind a namespace separator or another segment, where no literal is +expected, the same word is a name segment. This makes `\true` a reference to +a type literally named `true`, distinct from the `true` literal: -```typescript counter-example -TrUe +```typescript +\true ``` -A reserved word MAY, however, appear as an {Identifier} _inside_ a qualified -{Name} — that is, when it is preceded by a namespace separator or another -identifier (see [Names and Namespaces](#sec-Names-and-Namespaces)). For -example, `\true` references a type literally named `true`, whereas the bare -`true` is the boolean literal. +The conditional operator keywords denote no type of their own, so nothing +shadows them, and a bare `is` or `not` is an ordinary named type: + +```typescript +not +``` ### Variable -Variable :: `$` NameStart NameContinue\* +Variable :: `$` NameStart VariableContinue\* + +VariableContinue :: NameContinue but not `-` -ThisVariable :: `$this` [lookahead != NameContinue] +ThisVariable :: `$this` [lookahead != VariableContinue] A {Variable} token begins with a dollar sign (`$`) followed by a sequence -matching the body of a {NameToken}. Variables are used to name callable -parameters (see [Callable Types](#sec-Callable-Types)) and as operands in +matching the body of a {NameToken}, save for the dash a variable cannot +carry. Variables are used to name callable parameters (see [Callable Types](#sec-Callable-Types)) and as operands in conditional types (see [Conditional Types](#sec-Conditional-Types)). The special variable `$this` is recognised as a distinct token and @@ -283,17 +320,30 @@ IntLiteral :: NegativeSign :: `-` +Sign :: one of `-` `+` + DigitSeparator :: `_` An integer literal denotes a value of the PHP `int` type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a -{NegativeSign}. Within the digits of any integer literal, underscores -({DigitSeparator}) MAY appear freely as visual separators; they carry no -meaning and do not affect the denoted value. +{Sign}. A {NegativeSign} makes the denoted value negative; a leading `+` denotes +the same value the absence of a {Sign} does. Underscores ({DigitSeparator}) MAY +appear as visual separators; they carry no meaning and do not affect the denoted +value. A {DigitSeparator} MUST stand between two digits, and therefore may +neither lead a run of digits, nor trail one, nor stand beside another +{DigitSeparator}. + +Digits :: Digit (DigitSeparator? Digit)\* + +**Decimal.** A decimal literal other than a lone `0` MUST NOT begin with a +zero, which is the mark of an {OctalIntLiteral}. -**Decimal.** +DecimalIntLiteral :: -DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\* +- Sign? NonZeroDigit (DigitSeparator? Digit)\* +- Sign? `0` + +NonZeroDigit :: one of `1` `2` `3` `4` `5` `6` `7` `8` `9` ```typescript 1_000_000 @@ -301,35 +351,61 @@ DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\* **Binary.** Prefixed with `0b` or `0B`; digits are `0` and `1` only. -BinaryIntLiteral :: NegativeSign? `0` BinaryIndicator BinaryDigit (BinaryDigit | DigitSeparator)\* +BinaryIntLiteral :: Sign? `0` BinaryIndicator BinaryDigits BinaryIndicator :: one of `b` `B` +BinaryDigits :: BinaryDigit (DigitSeparator? BinaryDigit)\* + BinaryDigit :: one of `0` `1` ```typescript 0b1010_1101 ``` -**Octal.** Prefixed with `0o` or `0O`; digits are `0` through `7` only. +**Octal.** Prefixed with `0o` or `0O` or, as octal has been written since long +before that prefix, with a leading `0` alone; digits are `0` through `7` only. +A `0123` denotes the same value a `0o123` does. -OctalIntLiteral :: NegativeSign? `0` OctalIndicator OctalDigit (OctalDigit | DigitSeparator)\* +OctalIntLiteral :: + +- Sign? `0` OctalIndicator OctalDigits +- Sign? `0` (DigitSeparator? OctalDigit)+ OctalIndicator :: one of `o` `O` +OctalDigits :: OctalDigit (DigitSeparator? OctalDigit)\* + OctalDigit :: one of `0` `1` `2` `3` `4` `5` `6` `7` ```typescript 0o42 ``` +```typescript +04_23 +``` + +**Counter-example.** A leading zero makes a literal an octal, so a digit +outside the octal radix may not follow one. + +```typescript counter-example +08 +``` + +``` +ParseException: Syntax error, unexpected "8" +``` + **Hexadecimal.** Prefixed with `0x` or `0X`; digits are `0` through `9` and `a` through `f`, in either case. -HexIntLiteral :: NegativeSign? `0` HexIndicator HexDigit (HexDigit | DigitSeparator)\* +HexIntLiteral :: Sign? `0` HexIndicator HexDigits HexIndicator :: one of `x` `X` +HexDigits :: HexDigit (DigitSeparator? HexDigit)\* + HexDigit :: one of - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -352,9 +428,9 @@ ParseException: Syntax error, unexpected "42" **Static Semantics.** -A {DecimalIntLiteral} whose first digit is `0` and which is longer than a -single character is interpreted in the _legacy octal_ radix (base 8), for -compatibility with historical PHP source. Thus, `042` denotes the value 34. +An {OctalIntLiteral} written without an {OctalIndicator} is the _legacy octal_ +spelling kept for compatibility with historical PHP source. It is read in the +same radix (base 8) the prefixed spelling is, so `042` denotes the value 34. A conforming implementation MUST retain the original (raw) lexeme of every integer literal. When the denoted value exceeds the range representable by the @@ -370,19 +446,19 @@ FloatLiteral :: - TrailingFloatLiteral - ExponentFloatLiteral -LeadingFloatLiteral :: NegativeSign? Digit+ `.` Digit\* ExponentPart? +LeadingFloatLiteral :: Sign? Digits `.` Digits? ExponentPart? -TrailingFloatLiteral :: NegativeSign? Digit\* `.` Digit+ ExponentPart? +TrailingFloatLiteral :: Sign? `.` Digits ExponentPart? -ExponentFloatLiteral :: NegativeSign? Digit+ ExponentPart +ExponentFloatLiteral :: Sign? Digits ExponentPart -ExponentPart :: ExponentIndicator NegativeSign? Digit+ +ExponentPart :: ExponentIndicator Sign? Digits ExponentIndicator :: one of `e` `E` A floating-point literal denotes a value of the PHP `float` type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed -with a {NegativeSign}. +with a {Sign}. Either the leading run of digits (before the decimal point) or the trailing run (after it) MAY be omitted, but not both: @@ -399,13 +475,25 @@ run (after it) MAY be omitted, but not both: 1. ``` -Scientific notation uses the case-insensitive `e` indicator followed by an -optionally negative decimal exponent: +Scientific notation uses the case-insensitive `e` indicator followed by a +decimal exponent, which takes a {Sign} of its own: ```typescript 10e-2 ``` +```typescript +10e+2 +``` + +Every run of digits a float is written of accepts the {DigitSeparator} under +the same rule an integer does, and, unlike a {DecimalIntLiteral}, MAY begin +with a zero: + +```typescript +2_3.4_5e-6_7 +``` + **Counter-example.** A lone decimal point is not a valid float. ```typescript counter-example diff --git a/docs/Specification/Section 3 -- Types.md b/docs/Specification/Section 3 -- Types.md index c40ba32..dabbfe8 100644 --- a/docs/Specification/Section 3 -- Types.md +++ b/docs/Specification/Section 3 -- Types.md @@ -47,7 +47,7 @@ ConditionalOperand : - Type - Variable -ConditionalOperator : one of `is` `is not` `>=` `<=` `<` `>` +ConditionalOperator : one of `is` `is not` A _conditional type_ (also called a ternary type) selects between two types based on a comparison between a _subject_ and a _comparand_. It is written as @@ -58,9 +58,9 @@ condition holds, a `:`, and the type chosen otherwise. T is string ? int : bool ``` -The negative-equality operator is spelled `is not`. At the lexical level, the -two words MUST be separated only by horizontal whitespace and are scanned as a -single token; no line terminator may appear between them. +The negative-equality operator is spelled `is not`. The two words are separate +tokens, so whitespace and comments MAY stand between them the way they MAY +stand between any other two tokens. ```typescript T is not string ? int : bool @@ -77,11 +77,6 @@ $value is array ? non-empty-array : null array is $value ? non-empty-array : null ``` -In addition to `is` and `is not`, the relational operators `<`, `>`, `<=` and -`>=` MAY be used as the condition operator. This is an extension beyond what -PHPStan and Psalm accept; see -[Relationship to Other Tools](#sec-Relationship-to-Other-Tools). - Note: A bare {Variable} is only a valid type when it carries a {ConditionalSuffix}. A {Variable} MUST NOT otherwise stand alone as a type; the only variable-like token that may appear as a primary type on its own is @@ -189,19 +184,16 @@ User[] User[][] ``` -**Counter-example.** The legacy list syntax does not accept a key type between -the brackets; for that, use [offset access](#sec-Offset-Access) or the modern -`array<…>` generic. +The legacy list syntax accepts no key type between the brackets; for that, +use [offset access](#sec-Offset-Access) or the modern `array<…>` generic. A +`User[int]` is a well-formed document all the same — an _offset access_ type +(see below) rather than a list, since `int` is a valid {Type}. It is the +absence of a type between the brackets that selects the list reading: -```typescript counter-example +```typescript User[int] ``` -This document, however, parses `User[int]` successfully — as an -_offset access_ type (see below) rather than a list — because `int` is a -valid {Type}. It is the absence of a type between the brackets that selects -the list interpretation. - ### Offset Access A pair of square brackets enclosing a {Type} forms an _offset access_ type, @@ -275,6 +267,14 @@ that immediately follows it: `*` selects a [generic or shape](#sec-Generic-Types) named type; any other following token (or the end of the document) leaves a plain named type. +Note: A bare {ReservedWord} is likewise both a {LiteralType} and, through +{Identifier}, a {Name} of a single segment (see +[Reserved Words](#sec-Reserved-Words)). Where one source could be read as +either, the {LiteralType} is the one taken, so a `true` denotes the boolean +literal and never a type named `true`. An `is` and a `not` denote no literal +of their own, and nothing is therefore left to shadow the named type each of +them is. + ## Names and Namespaces Name : @@ -307,13 +307,12 @@ Example\Name A separator MAY appear at the start of a name or between two segments, but MUST NOT appear at the end. -**Reserved words within names.** The reserved words `true`, `false`, `null` -and `is` (see [Reserved Words](#sec-Reserved-Words)) MAY appear as an -{Identifier} segment of a {Name}. In isolation, such a word is scanned as a -literal or operator and so cannot stand as a bare type name; but in a -qualified position — following a separator or another segment — it denotes a -name segment. This makes `\null` a reference to a type named `null`, distinct -from the `null` literal. +**Reserved words within names.** Every reserved word — `true`, `false`, +`null`, `is` and `not` (see [Reserved Words](#sec-Reserved-Words)) — MAY +appear as an {Identifier} segment of a {Name}. In a qualified position — +following a separator or another segment — such a word is a name segment and +nothing besides. This makes `\null` a reference to a type named `null`, +distinct from the `null` literal. **Counter-example.** A name cannot end with a separator. @@ -348,13 +347,16 @@ implementation and is outside the scope of this specification. TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>` -TemplateArgument : TemplateArgumentHint | TemplateArgumentType +TemplateArgument : TemplateArgumentHint | TemplateArgumentValue -TemplateArgumentType : Type +TemplateArgumentValue : Wildcard | Type + +Wildcard : `*` A _generic type_ supplies a named type with one or more _template arguments_, -each of which is itself a {Type}. Arguments are enclosed in angle brackets -(`<` and `>`) and separated by commas. A trailing comma is permitted. +each of which is itself a {Type} or a {Wildcard}. Arguments are enclosed in +angle brackets (`<` and `>`) and separated by commas. A trailing comma is +permitted. Validating the number of arguments, their bounds, and their nesting is the responsibility of the implementation, not of this grammar, which imposes no @@ -391,9 +393,9 @@ describes only use sites, and therefore only template arguments. ### Template Argument Hints -TemplateArgumentHint : Identifier Type +TemplateArgumentHint : NameToken TemplateArgumentValue -A template argument MAY carry a single leading _hint_: an {Identifier} placed +A template argument MAY carry a single leading _hint_: a {NameToken} placed before the argument's type. Hints are used by tooling — for example, to express [call-site variance](https://phpstan.org/blog/guide-to-call-site-generic-variance#call-site-variance) with identifiers such as `in`, `out`, `covariant`, or `contravariant`. @@ -402,31 +404,36 @@ with identifiers such as `in`, `out`, `covariant`, or `contravariant`. HashMap ``` -At the lexical level, the hint and the type it modifies MUST be separated by -whitespace; this separation is what distinguishes a hint from the start of -the argument's own type (see [Ignored Tokens](#sec-Ignored-Tokens)). Each -argument may carry at most one hint. +At the lexical level, {Whitespace} MUST stand directly behind the hint; this +separation is what distinguishes a hint from the start of the argument's own +type, and a {Comment} does not stand in its place (see +[Ignored Tokens](#sec-Ignored-Tokens)). Each argument may carry at most one +hint. -**Counter-example.** A hint must be a single valid identifier. +**Counter-example.** A hint is a single {NameToken}. A second one reads as +the bound of a [template parameter](#sec-Callable-Types), and a parameter +list belongs to a callable, so the statement is refused where the `(` it +would go on with is missing. ```typescript counter-example HashMap ``` ``` -ParseException: Syntax error, unexpected "Request" +ParseException: Syntax error, unexpected end of input ``` -**Counter-example.** A hint cannot be a reserved word: `is`, `true`, `false` -and `null` are always scanned as the corresponding operator or literal token -(see [Reserved Words](#sec-Reserved-Words)) and never as a hint {Identifier}. +**Counter-example.** A hint cannot be a reserved word: `true`, `false`, +`null`, `is` and `not` are always scanned as the corresponding literal or +operator token (see [Reserved Words](#sec-Reserved-Words)) and never as a +{NameToken}. ```typescript counter-example HashMap ``` ``` -ParseException: Syntax error, unexpected "Request" +ParseException: Syntax error, unexpected ">" ``` ## Literal Types @@ -527,49 +534,92 @@ ParseException: Syntax error, unexpected "\" ConstantMask : -- Name `*` -- Name `::` Identifier `*` -- Name `::` `*` +- GlobalConstantMask +- ClassConstantMask + +GlobalConstantMask : -A _constant mask_ denotes a family of constants whose names share a common -prefix. A mask MUST terminate with an asterisk (`*`). +- Name MaskTail +- Name `\` MaskTail +- LeadingMask -A global constant mask matches every global constant beginning with the -prefix: +ClassConstantMask : + +- Name `::` Identifier MaskTail? +- Name `::` MaskTail + +MaskTail : Wildcard (Identifier Wildcard)\* Identifier? + +LeadingMask : Wildcard Identifier MaskTail? + +A _constant mask_ denotes a family of constants whose names are written of the +same segments, in the same order, with a {Wildcard} standing wherever the name +is left unsaid. A mask MUST contain at least one {Wildcard}, and its segments +and wildcards alternate, so two wildcards in a row denote nothing a single one +does not. + +A global constant mask matches every global constant the segments describe: ```typescript JSON_* ``` -A class constant mask matches every constant of a class whose name begins -with the prefix; the prefix MAY be omitted entirely, in which case the mask -matches every constant of the class: +The name of a global constant MAY be left unsaid from its very beginning, and +the namespace it belongs to MAY be written in front of it. A {Name} standing in +front of a {MaskTail} carries the namespace and, in its last segment, the +leading segment of the mask; a {Name} separated from the mask by a `\` is a +namespace whole, and the mask then matches every constant that namespace holds: + +```typescript +*_SUFFIX +``` + +```typescript +Path\To\JSON_* +``` + +```typescript +Path\To\* +``` + +A class constant mask matches every constant of a class the segments describe; +the leading segment MAY be omitted entirely, in which case the mask matches +every constant of the class: ```typescript Path\To\ClassName::PREFIX_* ``` +```typescript +Path\To\ClassName::*_SUFFIX +``` + +```typescript +Path\To\ClassName::PREFIX_*_SUFFIX +``` + ```typescript Path\To\ClassName::* ``` -**Counter-example.** A global mask must have a prefix; a lone asterisk is not -a type. +**Counter-example.** A global mask must have at least one segment of a name, +whether it stands in front of a wildcard or behind it; a lone asterisk, which +would match every constant there is, is not a type. ```typescript counter-example * ``` ``` -ParseException: Syntax error, unexpected "*" +ParseException: Syntax error, unexpected end of input ``` -**Counter-example.** The asterisk must be the final character of the mask. +**Counter-example.** Two wildcards in a row are not a mask. ```typescript counter-example -Path\To\ClassName::PREFIX_*_SUFFIX +Path\To\ClassName::PREFIX_** ``` ``` -ParseException: Syntax error, unexpected "_SUFFIX" +ParseException: Syntax error, unexpected "*" ``` diff --git a/docs/Specification/Section 4 -- Structural Types.md b/docs/Specification/Section 4 -- Structural Types.md index b2173ca..f6de26f 100644 --- a/docs/Specification/Section 4 -- Structural Types.md +++ b/docs/Specification/Section 4 -- Structural Types.md @@ -158,7 +158,7 @@ array{ user: User, ... } ## Callable Types -CallableType : Name `(` CallableParameters? `)` CallableReturnType? +CallableType : Name TemplateParameters? `(` CallableParameters? `)` CallableReturnType? CallableParameters : CallableParameter (`,` CallableParameter)\* `,`? @@ -166,8 +166,10 @@ CallableReturnType : `:` Type A _callable type_ describes a function-like value. It is a [name](#sec-Names-and-Namespaces) — commonly `callable` or `Closure`, but any -name is accepted — followed by a parenthesized, possibly empty, parameter -list, and an optional return type introduced by a colon. +name is accepted — optionally followed by the +[template parameters](#sec-Template-Parameters) it declares, then by a +parenthesized, possibly empty, parameter list, and an optional return type +introduced by a colon. ```typescript callable() @@ -181,6 +183,116 @@ callable(): void Closure(int<0, max>, callable(?C): mixed): void ``` +### Template Parameters + +TemplateParameters : `<` TemplateParameter (`,` TemplateParameter)\* `,`? `>` + +TemplateParameter : Identifier TemplateBound\* TemplateDefault? + +TemplateBound : + +- UpperBound +- LowerBound + +UpperBound : UpperBoundOperator Type + +UpperBoundOperator : one of `of` `as` + +LowerBound : `super` Type + +TemplateDefault : `=` Type + +A callable type MAY declare the _template parameters_ it introduces, written +as a `<...>` list between the name and the parameter list. Each parameter is +an {Identifier} followed by as many _bounds_ as are put on it. + +A parameter accepts three kinds of limit, each of which MUST be written at +most once. The two bounds MAY be written in either order, while a +{TemplateDefault} MUST be written last: a bound standing behind it belongs to +the {Type} of the default rather than to the parameter. + +- An {UpperBound} narrows the parameter from above: the argument is to be a + subtype of the given {Type}. Its two operators denote the same thing, and a + conforming implementation MUST retain the one that was written. +- A {LowerBound} bounds the parameter from below: the argument is to be a + supertype of the given {Type}. +- A {TemplateDefault} is the {Type} the parameter takes when no argument is + supplied. It bounds nothing. + +Each operator is matched the one way it is spelled, so an `OF` bounds nothing +an `of` does. + +```typescript +callable(T): T +``` + +```typescript +callable(T): T +``` + +```typescript +Closure(T, U): V +``` + +```typescript +callable(T): void +``` + +**Counter-example.** A word that bounds nothing is not an operator. + +```typescript counter-example +callable(): void +``` + +``` +ParseException: Template parameter cannot be bounded with "whatever", expected one of "of", "as" or "super" +``` + +**Counter-example.** Neither is a word that is spelled the way an operator is +spelled, but written in a case of its own. + +```typescript counter-example +callable(): void +``` + +``` +ParseException: Template parameter cannot be bounded with "OF", expected one of "of", "as" or "super" +``` + +**Counter-example.** Each kind of limit is written at most once. + +```typescript counter-example +callable(): void +``` + +``` +ParseException: Template parameter cannot have more than one upper bound +``` + +**Counter-example.** A bound does not stand behind a default. + +```typescript counter-example +callable(): void +``` + +``` +ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself +``` + +A `<...>` list is written the same way whether it declares template parameters +or supplies [template arguments](#sec-Generic-Types), and only the `(` that +follows a parameter list tells them apart. A list that no `(` follows is +therefore a list of template arguments, which describes no bounds. + +**Counter-example.** Bounds belong to a callable alone. + +```typescript counter-example +Collection +``` + +``` +ParseException: Syntax error, unexpected end of input +``` ### Callable Parameters CallableParameter : Type `&`? `...`? Variable? `=`? From de14476e00365279c10648d1a21b18b19202f388 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 17:00:48 +0300 Subject: [PATCH 109/119] Actualize docs --- docs/Writerside/cfg/static/spec.html | 480 ++++- docs/Writerside/tl.tree | 1 + .../Writerside/topics/language/basic-types.md | 93 + .../topics/language/callable-types.md | 100 + docs/Writerside/topics/language/comparison.md | 1797 +++++++++++------ .../topics/language/conditional-types.md | 36 - .../Writerside/topics/language/const-types.md | 53 +- .../topics/language/generic-types.md | 44 +- .../topics/language/literal-types.md | 104 +- docs/Writerside/topics/parser.md | 51 +- docs/Writerside/topics/parser/features.md | 36 +- .../Writerside/topics/parser/tolerant-mode.md | 119 +- docs/Writerside/topics/phpdoc.md | 2 +- docs/Writerside/topics/printer.md | 2 +- docs/Writerside/topics/reader.md | 2 +- 15 files changed, 2081 insertions(+), 839 deletions(-) diff --git a/docs/Writerside/cfg/static/spec.html b/docs/Writerside/cfg/static/spec.html index a1f5ce2..09f6a23 100644 --- a/docs/Writerside/cfg/static/spec.html +++ b/docs/Writerside/cfg/static/spec.html @@ -93,7 +93,8 @@
    Statu
  • 6.2Callable Types
      -
    1. 6.2.1Callable Parameters
    2. +
    3. 6.2.1Template Parameters
    4. +
    5. 6.2.2Callable Parameters
  • @@ -220,7 +221,7 @@

    nopqrstuvwxyz -Any byte from U+0080 to U+00FF +Any byte from 0x80 to 0xFF

    Ignored tokens improve readability and separate lexical tokens, but are otherwise insignificant. Any amount of Ignored MAY appear before and after every lexical Token.

    -
    -Note -Although Ignored between two tokens is never itself significant, the presence of separating whitespace is what distinguishes a NameToken that is immediately followed by another token from a NameToken that introduces a template argument hint (see Template Argument Hints). This is the only place in the grammar where the presence of separating whitespace is observable to the syntactic grammar.
    +
    +Note +Although Ignored between two tokens is never itself significant, the presence of Whitespace standing directly behind a NameToken is what distinguishes a NameToken that is immediately followed by another token from a NameToken that introduces a template argument hint (see Template Argument Hints). This is the only place in the grammar where Ignored is observable to the syntactic grammar, and the one place a Comment does not stand for the whitespace it is written among: a comment MAY follow that whitespace, but MUST NOT stand in its stead.

    4.2.1White Space

    -Whitespace
    Any Unicode whitespace character
    +Whitespace
    + + + + + + + + + + + + +
    Horizontal Tab (U+0009)
    Line Feed (U+000A)
    Vertical Tab (U+000B)
    Form Feed (U+000C)
    Carriage Return (U+000D)
    Space (U+0020)
    -

    Whitespace separates tokens and improves the legibility of the source text. It includes, at minimum, the space (U+0020), horizontal tab (U+0009), line feed (U+000A) and carriage return (U+000D) characters. TypeLang does not distinguish between horizontal whitespace and line terminators, except within the multi-word is not operator (see Conditional Types).

    +

    Whitespace separates tokens and improves the legibility of the source text. TypeLang does not distinguish between horizontal whitespace and line terminators, and a Comment stands wherever whitespace stands, save for the one place the presence of the whitespace itself is read (see Ignored Tokens).

    +
    +Note +The six characters above are the whole of it. A source text is read as a sequence of bytes (see Source Text), so a character that parts words in a script of its own — a no-break space (U+00A0), say — is no whitespace here: every byte it is written of falls in the range a Letter covers, and it is read as a part of the name it stands in.

    4.2.2Comments

    @@ -303,12 +320,12 @@

    ()[]{} -<><=>= +<> ::\... -

    TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, < and <=; : and ::; . in ...); in every such case, the longest matching punctuator is taken.

    +

    TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, : and ::; . in ...); in every such case, the longest matching punctuator is taken.

    4.3.2Names

    @@ -326,6 +343,9 @@

    4
    -

    A NameToken is the raw lexical unit that is later assembled, together with the namespace separator, into the syntactic Name production (see Names and Namespaces); the two are distinct non-terminals of, respectively, the lexical and the syntactic grammar. A NameToken MUST begin with a Letter or an underscore (_) and MAY continue with any Letter, Digit, underscore, or dash (-). The only difference from the PHP identifier grammar is that the dash character is additionally permitted in any non-leading position.

    +

    A source text is read as a sequence of bytes rather than of codepoints, and every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a Letter covers. A name MAY therefore be written in any script.

    +
    Проект\Тип
    +

    A NameToken is always the longest possible valid sequence; it MUST NOT be followed by a NameContinue character.

    ExampleTypeName
     
    @@ -346,25 +366,33 @@

    ReservedWord
    - +
    truefalsenullis
    truefalsenullisnot
    -

    The words true, false and null are literal keywords (see Literal Tokens), and is is the conditional operator keyword (see Conditional Types). All four are matched case-insensitively, and only when not immediately followed by a NameContinue character.

    -

    When the source contains one of these words standing alone, in a position where a type is expected, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. Consequently, a bare reserved word cannot be used as a type name.

    -

    Counter-example. A bare reserved word is scanned as a literal, not a type name.

    -
    Counter Example № 2TrUe
    +

    The words true, false and null are literal keywords (see Literal Tokens), and is and not are the conditional operator keywords (see Conditional Types). Each is matched only when not immediately followed by a NameContinue character. The three literal keywords are matched case-insensitively, while is and not are matched the one way they are spelled.

    +

    Wherever the source contains one of these words, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. It remains an Identifier all the same (see Names and Namespaces), so a reserved word is shadowed rather than forbidden: it stands as a name wherever the token it is scanned as denotes no type of its own.

    +

    A literal keyword standing alone, in a position where a type is expected, is read as the LiteralType it denotes and not as the NamedType it would otherwise be (see Primary Types):

    +
    TrUe
    +
    +

    Behind a namespace separator or another segment, where no literal is expected, the same word is a name segment. This makes \true a reference to a type literally named true, distinct from the true literal:

    +
    \true
    +
    +

    The conditional operator keywords denote no type of their own, so nothing shadows them, and a bare is or not is an ordinary named type:

    +
    not
     
    -

    A reserved word MAY, however, appear as an Identifier inside a qualified Name — that is, when it is preceded by a namespace separator or another identifier (see Names and Namespaces). For example, \true references a type literally named true, whereas the bare true is the boolean literal.

    4.3.4Variable

    + -

    A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).

    +

    A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken, save for the dash a variable cannot carry. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).

    The special variable $this is recognised as a distinct token and additionally denotes the current object type when used as a primary type (see Primary Types).

    Note @@ -402,20 +430,35 @@

    NegativeSign
    -

    +
    +Sign
    + + +
    -+
    +
    -

    An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a NegativeSign. Within the digits of any integer literal, underscores (DigitSeparator) MAY appear freely as visual separators; they carry no meaning and do not affect the denoted value.

    -
    -
    Decimal.
    +

    An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a Sign. A NegativeSign makes the denoted value negative; a leading + denotes the same value the absence of a Sign does. Underscores (DigitSeparator) MAY appear as visual separators; they carry no meaning and do not affect the denoted value. A DigitSeparator MUST stand between two digits, and therefore may neither lead a run of digits, nor trail one, nor stand beside another DigitSeparator.

    +
    +Digits
    Digit(DigitSeparator?Digit)*
    +
    +

    Decimal. A decimal literal other than a lone 0 MUST NOT begin with a zero, which is the mark of an OctalIntLiteral.

    +
    +NonZeroDigit
    + + +
    123456789
    1_000_000
     

    Binary. Prefixed with 0b or 0B; digits are 0 and 1 only.

    BinaryIndicator
    @@ -423,6 +466,9 @@
    Decimal.
    bB
    +
    BinaryDigit
    @@ -431,9 +477,10 @@
    Decimal.
    0b1010_1101
     
    -

    Octal. Prefixed with 0o or 0O; digits are 0 through 7 only.

    +

    Octal. Prefixed with 0o or 0O or, as octal has been written since long before that prefix, with a leading 0 alone; digits are 0 through 7 only. A 0123 denotes the same value a 0o123 does.

    @@ -441,6 +488,9 @@
    Decimal.
    oO
    +
    +OctalDigits
    OctalDigit(DigitSeparator?OctalDigit)*
    +
    OctalDigit
    @@ -449,9 +499,16 @@
    Decimal.
    0o42
     
    +
    04_23
    +
    +

    Counter-example. A leading zero makes a literal an octal, so a digit outside the octal radix may not follow one.

    +
    Counter Example № 208
    +
    +
    ParseException: Syntax error, unexpected "8"
    +

    Hexadecimal. Prefixed with 0x or 0X; digits are 0 through 9 and a through f, in either case.

    @@ -459,6 +516,9 @@
    Decimal.
    xX
    +
    +HexDigits
    HexDigit(DigitSeparator?HexDigit)*
    +
    HexDigit
    @@ -476,10 +536,9 @@
    Decimal.
    ParseException: Syntax error, unexpected "42"
     
    -
    Static Semantics.
    -

    A DecimalIntLiteral whose first digit is 0 and which is longer than a single character is interpreted in the legacy octal radix (base 8), for compatibility with historical PHP source. Thus, 042 denotes the value 34.

    +

    An OctalIntLiteral written without an OctalIndicator is the legacy octal spelling kept for compatibility with historical PHP source. It is read in the same radix (base 8) the prefixed spelling is, so 042 denotes the value 34.

    A conforming implementation MUST retain the original (raw) lexeme of every integer literal. When the denoted value exceeds the range representable by the implementation’s platform integer type, the numeric value MUST be clamped to the nearest representable bound (the platform’s PHP_INT_MAX or PHP_INT_MIN) while the raw lexeme MUST be preserved unchanged.

    @@ -491,16 +550,16 @@

    4

    @@ -508,7 +567,7 @@

    4

    eE
    -

    A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a NegativeSign.

    +

    A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a Sign.

    Either the leading run of digits (before the decimal point) or the trailing run (after it) MAY be omitted, but not both:

    0.9
     
    @@ -516,9 +575,14 @@

    4
    1.
     
    -

    Scientific notation uses the case-insensitive e indicator followed by an optionally negative decimal exponent:

    +

    Scientific notation uses the case-insensitive e indicator followed by a decimal exponent, which takes a Sign of its own:

    10e-2
     
    +
    10e+2
    +
    +

    Every run of digits a float is written of accepts the DigitSeparator under the same rule an integer does, and, unlike a DecimalIntLiteral, MAY begin with a zero:

    +
    2_3.4_5e-6_7
    +

    Counter-example. A lone decimal point is not a valid float.

    Counter Example № 4.
     
    @@ -671,13 +735,13 @@

    ConditionalOperator
    - +
    isis not>=<=<>
    isis not

    A conditional type (also called a ternary type) selects between two types based on a comparison between a subject and a comparand. It is written as the subject, an operator, the comparand, a ?, the type chosen when the condition holds, a :, and the type chosen otherwise.

    T is string ? int : bool
     
    -

    The negative-equality operator is spelled is not. At the lexical level, the two words MUST be separated only by horizontal whitespace and are scanned as a single token; no line terminator may appear between them.

    +

    The negative-equality operator is spelled is not. The two words are separate tokens, so whitespace and comments MAY stand between them the way they MAY stand between any other two tokens.

    T is not string ? int : bool
     

    Either the subject or the comparand MAY be a Variable instead of a Type, including in “Yoda” order, where the variable appears on the right:

    @@ -685,7 +749,6 @@

    array is $value ? non-empty-array : null -

    In addition to is and is not, the relational operators <, >, <= and >= MAY be used as the condition operator. This is an extension beyond what PHPStan and Psalm accept; see Relationship to Other Tools.

    Note A bare Variable is only a valid type when it carries a ConditionalSuffix. A Variable MUST NOT otherwise stand alone as a type; the only variable-like token that may appear as a primary type on its own is $this (see Primary Types).
    @@ -763,10 +826,9 @@

    User[][] -

    Counter-example. The legacy list syntax does not accept a key type between the brackets; for that, use offset access or the modern array<…> generic.

    -
    Counter Example № 6User[int]
    +

    The legacy list syntax accepts no key type between the brackets; for that, use offset access or the modern array<…> generic. A User[int] is a well-formed document all the same — an offset access type (see below) rather than a list, since int is a valid Type. It is the absence of a type between the brackets that selects the list reading:

    +
    User[int]
     
    -

    This document, however, parses User[int] successfully — as an offset access type (see below) rather than a list — because int is a valid Type. It is the absence of a type between the brackets that selects the list interpretation.

    5.4.2Offset Access

    @@ -781,7 +843,7 @@

    T<U>[object{key: int, ...}]

    Counter-example. Each offset is enclosed in a single pair of brackets.

    -
    Counter Example № 7Collection[[Some]]
    +
    Counter Example № 6Collection[[Some]]
     
    ParseException: Syntax error, unexpected "["
     
    @@ -806,6 +868,9 @@

    Note Each of CallableType, NamedType, and the ClassConstant and ConstantMask forms of LiteralType begins with a Name. Once a complete Name has been recognized, these four are distinguished by the single token that immediately follows it: * selects a constant mask; :: selects a class constant; ( selects a callable type; < or { selects a generic or shape named type; any other following token (or the end of the document) leaves a plain named type. +
    +Note +A bare ReservedWord is likewise both a LiteralType and, through Identifier, a Name of a single segment (see Reserved Words). Where one source could be read as either, the LiteralType is the one taken, so a true denotes the boolean literal and never a type named true. An is and a not denote no literal of their own, and nothing is therefore left to shadow the named type each of them is.

    5.6Names and Namespaces

    @@ -829,9 +894,9 @@

    \Absolute\Type\Name

    A separator MAY appear at the start of a name or between two segments, but MUST NOT appear at the end.

    -

    Reserved words within names. The reserved words true, false, null and is (see Reserved Words) MAY appear as an Identifier segment of a Name. In isolation, such a word is scanned as a literal or operator and so cannot stand as a bare type name; but in a qualified position — following a separator or another segment — it denotes a name segment. This makes \null a reference to a type named null, distinct from the null literal.

    +

    Reserved words within names. Every reserved word — true, false, null, is and not (see Reserved Words) — MAY appear as an Identifier segment of a Name. In a qualified position — following a separator or another segment — such a word is a name segment and nothing besides. This makes \null a reference to a type named null, distinct from the null literal.

    Counter-example. A name cannot end with a separator.

    -
    Counter Example № 8Example\Name\
    +
    Counter Example № 7Example\Name\
     
    ParseException: Syntax error, unexpected end of input
     
    @@ -852,12 +917,15 @@

    TemplateArguments + -
    -TemplateArgumentType + -

    A generic type supplies a named type with one or more template arguments, each of which is itself a Type. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.

    +

    A generic type supplies a named type with one or more template arguments, each of which is itself a Type or a Wildcard. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.

    Validating the number of arguments, their bounds, and their nesting is the responsibility of the implementation, not of this grammar, which imposes no such limits.

    Path\To\ExampleClass<T, U>
     
    @@ -866,7 +934,7 @@

    HashMap<Request, User,>

    Counter-example. At least one argument is required, and a leading comma is not permitted.

    -
    Counter Example № 9example<>
    +
    Counter Example № 8example<>
     
    ParseException: Syntax error, unexpected ">"
     
    @@ -876,21 +944,21 @@

    5.8.1Template Argument Hints

    -

    A template argument MAY carry a single leading hint: an Identifier placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.

    +

    A template argument MAY carry a single leading hint: a NameToken placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.

    HashMap<array-key, covariant Request>
     
    -

    At the lexical level, the hint and the type it modifies MUST be separated by whitespace; this separation is what distinguishes a hint from the start of the argument’s own type (see Ignored Tokens). Each argument may carry at most one hint.

    -

    Counter-example. A hint must be a single valid identifier.

    -
    Counter Example № 10HashMap<array-key, some covariant Request>
    +

    At the lexical level, Whitespace MUST stand directly behind the hint; this separation is what distinguishes a hint from the start of the argument’s own type, and a Comment does not stand in its place (see Ignored Tokens). Each argument may carry at most one hint.

    +

    Counter-example. A hint is a single NameToken. A second one reads as the bound of a template parameter, and a parameter list belongs to a callable, so the statement is refused where the ( it would go on with is missing.

    +
    Counter Example № 9HashMap<array-key, some covariant Request>
     
    -
    ParseException: Syntax error, unexpected "Request"
    +
    ParseException: Syntax error, unexpected end of input
     
    -

    Counter-example. A hint cannot be a reserved word: is, true, false and null are always scanned as the corresponding operator or literal token (see Reserved Words) and never as a hint Identifier.

    -
    Counter Example № 11HashMap<is Request>
    +

    Counter-example. A hint cannot be a reserved word: true, false, null, is and not are always scanned as the corresponding literal or operator token (see Reserved Words) and never as a NameToken.

    +
    Counter Example № 10HashMap<is Request>
     
    -
    ParseException: Syntax error, unexpected "Request"
    +
    ParseException: Syntax error, unexpected ">"
     

    @@ -940,7 +1008,7 @@

    Path\To\ClassName::ANOTHER_CONSTANT_NAME

    Counter-example. The constant part may not contain a namespace separator.

    -
    Counter Example № 12ClassName::SOME\ANY
    +
    Counter Example № 11ClassName::SOME\ANY
     
    ParseException: Syntax error, unexpected "\"
     
    @@ -948,28 +1016,53 @@

    5.10.3Constant Masks

    + + + -

    A constant mask denotes a family of constants whose names share a common prefix. A mask MUST terminate with an asterisk (*).

    -

    A global constant mask matches every global constant beginning with the prefix:

    + +

    A constant mask denotes a family of constants whose names are written of the same segments, in the same order, with a Wildcard standing wherever the name is left unsaid. A mask MUST contain at least one Wildcard, and its segments and wildcards alternate, so two wildcards in a row denote nothing a single one does not.

    +

    A global constant mask matches every global constant the segments describe:

    JSON_*
     
    -

    A class constant mask matches every constant of a class whose name begins with the prefix; the prefix MAY be omitted entirely, in which case the mask matches every constant of the class:

    +

    The name of a global constant MAY be left unsaid from its very beginning, and the namespace it belongs to MAY be written in front of it. A Name standing in front of a MaskTail carries the namespace and, in its last segment, the leading segment of the mask; a Name separated from the mask by a \ is a namespace whole, and the mask then matches every constant that namespace holds:

    +
    *_SUFFIX
    +
    +
    Path\To\JSON_*
    +
    +
    Path\To\*
    +
    +

    A class constant mask matches every constant of a class the segments describe; the leading segment MAY be omitted entirely, in which case the mask matches every constant of the class:

    Path\To\ClassName::PREFIX_*
     
    +
    Path\To\ClassName::*_SUFFIX
    +
    +
    Path\To\ClassName::PREFIX_*_SUFFIX
    +
    Path\To\ClassName::*
     
    -

    Counter-example. A global mask must have a prefix; a lone asterisk is not a type.

    -
    Counter Example № 13*
    +

    Counter-example. A global mask must have at least one segment of a name, whether it stands in front of a wildcard or behind it; a lone asterisk, which would match every constant there is, is not a type.

    +
    Counter Example № 12*
     
    -
    ParseException: Syntax error, unexpected "*"
    +
    ParseException: Syntax error, unexpected end of input
     
    -

    Counter-example. The asterisk must be the final character of the mask.

    -
    Counter Example № 14Path\To\ClassName::PREFIX_*_SUFFIX
    +

    Counter-example. Two wildcards in a row are not a mask.

    +
    Counter Example № 13Path\To\ClassName::PREFIX_**
     
    -
    ParseException: Syntax error, unexpected "_SUFFIX"
    +
    ParseException: Syntax error, unexpected "*"
     
    @@ -1039,12 +1132,12 @@

    array{ Path\To\ClassName::CONSTANT_NAME: First, JSON_*: Second }

    Mixed keys are not permitted. A single shape MUST use either explicit keys throughout or implicit keys throughout.

    -
    Counter Example № 15array{ named: First, Second }
    +
    Counter Example № 14array{ named: First, Second }
     
    ParseException: Cannot mix explicit and implicit shape keys
     

    Duplicate explicit keys are not permitted. No two explicit fields of the same shape may denote the same key, regardless of which of the five key forms each uses.

    -
    Counter Example № 16array{ 1: int, 2: int, 1: string }
    +
    Counter Example № 15array{ 1: int, 2: int, 1: string }
     
    ParseException: Duplicate key "1"
     
    @@ -1076,7 +1169,7 @@

    6.2Callable Types

    -

    A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — followed by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.

    +

    A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — optionally followed by the template parameters it declares, then by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.

    callable()
     
    callable(): void
     
    Closure(int<0, max>, callable(?C): mixed): void
     
    -
    -

    6.2.1Callable Parameters

    +
    +

    6.2.1Template Parameters

    + + + + +
    +UpperBoundOperator
    + + +
    ofas
    +
    + + +

    A callable type MAY declare the template parameters it introduces, written as a <...> list between the name and the parameter list. Each parameter is an Identifier followed by as many bounds as are put on it.

    +

    A parameter accepts three kinds of limit, each of which MUST be written at most once. The two bounds MAY be written in either order, while a TemplateDefault MUST be written last: a bound standing behind it belongs to the Type of the default rather than to the parameter.

    +
      +
    • An UpperBound narrows the parameter from above: the argument is to be a subtype of the given Type. Its two operators denote the same thing, and a conforming implementation MUST retain the one that was written.
    • +
    • A LowerBound bounds the parameter from below: the argument is to be a supertype of the given Type.
    • +
    • A TemplateDefault is the Type the parameter takes when no argument is supplied. It bounds nothing.
    • +
    +

    Each operator is matched the one way it is spelled, so an OF bounds nothing an of does.

    +
    callable<T>(T): T
    +
    +
    callable<T of Some>(T): T
    +
    +
    Closure<T of Some, U super Any, V = int>(T, U): V
    +
    +
    callable<T of Some super Any = int>(T): void
    +
    +

    Counter-example. A word that bounds nothing is not an operator.

    +
    Counter Example № 16callable<T whatever Some>(): void
    +
    +
    ParseException: Template parameter cannot be bounded with "whatever", expected one of "of", "as" or "super"
    +
    +

    Counter-example. Neither is a word that is spelled the way an operator is spelled, but written in a case of its own.

    +
    Counter Example № 17callable<T OF Some>(): void
    +
    +
    ParseException: Template parameter cannot be bounded with "OF", expected one of "of", "as" or "super"
    +
    +

    Counter-example. Each kind of limit is written at most once.

    +
    Counter Example № 18callable<T of Some as Any>(): void
    +
    +
    ParseException: Template parameter cannot have more than one upper bound
    +
    +

    Counter-example. A bound does not stand behind a default.

    +
    Counter Example № 19callable<T = int of Some>(): void
    +
    +
    ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself
    +
    +

    A <...> list is written the same way whether it declares template parameters or supplies template arguments, and only the ( that follows a parameter list tells them apart. A list that no ( follows is therefore a list of template arguments, which describes no bounds.

    +

    Counter-example. Bounds belong to a callable alone.

    +
    Counter Example № 20Collection<T of Some>
    +
    +
    ParseException: Syntax error, unexpected end of input
    +
    +
    +
    +

    6.2.2Callable Parameters

    @@ -1102,7 +1265,7 @@

    callable(Type $name)

    Counter-example. A parameter given by name alone, without a type, is not permitted.

    -
    Counter Example № 17callable($name)
    +
    Counter Example № 21callable($name)
     
    ParseException: Syntax error, unexpected ")"
     
    @@ -1115,7 +1278,7 @@

    callable(T &$name)

    Counter-example. The ampersand must follow the type; it must not precede it.

    -
    Counter Example № 18callable(&T)
    +
    Counter Example № 22callable(&T)
     
    ParseException: Syntax error, unexpected "&"
     
    @@ -1130,12 +1293,12 @@

    callable(T &...$name)

    Counter-example. The ellipsis must follow the type; it must not precede it.

    -
    Counter Example № 19callable(...T)
    +
    Counter Example № 23callable(...T)
     
    ParseException: Syntax error, unexpected "..."
     

    Counter-example. A variadic parameter is already optional and therefore must not additionally carry a default marker.

    -
    Counter Example № 20callable(T ...$name=)
    +
    Counter Example № 24callable(T ...$name=)
     
    ParseException: Cannot have variadic param with a default
     
    @@ -1242,7 +1405,7 @@
    nopqrstuvwxyz -Any byte from U+0080 to U+00FF +Any byte from 0x80 to 0xFF
    -Whitespace
    Any Unicode whitespace character
    +Whitespace
    + + + + + + + + + + + + +
    Horizontal Tab (U+0009)
    Line Feed (U+000A)
    Vertical Tab (U+000B)
    Form Feed (U+000C)
    Carriage Return (U+000D)
    Space (U+0020)
    LineTerminator
    @@ -1309,7 +1485,7 @@
    (
    - +
    )[]{}
    <><=>=
    <>
    ::\...
    @@ -1330,14 +1506,17 @@
    ReservedWord
    - +
    truefalsenullis
    truefalsenullisnot
    +
    @@ -1354,9 +1533,18 @@
    NegativeSign
    -
    +
    +Sign
    + + +
    -+
    +
    +
    +Digits
    Digit(DigitSeparator?Digit)*
    +
    +
    +NonZeroDigit
    + + +
    123456789
    BinaryIndicator
    @@ -1375,6 +1570,9 @@
    b
    B
    + + +
    +HexDigits
    HexDigit(DigitSeparator?HexDigit)*
    +
    @@ -1570,13 +1778,16 @@
    TemplateArguments + - + + + +
    @@ -1637,7 +1862,32 @@
    Callable Types
    + + + + +
    +UpperBoundOperator
    + + +
    ofas
    +
    + +
    @@ -1854,7 +2123,8 @@

  • 6.2Callable Types
      -
    1. 6.2.1Callable Parameters
    2. +
    3. 6.2.1Template Parameters
    4. +
    5. 6.2.2Callable Parameters
  • diff --git a/docs/Writerside/tl.tree b/docs/Writerside/tl.tree index e6d987a..a68d237 100644 --- a/docs/Writerside/tl.tree +++ b/docs/Writerside/tl.tree @@ -29,6 +29,7 @@ topic="comparison.md"> + diff --git a/docs/Writerside/topics/language/basic-types.md b/docs/Writerside/topics/language/basic-types.md index d59c4ad..825f436 100644 --- a/docs/Writerside/topics/language/basic-types.md +++ b/docs/Writerside/topics/language/basic-types.md @@ -115,3 +115,96 @@ The namespace delimiter can be used in conjunction with keywords such as `true`, + +## The $this Type {id="this-type"} + +The `$this` variable denotes the type of the object the type is written on. It +is a type of its own, so it stands wherever a type stands: inside a union, +behind a question mark, or as the type a +[callable](callable-types.md) returns. + +> `$this` is the one variable that is a type. Any other variable names a +> [callable parameter](callable-types.md#named-parameters) or an operand of a +> [conditional type](conditional-types.md), and is not a type on its own. +> {style="note"} + + + + +> The type of the current object. +> ```typescript +> $this +> ``` + +> The current object type, or nothing at all. +> ```typescript +> ?$this +> ``` + +> A callable returning the object it was called on. +> ```typescript +> callable(): $this +> ``` + + + + +> Any other variable is not a type. +> ```typescript +> $var +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected end of input, T_EQ expected +> ``` +> {style="warning"} + + + + +## Comments + +A type MAY carry comments, and they are read the way whitespace is: they part +tokens and mean nothing of their own. + +A **line comment** begins with either a `//` or a `#` marker and runs up to the +end of the line. A **block comment** begins with a `/*` and runs up to and +including the next `*/`. + + + + +> Block comment standing between two tokens. +> ```typescript +> int /* comment */ | string +> ``` + +> Line comment ending a line of a type written across several. +> ```typescript +> int | // comment +> string +> ``` + +> A `#` marker begins a line comment as well. +> ```typescript +> # comment +> int +> ``` + + + + +> A comment is not a type, so a source made of one alone describes nothing. +> ```typescript +> // comment +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected end of input +> ``` +> {style="warning"} + + + diff --git a/docs/Writerside/topics/language/callable-types.md b/docs/Writerside/topics/language/callable-types.md index 25eec22..7945d5e 100644 --- a/docs/Writerside/topics/language/callable-types.md +++ b/docs/Writerside/topics/language/callable-types.md @@ -210,3 +210,103 @@ a parameter carries both markers, the ampersand ("`&`") comes first. + +## Template Parameters + +A callable MAY declare the template parameters it introduces, written as a +`<...>` list between the name and the parameter list. Each parameter is a +name, optionally followed by the bounds put on it. + +A parameter accepts three kinds of limit, each written at most once. The two +bounds are written in either order, and the default is written last: a bound +behind it would read as a bound of the default itself. + +* `of T` or `as T` — the **upper bound**: the argument is to be a subtype + of `T`. The two words mean the same and are kept as they are written. +* `super T` — the **lower bound**: the argument is to be a supertype of `T`. +* `= T` — the **default**: the type the parameter takes when no argument is + passed. It bounds nothing. + +The words are not case-sensitive, so an `OF` reads the same way an `of` does. + +> A `<...>` that no parenthesis follows is a list of +> [template arguments](generic-types.md), not of template parameters, and +> template arguments describe no bounds. +> {style="note"} + + + + +> Callable type declaring one template parameter. +> ```typescript +> callable(T): T +> ``` + +> Callable type declaring a bounded template parameter. +> ```typescript +> callable(T): T +> ``` + +> Every kind of bound, and several parameters at once. +> ```typescript +> Closure(T, U): V +> ``` + +> One parameter carrying every limit at once. +> ```typescript +> callable(T): void +> ``` + + + + +> Bounds belong to a callable alone, so a type used with template arguments +> describes none. +> ```typescript +> Collection +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected end of input +> ``` +> {style="warning"} + +> Only `of`, `as` and `super` bound a parameter. +> ```typescript +> callable(): void +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Template parameter cannot be bounded with "whatever", +> expected one of "of", "as" or "super" +> ``` +> {style="warning"} + +> Each kind of limit is written at most once. +> ```typescript +> callable(): void +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Template parameter cannot have more than one upper bound +> ``` +> {style="warning"} + +> A bound cannot stand behind the default, since it would read as a bound +> of the default itself: the `of Some` below bounds the `int`, not the `T`. +> ```typescript +> callable(): void +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Template parameter default must be written last, since +> a bound behind it reads as a bound of the default itself +> ``` +> {style="warning"} + + + diff --git a/docs/Writerside/topics/language/comparison.md b/docs/Writerside/topics/language/comparison.md index d91a45e..c72ae0c 100644 --- a/docs/Writerside/topics/language/comparison.md +++ b/docs/Writerside/topics/language/comparison.md @@ -7,21 +7,8 @@ static code analysis tools: [PHPStan](https://phpstan.org/) and [Psalm](https:// - PHPStan: [https://phpstan.org](https://phpstan.org/writing-php-code/phpdoc-types) - Psalm: [https://psalm.dev](https://psalm.dev/docs/annotating_code/type_syntax/atomic_types/) - - -There are others that are not currently tested for compatibility. -For example: - -
  • PHAN: https://github.com/phan
  • -
  • phpDocumentor: https://docs.phpdoc.org
  • -
    - -If you wish, you can add information about this software using a - - pull request to the documentation -. -
    - +- Phan: [https://github.com/phan](https://github.com/phan/phan/wiki/About-Union-Types) +- phpDocumentor: [https://docs.phpdoc.org](https://docs.phpdoc.org/guide/guides/types.html) > Below is a comparison list of all syntactic structures (grammar). > The logical component (the physical existence of the type) is **NOT** @@ -40,26 +27,103 @@ General table across all type parsing capabilities  PHPStan + +  Phan + + +  phpDocumentor + - - 69/69 - + + 82/83 + + + + + 40/83 + - 43/69 + 69/83 - 47/69 + 31/83 + + + + + 62/83 +## Methodology + +Each verdict below is the answer of the tool's own parser, asked directly, so +the table says what the tools do rather than what they are said to do. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ToolVersionA type is supported when
    +  TypeLang + type-lang/parser 2.xTypeParser::parse() returns a node
    +  Psalm + vimeo/psalm 6.5Psalm\Type::parseString() returns a type carrying what was written
    +  PHPStan + phpstan/phpdoc-parser 2.3TypeParser::parse() reads the source whole, leaving no trailing input
    +  Phan + phan/phan 5.5 + UnionType::fromStringInContext() returns something other than + a class name made of the whole source +
    +  phpDocumentor + phpdocumentor/type-resolver 2.0TypeResolver::resolve() returns a type, the source read whole
    + +> Two of these tools answer even where they did not understand the question, and +> the rules above are what tells the two apart. +> +> Phan reads whatever it cannot parse as a class name, so a `0b1010` comes back +> as a class called `\0b1010` rather than as an error. phpDocumentor builds on +> `phpstan/phpdoc-parser` and keeps what it managed to read, so a `JSON_*` comes +> back as the `JSON_` in front of it, the mask silently dropped. A verdict here +> counts neither. +> {style="note"} + ## Basic Types Below is a list of simple, logical and other common types. @@ -76,27 +140,43 @@ Below is a list of simple, logical and other common types.  PHPStan + +  Phan + + +  phpDocumentor + - 12/12 + 20/20 - 9/12 + 10/20 + + + + + 14/20 - 10/12 + 8/20 + + + + + 14/20 - + Class or type name (including FQN) @@ -110,9 +190,11 @@ Below is a list of simple, logical and other common types. + + - + Logical union types @@ -126,14 +208,16 @@ Below is a list of simple, logical and other common types. + + - + Logical intersection types - T & U & V + T & U & V @@ -142,9 +226,11 @@ Below is a list of simple, logical and other common types. + + - + Logical nullable types @@ -158,14 +244,34 @@ Below is a list of simple, logical and other common types. + + - + + + Grouping parentheses + + + (T | U)[] + + + + + + + + + + + + + Legacy list types syntax - User[] + User[] @@ -174,33 +280,51 @@ Below is a list of simple, logical and other common types. + + - - - Template arguments (Generics) + + + Type offsets (offset access) - ExampleCollection<array-key, User> + ExampleShape['key'] + ClassName::CONSTANT[0] - - - - Trailing comma not supported 1 + + + + + + + + + Template arguments (Generics) + + ExampleCollection<array-key, User> + + + + + + Trailing comma not supported 1 + + - + - + ExampleCollection<array-key, User,> @@ -210,12 +334,12 @@ Below is a list of simple, logical and other common types. - + Template argument hints - ExampleCollection<in array-key, out User> + ExampleCollection<in array-key, out User> @@ -228,18 +352,20 @@ Below is a list of simple, logical and other common types. Not Supported - - - - Call-site variance 1 - - + Other keywords 1 + + Other keywords 1 - + - + +

    + The in and out hints are not read, but the + same thing is said with contravariant and + covariant. +

    Collection<covariant Animal> @@ -249,12 +375,70 @@ Below is a list of simple, logical and other common types. - + + + Wildcard template arguments + + + ExampleCollection<*> + + + + + + + + Read as a bivariant mixed 1 + + Read as a bivariant mixed 1 + + + + + + +

    + An asterisk is read as a mixed carrying a + bivariant hint, so the argument parses, but nothing + of the wildcard itself is left in the tree. +

    + + // Written + ExampleCollection<*> + + + // Read + ExampleCollection<bivariant mixed> + +
    +
    + + + + + + The $this type + + + $this + + + + + + + + + + + + + Class constant types - ClassName::CONSTANT_NAME + ClassName::CONSTANT_NAME @@ -263,14 +447,16 @@ Below is a list of simple, logical and other common types. + + - + Prefixed class constant mask types - ClassName::CONSTANT_* + ClassName::CONSTANT_* @@ -279,30 +465,70 @@ Below is a list of simple, logical and other common types. + + - + Non-prefixed class constant mask types - ClassName::* + ClassName::* + + + + + + + + + + + + + + + Suffixed class constant mask types + + + ClassName::*_SUFFIX + + - + + + Class constant mask types carrying both ends + + + ClassName::PREFIX_*_SUFFIX + + + + + + + + + + + + + Global constant mask types - JSON_* + JSON_* @@ -321,6 +547,63 @@ Below is a list of simple, logical and other common types. Not Supported + + + + + + + Suffixed global constant mask types + + + *_SUFFIX + + + + + + + + + + + + + + + Namespaced constant mask types + + + Path\To\JSON_* + Path\To\* + + + + + + + + + + + + + + + Comments inside a type + + + int /* comment */ | string + + + + + + + + + + @@ -340,28 +623,44 @@ Below is a list of conditional types.  PHPStan + +  Phan + + +  phpDocumentor + - 10/10 + 6/6 - 4/10 + 4/6 + + + + + 3/6 - 4/10 + 0/6 + + + + + 3/6 - - + + Conditional positive equality types @@ -372,12 +671,30 @@ Below is a list of conditional types. - - + Brackets required 1 + Brackets required 1 + + Brackets required 1 - - + + + + +

    + A condition is written inside brackets and is only read there, so + the bare form below is read as the type T alone. +

    + + (T is A ? B : C) + +
    +
    + + + + +
    Conditional negative equality types @@ -388,12 +705,30 @@ Below is a list of conditional types. - - + Brackets required 1 + Brackets required 1 + + Brackets required 1 - - + + + + +

    + A condition is written inside brackets and is only read there, so + the bare form below is read as the type T alone. +

    + + (T is A ? B : C) + +
    +
    + + + + +
    Conditional referenced types @@ -405,17 +740,33 @@ Below is a list of conditional types. - - + Brackets required 1 + Brackets required 1 + + Brackets required 1 - - - Conditional referenced types - - - in Yoda-style + + + + +

    + A condition is written inside brackets and is only read there, so + the bare form below is read as the type T alone. +

    + + (T is A ? B : C) + +
    +
    + + + + +
    + Conditional referenced types + in Yoda-style A is $var ? B : C A is not $var ? B : C @@ -437,10 +788,12 @@ Below is a list of conditional types. Not Supported + + - - + + Functions in conditional types @@ -452,14 +805,16 @@ Below is a list of conditional types. - List of supported functions 1 - List of supported functions 2 + List of supported functions 1 + No call syntax 2 + + No call syntax 2 - + - +
  • define()
  • array_map()
  • @@ -472,24 +827,26 @@ Below is a list of conditional types.
  • class_alias()
  • - - -
  • func_get_arg()
  • -
  • func_get_args()
  • -
  • func_num_args()
  • -
    + +

    + A parenthesis behind a name ends the type, whatever the name is, + so the functions PHPStan reads in a condition are read by rules + of its own rather than by the type language. +

    + + (func_num_args() is 1 ? A : B) + // Unexpected token "(", expected ')' at offset 14 +
    - - + + Functions in conditional types - - in Yoda-style - + in Yoda-style A is foo() ? B : C A is not foo() ? B : C @@ -511,94 +868,8 @@ Below is a list of conditional types. Not Supported - - - - - Less-than conditional types - - - T < A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - - - - - - Greater-than conditional types - - - T > A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - - - - - - Less-than-or-equal conditional types - - - T <= A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - - - - - - Greater-than-or-equal conditional types - - - T >= A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - + + @@ -618,27 +889,43 @@ Below is a list of literal types/lexemes.  PHPStan + +  Phan + + +  phpDocumentor + - 19/19 + 21/21 + + + + + 7/21 + + + + + 21/21 - 7/19 + 5/21 - 11/19 + 15/21 - + Boolean true and false literals @@ -653,9 +940,11 @@ Below is a list of literal types/lexemes. + + - + The null literals @@ -669,9 +958,11 @@ Below is a list of literal types/lexemes. + + - + Single-quoted string literals @@ -685,9 +976,11 @@ Below is a list of literal types/lexemes. + + - + Double-quoted string literals @@ -701,9 +994,11 @@ Below is a list of literal types/lexemes. + + - + Escape sequences in a double-quoted string literals @@ -721,15 +1016,12 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - - + + + - + Hexadecimal sequences in a double-quoted string literals @@ -747,15 +1039,12 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - - + + + - + Unicode sequences in a double-quoted string literals @@ -773,15 +1062,12 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - - + + + - + Integer literals @@ -795,42 +1081,53 @@ Below is a list of literal types/lexemes. + + - + BigInteger (PHP_INT_MAX + 1 or PHP_INT_MIN - 1) literals - 42 + 9999999999999999999 - Works with restrictions 1 - Works with restrictions 2 - Works with restrictions 3 + Kept in full 1 + Works with restrictions 2 + Kept in full 3 + Works with restrictions 4 + Works with restrictions 5 - + - + +

    + The $decimal carries the value written out in base + 10, so a number too large for the platform's int is + still readable in full. The $value beside it is the + native one, and that is the only one a platform limit applies to. +

    // Input 9999999999999999999 // Stored - TypeLang\Parser\Node\Literal\IntLiteralNode { + TypeLang\Type\Literal\IntLiteralNode { +offset: 0 - +raw: "9999999999999999999" +value: 9223372036854775807 + +raw: "9999999999999999999" + +decimal: "9999999999999999999" }
    - + // Input 9999999999999999999 @@ -841,22 +1138,55 @@ Below is a list of literal types/lexemes. Open in psalm.dev - + +

    + The literal is kept as it was written, so nothing is lost while + the type is read. The number does not survive the analysis that + follows, though: what comes out of it is the nearest + int the platform carries. +

    // Input 9999999999999999999 // Stored - 9223372036854775807 + PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode { + value: "9999999999999999999" + } + + + // Analysed + \PHPStan\dumpType($value); + // Dumped type: 9223372036854775807 Open in phpstan.org
    + + + // Input + 9999999999999999999 + + + // Stored + 1.0E+19 + + + + + // Input + 9999999999999999999 + + + // Stored + 9223372036854775807 + +
    - + Integer literals in binary format @@ -874,15 +1204,33 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0b10101010 + + + // Read + 0 + +
    +
    - + Integer literals in octal format @@ -900,15 +1248,33 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0o42 + + + // Read + 0 + +
    +
    - + Integer literals in legacy octal format @@ -926,15 +1292,33 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as decimal 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 042 + + + // Read + 42 + +
    +
    - + Integer literals in hexadecimal format @@ -952,15 +1336,69 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0xDEAD_BEEF + + + // Read + 0 + +
    +
    + + + + + + Underscore (_) separators in integer literals + + + 42_04 + + + + + + + + + + + + + + + Explicitly signed number literals + + +42 + - + + + + + + + + + Float literals @@ -974,9 +1412,11 @@ Below is a list of literal types/lexemes. + + - + Float literals without leading zero @@ -995,9 +1435,11 @@ Below is a list of literal types/lexemes. + + - + Float literals without trailing zero @@ -1016,9 +1458,11 @@ Below is a list of literal types/lexemes. + + - + Float literals in scientific notation @@ -1037,14 +1481,16 @@ Below is a list of literal types/lexemes. + + - + - Float literals in scientific notation + Float literals in scientific notation carrying a signed exponent - 2e2 + -1.5e+3 @@ -1058,11 +1504,13 @@ Below is a list of literal types/lexemes. + + - - - Float hexadecimal literals in scientific notation + + + Hexadecimal integer literals carrying an e 0x42e2 @@ -1078,11 +1526,29 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0x42e2 + + + // Read + 0 + +
    +
    @@ -1091,7 +1557,6 @@ Below is a list of literal types/lexemes. Below is a list of grammar of shaped types. - @@ -1104,32 +1569,48 @@ Below is a list of grammar of shaped types. + + + + - - + + + - - - - - - + + + - - - - - - + + + - - + + - - + + + - - - - - - + + + - - - - - - + + + - - - + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -1475,55 +1902,19 @@ Below is a list of grammar of shaped types. - - - - - - + + + + - @@ -1531,18 +1922,19 @@ Below is a list of grammar of shaped types. - - + + + + - @@ -1550,12 +1942,35 @@ Below is a list of grammar of shaped types. + + - + + + + +
     PHPStan +  Phan + +  phpDocumentor +
    - 13/13 + 15/15 - 10/13 + 9/15 + + + + 13/15 + + + + 5/15 - 10/13 + 13/15
    - + + Explicit shape types - object { + array { key: ValueType } @@ -1138,38 +1619,18 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key: ValueType - } - - - // Error - Custom\ObjectType { - key: ValueType - } - - Open in psalm.dev - - -
    - + + Trailing comma in explicit shape types - object { + array { key: ValueType, } @@ -1178,38 +1639,18 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key: ValueType, - } - - - // Error - Custom\ObjectType { - key: ValueType, - } - - Open in psalm.dev - - -
    - + + Implicit shape types - object { + array { ValueType } @@ -1218,38 +1659,36 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1 Arrays and lists only 1 Arrays and lists only 1
    + - + // OK - object { - ValueType - } + array { ValueType } + list { ValueType } - // Error - Custom\ObjectType { - ValueType - } + // Syntax Error + object { ValueType } - Open in psalm.dev
    - + + Trailing comma in implicit shape types - object { + array { ValueType, } @@ -1265,14 +1704,16 @@ Below is a list of grammar of shaped types.
    - + + Optional keys in explicit shape types - object { + array { key?: ValueType } @@ -1281,106 +1722,75 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key?: ValueType - } - - - // Error - Custom\ObjectType { - key?: ValueType - } - - Open in psalm.dev - - -
    + Empty (closed) shape types - object {} + array {}
    Works with restrictions 1
    - - - - // OK - object {} - - - // Error - Custom\ObjectType {} - - Open in psalm.dev - - -
    + Unsealed shape types - object { ... } + array { ... }
    Works with restrictions 1 Read as an empty array 1
    + - + +

    + A shape carrying fields keeps what follows them, but one made of + the ellipsis alone comes back sealed and empty. +

    - // OK - object { ... } + // Written + array { ... } - // Error - Custom\ObjectType { ... } + // Read + array<never, never> - Open in psalm.dev
    + Explicit unsealed shape types - object { + array { key: ValueType, ... } @@ -1390,40 +1800,18 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key: ValueType, - ... - } - - - // Error - Custom\ObjectType { - key: ValueType, - ... - } - - Open in psalm.dev - - -
    + Implicit unsealed shape types - object { + array { ValueType, ... } @@ -1433,41 +1821,80 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - ValueType, - ... - } - - - // Error - Custom\ObjectType { - ValueType, - ... - } - - Open in psalm.dev - - -
    + + Typed unsealed shape types + + + array { + ...<array-key, ValueType> + } + +
    + + Class constant shape keys + + + array { + Path\To\ClassName::CONSTANT_NAME: string, + } + +
    + + Constant mask shape keys + + + array { + Path\To\ClassName::PREFIX_*: string, + JSON_*: string, + } + +
    - - Typed unsealed shape types + + + String literal shape keys - object { - ...<array-key, Type> + array { + 'some key': ValueType, }
    Works with restrictions 1 Works with restrictions 2
    - - - - // OK - object { - ...<array-key, Type> - } - - - // Error - Custom\ObjectType { - ...<array-key, Type> - } - - Open in psalm.dev - - - - // OK - array { - ...<array-key, Type> - } - - - // Syntax Error - list { - array-key, - ...<Type> - } - - Open in phpstan.org - - -
    - - Class constant shape keys + + + Numeric shape keys - array{ - Path\To\ClassName::CONSTANT_NAME: string, + array { + 0: ValueType, }
    - - Constant mask shape keys + + + Shapes of an arbitrary type name - array{ - Path\To\ClassName::PREFIX_*: string, - JSON_*: string, + Custom\ObjectType { + key: ValueType }
    Fixed set of names 1 Fixed set of names 2 Fixed set of names 2
    + + +

    + A shape follows an array, a list, an + object or a callable-array, and no other + name. +

    + Open in psalm.dev +
    + +

    + A shape follows an array, a list or an + object, and no other name. +

    +
    +
    +
    -## Callables Types +## Callable Types Below is a list of grammar of callable (function) types. @@ -1571,27 +1986,43 @@ Below is a list of grammar of callable (function) types.  PHPStan + +  Phan + + +  phpDocumentor + - - 15/15 - + + 20/21 + - 13/15 + 10/21 + + + + + 18/21 + + + + + 13/21 - 12/15 + 17/21 - + Non-typed callable types @@ -1610,9 +2041,11 @@ Below is a list of grammar of callable (function) types. Not Supported + + - + Typed callable types @@ -1626,9 +2059,11 @@ Below is a list of grammar of callable (function) types. + + - + Callable with typed parameters @@ -1642,9 +2077,29 @@ Below is a list of grammar of callable (function) types. + + + + + + + Trailing comma in the parameter list + + + callable(Type,): T + + + + + + + + + + - + Callable with optional parameters @@ -1658,9 +2113,11 @@ Below is a list of grammar of callable (function) types. + + - + Callable with named parameters @@ -1674,10 +2131,12 @@ Below is a list of grammar of callable (function) types. + + - - + + Callable with optional named parameters @@ -1695,36 +2154,40 @@ Below is a list of grammar of callable (function) types. + + - + Callable with output parameters - callable(T&): T + callable(T&): T - Works with restrictions 1 + Works with restrictions 1 + + - + - + // OK - callable(T&): U + callable(T&): U // Bug: Intersection types must be all objects, // Psalm\Type\Atomic\TInt provided in docblock - callable(int&): U + callable(int&): U Open in psalm.dev @@ -1732,47 +2195,49 @@ Below is a list of grammar of callable (function) types. - - + + Callable with output optional parameters - callable(T&=): T + callable(T&=): T - Works with restrictions 1 + Works with restrictions 1 + + - + - + // OK - callable(T&=): U + callable(T&): U // Bug: Intersection types must be all objects, // Psalm\Type\Atomic\TInt provided in docblock - callable(int&=): U + callable(int&): U - Open in psalm.dev + Open in psalm.dev - - + + Callable with output optional named parameters - callable(T &$name=): T + callable(T &$name=): T @@ -1786,9 +2251,11 @@ Below is a list of grammar of callable (function) types. + + - + Callable with suffixed variadic parameters @@ -1802,14 +2269,16 @@ Below is a list of grammar of callable (function) types. + + - + - Callable with prefixed variadic parameters + Callable with named variadic parameters - callable(...Type): T + callable(Type ...$name): T @@ -1817,90 +2286,112 @@ Below is a list of grammar of callable (function) types. - - - - Not Supported + + + + + + + + Callable with output named variadic parameters + + callable(Type &...$name): T + - + + + + + + + + + - Callable with prefixed and suffixed variadic parameters + Callable with prefixed variadic parameters - callable(...Type ...$name): T + callable(...Type): T + Suffix form only 1 - Works with restrictions 1 - Works with restrictions 2 + + + + Not Supported + + + Parameter dropped 2 + - - - This expression is incorrect because the "variadic" - lexeme ... must be present in the parameter - in a single copy. The parser must throw a syntax or - semantic error. - + - + +

    + The ellipsis stands behind the type, the way PHP itself writes it. + The prefix form was read by earlier versions of the parser and is + a syntax error now. +

    - callable(...int ...$name) - // Internal Psalm error on line ...: - // Unrecognised parse tree type Psalm\Internal\Type\ParseTree\CallableParamTree + callable(Type...): T - Open in psalm.dev
    - + - callable(...int ...$name) - // PHPDoc tag @return has invalid value (callable(...int ...$name): T): - // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ... + // Written + callable(...Type): T + + + // Read + callable(): T - Open in phpstan.org
    - + Callable with optional variadic parameters callable(Type...=): T - callable(...Type=): T + callable(Type ...$name=): T - Works with restrictions 1 + Error depends on the spelling 1 - + No Error + No Error + No Error - + - This expression is incorrect because any variadic - parameter is already optional. The parser must - throw a syntax or semantic error. + This expression is incorrect because any variadic parameter is + already optional. The parser must throw a syntax or semantic + error. - + // OK: Cannot have variadic param with a default in docblock callable(...T=): T @@ -1915,87 +2406,153 @@ Below is a list of grammar of callable (function) types. - + - Callable with named variadic parameters + Callable carrying the variadic marker twice - callable(Type ...$name): T - callable(...Type $name): T + callable(...Type ...$name): T - Works with restrictions 1 - Works with restrictions 2 + Internal error 1 + + No Error + No Error - + + + This expression is incorrect because the "variadic" lexeme + ... must be present in the parameter in a single + copy. The parser must throw a syntax or semantic error. + - + - callable(...Type $name): T - // Internal Psalm error on line ...: + callable(...int ...$name) + // Internal Psalm error on line ...: // Unrecognised parse tree type Psalm\Internal\Type\ParseTree\CallableParamTree - Open in psalm.dev - - - - callable(...Type $name): T - // PHPDoc tag @return has invalid value (callable(...Type $name): T): - // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ... - - Open in phpstan.org + Open in psalm.dev - - - Callable with output named variadic parameters + + + The $this return type + + + callable(): $this + + + + + + + + + + + + + + + Callable declaring template parameters + + + callable<T>(T): T + + + + + + + + + + + + + + + Upper bound of a template parameter - callable(Type &...$name): T - callable(...Type &$name): T + callable<T of Some>(T): T + callable<T as Some>(T): T - Works with restrictions 1 - Works with restrictions 2 + + Both words read as one 1 + + Both words read as one 1 - + - + +

    + An as comes back as an of, where TypeLang + keeps the bound as it was written. +

    - // OK - callable(T &...$name): U + // Written + callable<T as Some>(T): T - // Bug: Intersection types must be all objects, - // Psalm\Type\Atomic\TInt provided in docblock - callable(int &...$name): U - - Open in psalm.dev -
    - - - callable(...Type &$name): T - // PHPDoc tag @return has invalid value (callable(...Type &$name): T): - // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ... + // Read + callable<T of Some>(T): T - Open in phpstan.org
    + + + + Lower bound of a template parameter + + + callable<T super Some>(T): T + + + + + + + + + + + + + + + Default of a template parameter + + + callable<T = int>(T): T + + + + + + + + + + + diff --git a/docs/Writerside/topics/language/conditional-types.md b/docs/Writerside/topics/language/conditional-types.md index 409005a..b3854d9 100644 --- a/docs/Writerside/topics/language/conditional-types.md +++ b/docs/Writerside/topics/language/conditional-types.md @@ -101,42 +101,6 @@ The `is` keyword checks whether the subject **is** of the compared type, and -## Comparison Operators - -In addition to the `is` / `is not` equality checks, TypeLang also allows -comparing the subject using the `<`, `>`, `<=` and `>=` operators. - - - - -> Less than. -> ```typescript -> T < int<0, 100> ? A : B -> ``` - -> Greater than. -> ```typescript -> T > 0 ? A : B -> ``` - -> Less than or equal. -> ```typescript -> T <= 100 ? A : B -> ``` - -> Greater than or equal. -> ```typescript -> T >= 0 ? A : B -> ``` - - - - -> This is a TypeLang-specific extension: neither PHPStan nor Psalm support -> comparison operators in conditional types, only `is` and `is not`. See the -> [syntax comparison](comparison.md) page for details. -> {style="note"} - ## Yoda-Style Conditions Since the subject and the compared type can both be arbitrary type diff --git a/docs/Writerside/topics/language/const-types.md b/docs/Writerside/topics/language/const-types.md index 5ede64b..4867836 100644 --- a/docs/Writerside/topics/language/const-types.md +++ b/docs/Writerside/topics/language/const-types.md @@ -85,26 +85,59 @@ a double colon (`::`) character, and then the constant name. ## Constant Masks -A reference to a certain set of constants can be defined using a mask. The -use of masks is identical to regular constants, but must be terminated with -an asterisk (`*`). +A reference to a certain set of constants can be defined using a mask: A +sequence of name segments with an asterisk (`*`) standing wherever the name +is left unsaid. + +A mask is made of at least one asterisk, and its segments and asterisks +alternate, so two asterisks in a row describe nothing a single one does not. Prefixes on class constants can be omitted, so type will mean any class constant. +A global constant may be left unsaid from its very beginning as well, and the +namespace it belongs to may be written in front of the mask. A name separated +from the mask by a backslash is a namespace whole, so the mask then means any +constant that namespace holds. -> Reference to any constant with the `JSON_*` prefix. +> Reference to any constant with the `JSON_` prefix. > ```typescript > JSON_* > ``` +> Reference to any constant with the `_SUFFIX` suffix. +> ```typescript +> *_SUFFIX +> ``` + +> Reference to any constant with the `JSON_` prefix in the `Path\To` namespace. +> ```typescript +> Path\To\JSON_* +> ``` + +> Reference to any constant of the `Path\To` namespace. +> ```typescript +> Path\To\* +> ``` + > Reference to any class constant with the `PREFIX_` prefix. > ```typescript > Path\To\ClassName::PREFIX_* > ``` +> Reference to any class constant with the `_SUFFIX` suffix. +> ```typescript +> Path\To\ClassName::*_SUFFIX +> ``` + +> Reference to any class constant written of the `PREFIX_` and `_SUFFIX` +> segments, in that order. +> ```typescript +> Path\To\ClassName::PREFIX_*_SUFFIX +> ``` + > Reference to any class constant > ```typescript > Path\To\ClassName::* @@ -113,25 +146,27 @@ Prefixes on class constants can be omitted, so type will mean any class constant -> It is not allowed to omit prefixes from global constants. +> A global mask needs at least one segment of a name, whether it stands in +> front of the asterisk or behind it. A lone asterisk would mean any constant +> there is, and that is not a type. > ```typescript > * > ``` > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "*" +> ParseException: Syntax error, unexpected end of input > ``` > {style="warning"} -> The asterisk (`*`) must be the final character. +> Two asterisks in a row are not a mask. > ```typescript -> Path\To\ClassName::PREFIX_*_SUFFIX +> Path\To\ClassName::PREFIX_** > ``` > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "_SUFFIX" +> ParseException: Syntax error, unexpected "*" > ``` > {style="warning"} diff --git a/docs/Writerside/topics/language/generic-types.md b/docs/Writerside/topics/language/generic-types.md index baef0bc..2685cb2 100644 --- a/docs/Writerside/topics/language/generic-types.md +++ b/docs/Writerside/topics/language/generic-types.md @@ -114,7 +114,49 @@ used, for example, in static analyzers to indicate the > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "Request" +> ParseException: Syntax error, unexpected end of input +> ``` +> {style="warning"} + + + + + +## Wildcard Arguments + +A template argument may be left unsaid, written as an asterisk (`*`) in the +place of the type. A wildcard argument accepts a [hint](#call-site-hints) +the same way any other argument does. + + + + +> Any argument at all. +> ```typescript +> Collection<*> +> ``` + +> A wildcard beside a type. +> ```typescript +> HashMap +> ``` + +> A wildcard carrying a hint. +> ```typescript +> Collection +> ``` + + + + +> An asterisk is a template argument alone and is not a type of its own. +> ```typescript +> int|* +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "*" > ``` > {style="warning"} diff --git a/docs/Writerside/topics/language/literal-types.md b/docs/Writerside/topics/language/literal-types.md index 4a7bdaf..6b8baaa 100644 --- a/docs/Writerside/topics/language/literal-types.md +++ b/docs/Writerside/topics/language/literal-types.md @@ -125,13 +125,58 @@ representation. The braces are required in the sequence. E.g. `"\u{41}" === "A"` The syntax of integer numbers is similar to the PHP language. Binary, octal, decimal and hexadecimal number systems are supported. -> Please note that in addition to numbers, underscores (`_`) are allowed. +> Please note that in addition to numbers, underscores (`_`) are allowed as a +> separator. A separator stands between two digits and nowhere else, so it +> may neither lead a number, nor trail it, nor stand beside another separator. > {style="note"} Decimal numbers can contain any digits from `0` to `9` (leading `0` not allowed) and must match the regular expression `[1-9][0-9]*`. For any other numbers, -a special prefixed format is used, described below. Negative values are prefixed -with a minus (`-`). +a special prefixed format is used, described below. A value may be prefixed by +a sign: a minus (`-`) makes it negative, and a plus (`+`) says what the absence +of a sign already says. + +> A number is kept whole, however large it is. The node carries both the +> native `$value`, which a platform limit applies to, and the `$decimal` +> beside it — the value written out in base 10 as a string, so a number too +> large for an `int` is still readable in full. +> {style="note"} + + + + +> A long number written with separators. +> ```typescript +> 42_04 +> ``` + + + + +> A separator cannot trail a number. +> ```typescript +> 42_ +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "_" +> ``` +> {style="warning"} + +> Two separators in a row are not allowed. +> ```typescript +> 4__2 +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "__2" +> ``` +> {style="warning"} + + + ### Binary @@ -212,6 +257,11 @@ numbers between `0` and `7` and must match the regular expression > 0o42_23 > ``` +> Number `275` in "legacy" octal format with `_` delimiters. +> ```typescript +> 04_23 +> ``` + @@ -226,6 +276,18 @@ numbers between `0` and `7` and must match the regular expression > ``` > {style="warning"} +> A leading `0` makes a number octal, so a digit outside the octal range +> cannot follow one. +> ```typescript +> 08 +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "8" +> ``` +> {style="warning"} + @@ -273,7 +335,18 @@ The syntax of float numbers is similar to the PHP language. Basic floating point syntax and scientific notation are supported. Every floating point number accepts the format `[0-9]+\.[0-9]+`. The leading or -trailing number may be omitted. Negative values are prefixed with a minus (`-`. +trailing number may be omitted. A value may be prefixed by a sign: a minus (`-`) +makes it negative, and a plus (`+`) says what the absence of a sign already says. + +> A number is kept whole, however large it is. The node carries both the +> native `$value`, which a platform limit applies to, and the `$decimal` +> beside it — the value written out in base 10 as a string, so a number too +> large for an `int` is still readable in full. +> {style="note"} + +Every run of digits a float is written of — the one before the dot, the one +after it and the exponent alike — accepts the underscore (`_`) separator under +the same rule an integer does. @@ -298,6 +371,17 @@ trailing number may be omitted. Negative values are prefixed with a minus (`-`. > -0.9 > ``` +> Signed floating point literal value (equivalent of `0.9`). +> ```typescript +> +0.9 +> ``` + +> Floating point literal value written with separators in every part +> (equivalent of `23.45e-67`). +> ```typescript +> 2_3.4_5e-6_7 +> ``` + @@ -352,6 +436,18 @@ writing out an inconveniently long string of digits. > 10e-2 > ``` +> Also number `1000.0` in scientific notation: The exponent takes a sign of +> its own, and a plus says what the absence of a sign already says. + +> A number is kept whole, however large it is. The node carries both the +> native `$value`, which a platform limit applies to, and the `$decimal` +> beside it — the value written out in base 10 as a string, so a number too +> large for an `int` is still readable in full. +> {style="note"} +> ```typescript +> 10e+2 +> ``` + diff --git a/docs/Writerside/topics/parser.md b/docs/Writerside/topics/parser.md index 49fcfed..e585432 100644 --- a/docs/Writerside/topics/parser.md +++ b/docs/Writerside/topics/parser.md @@ -2,8 +2,8 @@ -Parses TypeLang syntax into an AST of `TypeLang\Type\*` nodes, with strict and -tolerant parsing modes, feature toggling, and grammar checking. +Parses TypeLang syntax into an AST of `TypeLang\Type\*` nodes, with strict, +partial and validating reading modes, feature toggling, and grammar checking. @@ -21,7 +21,7 @@ out of it, checking the grammar along the way. **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` ## Quick Start @@ -40,23 +40,25 @@ var_dump($type); ```php object(TypeLang\Type\NamedTypeNode)#1 (4) { ["offset"]=> int(0) - ["name"]=> object(TypeLang\Type\Name)#2 (3) { + ["name"]=> object(TypeLang\Type\Name)#2 (5) { ["offset"]=> int(0) - ["segments"]=> array(1) { + ["parts"]=> array(1) { [0]=> object(TypeLang\Type\Identifier)#3 (2) { ["offset"]=> int(0) ["value"]=> string(5) "array" } } + ["first"]=> object(TypeLang\Type\Identifier)#3 { ... } + ["last"]=> object(TypeLang\Type\Identifier)#3 { ... } ["isFullyQualified"]=> bool(false) } ["arguments"]=> NULL ["fields"]=> object(TypeLang\Type\Shape\FieldsListNode)#4 (3) { ["offset"]=> int(7) ["items"]=> array(1) { - [0]=> object(TypeLang\Type\Shape\NamedFieldNode)#5 (5) { ... } + [0]=> object(TypeLang\Type\Shape\NamedFieldNode)#5 (4) { ... } } - ["sealed"]=> bool(true) + ["isSealed"]=> bool(true) } } ``` @@ -68,30 +70,32 @@ node classes themselves (`TypeLang\Type\*`) belong to the separate `type-lang/types` package — plain AST Nodes. > If a statement cannot be parsed, `parse()` throws an exception implementing -> `TypeLang\Parser\Exception\ParserExceptionInterface`. See the [tolerant -> mode](tolerant-mode.md) page for a way to parse partially valid input +> `TypeLang\Parser\Exception\ParserExceptionInterface`. See the [partial +> parsing](tolerant-mode.md) page for a way to read partially valid input > instead of failing outright. > {style="note"} -## Strict vs. Tolerant Parsing +## Three Ways of Reading a Source -`TypeParser` implements two parsing strategies, both declared on -`TypeParserInterface`: +`TypeParser` asks the grammar three different questions, all three declared +on `TypeParserInterface`: * `parse(): TypeNode` — strict mode. Requires the whole input to be a syntactically valid type statement; throws a `ParserExceptionInterface` on the first error. -* `parseTolerant(): ParsedResult` — tolerant mode. Parses as much of the - input as it can and returns a `TypeLang\Parser\ParsedResult` object - containing the (possibly partial) type and the offset up to which the - source was actually consumed — regardless of what follows. Useful for - phpdoc/docblock parsing where a type declaration is followed by a - free-text description. See [Tolerant mode](tolerant-mode.md). +* `partial(): ParsedResult` — reads as much of the input as the grammar + describes and returns a `TypeLang\Parser\Partial\ParsedResult` carrying the + type built out of the read part and the offset the reading stopped at, + whatever follows it. Useful for phpdoc/docblock parsing, where a type + declaration is followed by a free-text description. See + [](tolerant-mode.md). +* `validate(): CheckResult` — tells whether the source is a type, building + nothing of it. This is the cheapest of the three whenever the type itself + is of no use. See [](tolerant-mode.md#validation). ## Parser Arguments -The `parse()`/`parseTolerant()` methods accept the source code in -any of the following forms: +All three methods accept the source code in any of the following forms: @@ -137,10 +141,11 @@ Enable or disable individual language constructs (generics, shapes, unions, ...) — see [](features.md).
    - + -Parse a type declaration embedded in free-form text, such as a phpdoc -annotation — see [](tolerant-mode.md). +Read a type declaration embedded in free-form text, such as a phpdoc +annotation, or ask whether a source is a type at all — see +[](tolerant-mode.md). diff --git a/docs/Writerside/topics/parser/features.md b/docs/Writerside/topics/parser/features.md index 4ea4ca3..53533f3 100644 --- a/docs/Writerside/topics/parser/features.md +++ b/docs/Writerside/topics/parser/features.md @@ -31,7 +31,7 @@ $parser->parse('42'); ``` TypeLang\Parser\Exception\SemanticParseException: -Literal values not allowed in "42" at column 1 +Literal values not allowed in "42" ``` To override one or more flags on an *existing* parser, use `TypeParser::withFeatures()` @@ -59,7 +59,7 @@ $features = new TypeParserFeatures() | Flag | Enables syntax | Error message fragment | |------------------|------------------------------------|------------------------------------------| | `literals` | `42`, `"foo"`, `true`, `null`, ... | `Literal values not allowed` | -| `generics` | `Example` | `Template arguments not allowed` | +| `generics` | `Example`, `callable(T): T` | `Template arguments not allowed` | | `hints` | `Example` | `Template argument hints not allowed` | | `lists` | `Example[]` | `Square bracket list types not allowed` | | `offsets` | `Example[Type]` | `Type offsets not allowed` | @@ -83,7 +83,7 @@ $parser->parse('42'); ``` ``` -Literal values not allowed in "42" at column 1 +Literal values not allowed in "42" ``` ### Generics @@ -100,7 +100,19 @@ $parser->parse('Example'); ``` ``` -Template arguments not allowed in "Example" at column 8 +Template arguments not allowed in "Example" +``` + +The same flag closes the other side of the angle brackets: the +[template parameters](callable-types.md#template-parameters) a callable +declares. + +```php +$parser->parse('callable(T): T'); +``` + +``` +Template parameters not allowed in "callable(T): T" ``` ### Hints @@ -116,7 +128,7 @@ $parser->parse('Example'); ``` ``` -Template argument hints not allowed in "Example" at column 9 +Template argument hints not allowed in "Example" ``` ### Lists @@ -132,7 +144,7 @@ $parser->parse('Example[]'); ``` ``` -Square bracket list types not allowed in "Example[]" at column 1 +Square bracket list types not allowed in "Example[]" ``` ### Offsets @@ -150,7 +162,7 @@ $parser->parse('Example[Type]'); ``` ``` -Type offsets not allowed in "Example[Type]" at column 1 +Type offsets not allowed in "Example[Type]" ``` ### Callables @@ -166,7 +178,7 @@ $parser->parse('fn(): void'); ``` ``` -Callable types not allowed in "fn(): void" at column 1 +Callable types not allowed in "fn(): void" ``` ### Shapes @@ -184,7 +196,7 @@ $parser->parse(<<<'PHP' ``` ``` -Shape fields not allowed in "array{foo: T}" on line 1 at column 6 +Shape fields not allowed in "array{foo: T}" ``` ### Unions @@ -198,7 +210,7 @@ $parser->parse('T|U'); ``` ``` -Union types not allowed in "T|U" at column 1 +Union types not allowed in "T|U" ``` ### Intersections @@ -212,7 +224,7 @@ $parser->parse('T&U'); ``` ``` -Intersection types not allowed in "T&U" at column 1 +Intersection types not allowed in "T&U" ``` ### Conditions @@ -229,5 +241,5 @@ $parser->parse('T is U ? 23 : 42'); ``` ``` -Conditional expressions not allowed in "T is U ? 23 : 42" at column 1 +Conditional expressions not allowed in "T is U ? 23 : 42" ``` diff --git a/docs/Writerside/topics/parser/tolerant-mode.md b/docs/Writerside/topics/parser/tolerant-mode.md index b1e8fce..c8124c9 100644 --- a/docs/Writerside/topics/parser/tolerant-mode.md +++ b/docs/Writerside/topics/parser/tolerant-mode.md @@ -1,15 +1,15 @@ -# Tolerant Mode +# Partial Parsing -Parse as much of a type declaration as possible instead of failing on the -first syntax error, and recover the offset at which parsing actually stopped. +Read as much of a type declaration as the grammar describes instead of failing +on the first syntax error, recover the offset the reading stopped at, or ask +whether a source is a type at all. -The "tolerant" analysis mode allows parsing a type grammar embedded in other -arbitrary data, without requiring the full input to be a valid type -statement. +Partial parsing allows reading a type grammar embedded in other arbitrary +data, without requiring the full input to be a valid type statement. This is convenient, for example, for analyzing phpdoc (docblocks): a `@return` annotation contains a type declaration followed by a free-text @@ -17,20 +17,32 @@ description, and there is no delimiter between the two other than "the type grammar stops making sense here". Unlike `parse()`, which either returns a fully valid `TypeNode` or throws, -`TypeParser::parseTolerant()` always returns a `TypeLang\Parser\ParsedResult` -object: +`TypeParser::partial()` always returns a +`TypeLang\Parser\Partial\ParsedResult`, and which of the three it is says how +much of the source the grammar described. -```php -final readonly class ParsedResult -{ - public TypeNode $type; - - /** - * Last processed token offset, in bytes. - */ - public int $offset; -} -``` + + + +The source is a type whole, so the reading stopped at the very end of it. +Carries the `$type` built out of it and the `$offset` the reading stopped at. + + + + +The grammar described the beginning of the source alone. This is a successful +result as well, since a type has been built either way — the rest of the +source simply begins at the `$offset`. + + + + +The source opens no type at all, so nothing has been built of it. Carries the +`$message` of what stands in the way, the `$position` (line and column) and +the `$offset` the reading stopped at. + + + ## Basic Usage @@ -39,8 +51,9 @@ use TypeLang\Parser\TypeParser; $parser = new TypeParser(); -$result = $parser->parseTolerant('int and more text'); +$result = $parser->partial('int and more text'); +var_dump($result::class); // PartialParsedResult var_dump($result->type->name->toString()); // string(3) "int" var_dump($result->offset); // int(4) ``` @@ -60,8 +73,8 @@ var_dump($remainder); // string(13) "and more text" > ```php > $parser->parse('int and more text'); > // TypeLang\Parser\Exception\UnexpectedTokenException: -> // Syntax error, unexpected "and " in "int and more text" -> // at column 5 +> // Syntax error, unexpected "and " (T_NAME_WITH_SPACE), +> // T_NS_DELIMITER expected in "int and more text" > ``` > {style="note"} @@ -82,7 +95,7 @@ $content = <<<'PHP' $content = substr($content, strlen('@return ')); // "Example Returns something very interesting!" -$result = $parser->parseTolerant($content); +$result = $parser->partial($content); var_dump($result->type); ``` @@ -110,17 +123,17 @@ The description can then be recovered from the reported offset: $description = substr($content, $result->offset); var_dump($description); -// string(36) "Returns something very interesting!" +// string(35) "Returns something very interesting!" ``` ## Recovering from Incomplete Constructs -Tolerant mode does not attempt to *repair* broken syntax — it stops at the +Partial parsing does not attempt to *repair* broken syntax — it stops at the last point where a complete sub-rule was matched and treats everything past it as trailing content, however implausible that trailing content looks. ```php -$result = $parser->parseTolerant('array{'); +$result = $parser->partial('array{'); var_dump($result->type->name->toString()); // string(5) "array" var_dump($result->offset); // int(5) @@ -129,3 +142,57 @@ var_dump($result->offset); // int(5) Here the unterminated `{` of the shape-fields list is never entered, so the parser falls back to the last valid statement — the bare `array` named type — and reports offset `5`, right before the `{`. + +## Validation + +Where the type itself is of no use and the only question is whether the +source is one, `TypeParser::validate()` asks exactly that and builds nothing. +It returns a `TypeLang\Parser\Validation\CheckResult`, and which of the three +it is answers the question. + + + + +The source is a type whole, so nothing stands in the way of it. + + + + +The source is no type of its own. Carries the `$message` of what stands in +the way, the `$position` (line and column) and the `$offset` the reading +stopped at. + + + + +The grammar described the beginning of the source alone. This is a failure as +well, since a check asks about the source whole, and the rest of it begins at +the `$offset`. + + + + +```php +use TypeLang\Parser\TypeParser; +use TypeLang\Parser\Validation\SuccessfulCheckResult; + +$parser = new TypeParser(); + +var_dump($parser->validate('array{ field: result }')::class); +// TypeLang\Parser\Validation\SuccessfulCheckResult + +var_dump($parser->validate('array{ field: result } and more')::class); +// TypeLang\Parser\Validation\PartialCheckResult + +var_dump($parser->validate('array{')::class); +// TypeLang\Parser\Validation\PartialCheckResult + +// A check for a source that is a type whole: +$isType = $parser->validate($source) instanceof SuccessfulCheckResult; +``` + +> A `PartialCheckResult` **is** a `FailureCheckResult`, and a +> `PartialParsedResult` **is** a `SuccessfulParsedResult`. Each hierarchy is +> shaped around the question its method asks: a check asks about the source +> whole, whereas a partial parse is content with the beginning of it. +> {style="note"} diff --git a/docs/Writerside/topics/phpdoc.md b/docs/Writerside/topics/phpdoc.md index 308f0e7..f0b79c1 100644 --- a/docs/Writerside/topics/phpdoc.md +++ b/docs/Writerside/topics/phpdoc.md @@ -28,7 +28,7 @@ comment. **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` * `ext-mbstring` optional diff --git a/docs/Writerside/topics/printer.md b/docs/Writerside/topics/printer.md index 1c95d77..caa6646 100644 --- a/docs/Writerside/topics/printer.md +++ b/docs/Writerside/topics/printer.md @@ -38,7 +38,7 @@ It ships with two printers that render the very same AST differently: **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` * `ext-mbstring` optional ## Quick Start diff --git a/docs/Writerside/topics/reader.md b/docs/Writerside/topics/reader.md index 6874963..d9c5270 100644 --- a/docs/Writerside/topics/reader.md +++ b/docs/Writerside/topics/reader.md @@ -35,7 +35,7 @@ phpdoc is the job of the PHPDoc parser. **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` * `ext-mbstring` optional ## Quick Start From c11269b9a62e8c8cdb9dbf365c4dda38c0dfd4df Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sat, 12 Sep 2026 17:00:59 +0300 Subject: [PATCH 110/119] Improve benchmarks --- bench/PhpDoc/Tools/PHPStanParserBench.php | 2 +- bench/PhpDoc/Tools/PhpDocumentorParserBench.php | 2 +- bench/PhpDoc/Tools/TypeLangParserBench.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bench/PhpDoc/Tools/PHPStanParserBench.php b/bench/PhpDoc/Tools/PHPStanParserBench.php index 4152f28..d159a12 100644 --- a/bench/PhpDoc/Tools/PHPStanParserBench.php +++ b/bench/PhpDoc/Tools/PHPStanParserBench.php @@ -18,7 +18,7 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\ParserConfig; -#[Groups(['phpstan']), Revs(1), Warmup(1), Iterations(10)] +#[Groups(['phpstan']), Revs(3), Warmup(10), Iterations(5)] #[BeforeMethods('prepare'), RetryThreshold(5)] final class PHPStanParserBench extends DocBlockParserBench { diff --git a/bench/PhpDoc/Tools/PhpDocumentorParserBench.php b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php index 4a39799..d8c565f 100644 --- a/bench/PhpDoc/Tools/PhpDocumentorParserBench.php +++ b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php @@ -14,7 +14,7 @@ use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; -#[Groups(['phpdocumentor']), Revs(1), Warmup(1), Iterations(10)] +#[Groups(['phpdocumentor']), Revs(3), Warmup(10), Iterations(5)] #[BeforeMethods('prepare'), RetryThreshold(5)] final class PhpDocumentorParserBench extends DocBlockParserBench { diff --git a/bench/PhpDoc/Tools/TypeLangParserBench.php b/bench/PhpDoc/Tools/TypeLangParserBench.php index 4d0bdc7..6e7c6f5 100644 --- a/bench/PhpDoc/Tools/TypeLangParserBench.php +++ b/bench/PhpDoc/Tools/TypeLangParserBench.php @@ -14,7 +14,7 @@ use TypeLang\PhpDoc\DocBlockParser; use TypeLang\PhpDoc\DocBlockParserInterface; -#[Groups(['typelang', 'baseline']), Revs(1), Warmup(1), Iterations(10)] +#[Groups(['typelang', 'baseline']), Revs(3), Warmup(10), Iterations(5)] #[BeforeMethods('prepare'), RetryThreshold(5)] final class TypeLangParserBench extends DocBlockParserBench { From 90547a74c55d5cac4cea98f879b61a347113757e Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:17:46 +0300 Subject: [PATCH 111/119] Add assert tags --- .../Tag/AssertIfFalseTag/AssertIfFalseTag.php | 6 +-- .../AssertIfFalseTagDefinition.php | 48 ++++++------------ .../Tag/AssertIfTrueTag/AssertIfTrueTag.php | 6 +-- .../AssertIfTrueTagDefinition.php | 48 ++++++------------ .../src/DocBlock/Tag/AssertTag/AssertTag.php | 8 +-- .../Tag/AssertTag/AssertTagDefinition.php | 50 ++++++------------- 6 files changed, 56 insertions(+), 110 deletions(-) diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php index 8d105de..2f84d06 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php @@ -4,10 +4,10 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfFalseTag; -use TypeLang\PhpDoc\DocBlock\Tag\TypedVariableTag; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTag; /** - * The `@assert-if-false` tag asserts the given type for a variable, but + * The `@assert-if-false` tag asserts the given type for a subject, but * only when the function returns `false`. */ -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php index 8bcd4a3..3f5127d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php @@ -4,54 +4,36 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfFalseTag; -use TypeLang\PhpDoc\DocBlock\Combinator\DescriptionCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\TypeCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\VariableCombinator; use TypeLang\PhpDoc\DocBlock\Description\DescriptionInterface; +use TypeLang\PhpDoc\DocBlock\Reference\CodeReference; use TypeLang\PhpDoc\DocBlock\Reference\TypeReference; -use TypeLang\PhpDoc\DocBlock\TagDefinition\Spec; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagDefinition; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPayload; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPlacement; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTagDefinition; +use TypeLang\PhpDoc\DocBlock\Tag\AssertOperator; /** - * The `@assert-if-false` tag asserts the given type for a variable, but + * The `@assert-if-false` tag asserts the given type for a subject, but * only when the function returns `false`. * * ``` - * "@assert-if-false" [ ] + * "@assert-if-false" [ "!" | "=" | "!=" ] [ ] * ``` */ -final class AssertIfFalseTagDefinition extends TagDefinition +final class AssertIfFalseTagDefinition extends AssertionTagDefinition { public const NAME = 'assert-if-false'; public function __construct() { - parent::__construct( - name: self::NAME, - spec: Spec::sequence( - Spec::rule(TypeCombinator::NAME, 'type'), - Spec::rule(VariableCombinator::NAME, 'variable'), - Spec::maybe( - Spec::rule(DescriptionCombinator::NAME, 'description'), - ), - ), - placement: TagPlacement::Block, - ); + parent::__construct(self::NAME); } - public function create(string $name, TagPayload $result): AssertIfFalseTag - { - /** @var TypeReference $type */ - $type = $result->get('type'); - - /** @var non-empty-string $variable */ - $variable = $result->get('variable'); - - /** @var DescriptionInterface|null $description */ - $description = $result->find('description'); - - return new AssertIfFalseTag($name, $type, $variable, $description); + protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertIfFalseTag { + return new AssertIfFalseTag($name, $statement, $subject, $operator, $description); } } diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php index 91faebd..9c1e0e4 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php @@ -4,10 +4,10 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfTrueTag; -use TypeLang\PhpDoc\DocBlock\Tag\TypedVariableTag; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTag; /** - * The `@assert-if-true` tag asserts the given type for a variable, but + * The `@assert-if-true` tag asserts the given type for a subject, but * only when the function returns `true`. */ -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php index ec92dcb..0513a66 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php @@ -4,54 +4,36 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfTrueTag; -use TypeLang\PhpDoc\DocBlock\Combinator\DescriptionCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\TypeCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\VariableCombinator; use TypeLang\PhpDoc\DocBlock\Description\DescriptionInterface; +use TypeLang\PhpDoc\DocBlock\Reference\CodeReference; use TypeLang\PhpDoc\DocBlock\Reference\TypeReference; -use TypeLang\PhpDoc\DocBlock\TagDefinition\Spec; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagDefinition; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPayload; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPlacement; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTagDefinition; +use TypeLang\PhpDoc\DocBlock\Tag\AssertOperator; /** - * The `@assert-if-true` tag asserts the given type for a variable, but + * The `@assert-if-true` tag asserts the given type for a subject, but * only when the function returns `true`. * * ``` - * "@assert-if-true" [ ] + * "@assert-if-true" [ "!" | "=" | "!=" ] [ ] * ``` */ -final class AssertIfTrueTagDefinition extends TagDefinition +final class AssertIfTrueTagDefinition extends AssertionTagDefinition { public const NAME = 'assert-if-true'; public function __construct() { - parent::__construct( - name: self::NAME, - spec: Spec::sequence( - Spec::rule(TypeCombinator::NAME, 'type'), - Spec::rule(VariableCombinator::NAME, 'variable'), - Spec::maybe( - Spec::rule(DescriptionCombinator::NAME, 'description'), - ), - ), - placement: TagPlacement::Block, - ); + parent::__construct(self::NAME); } - public function create(string $name, TagPayload $result): AssertIfTrueTag - { - /** @var TypeReference $type */ - $type = $result->get('type'); - - /** @var non-empty-string $variable */ - $variable = $result->get('variable'); - - /** @var DescriptionInterface|null $description */ - $description = $result->find('description'); - - return new AssertIfTrueTag($name, $type, $variable, $description); + protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertIfTrueTag { + return new AssertIfTrueTag($name, $statement, $subject, $operator, $description); } } diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTag.php b/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTag.php index c2a278b..bca8fd8 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTag.php @@ -4,10 +4,10 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertTag; -use TypeLang\PhpDoc\DocBlock\Tag\TypedVariableTag; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTag; /** - * The `@assert` tag asserts that the given variable is narrowed to a - * specified type once the call returns. + * The `@assert` tag asserts that the given subject is narrowed to the type it + * carries once the call returns. */ -final class AssertTag extends TypedVariableTag {} +final class AssertTag extends AssertionTag {} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php index 44a85ca..8dc454c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertTag/AssertTagDefinition.php @@ -4,54 +4,36 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertTag; -use TypeLang\PhpDoc\DocBlock\Combinator\DescriptionCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\TypeCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\VariableCombinator; use TypeLang\PhpDoc\DocBlock\Description\DescriptionInterface; +use TypeLang\PhpDoc\DocBlock\Reference\CodeReference; use TypeLang\PhpDoc\DocBlock\Reference\TypeReference; -use TypeLang\PhpDoc\DocBlock\TagDefinition\Spec; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagDefinition; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPayload; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPlacement; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTagDefinition; +use TypeLang\PhpDoc\DocBlock\Tag\AssertOperator; /** - * The `@assert` tag asserts that the given variable is narrowed to a - * specified type once the call returns. + * The `@assert` tag asserts that the given subject is narrowed to the type it + * carries once the call returns. * * ``` - * "@assert" [ ] + * "@assert" [ "!" | "=" | "!=" ] [ ] * ``` */ -final class AssertTagDefinition extends TagDefinition +final class AssertTagDefinition extends AssertionTagDefinition { public const NAME = 'assert'; public function __construct() { - parent::__construct( - name: self::NAME, - spec: Spec::sequence( - Spec::rule(TypeCombinator::NAME, 'type'), - Spec::rule(VariableCombinator::NAME, 'variable'), - Spec::maybe( - Spec::rule(DescriptionCombinator::NAME, 'description'), - ), - ), - placement: TagPlacement::Block, - ); + parent::__construct(self::NAME); } - public function create(string $name, TagPayload $result): AssertTag - { - /** @var TypeReference $type */ - $type = $result->get('type'); - - /** @var non-empty-string $variable */ - $variable = $result->get('variable'); - - /** @var DescriptionInterface|null $description */ - $description = $result->find('description'); - - return new AssertTag($name, $type, $variable, $description); + protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertTag { + return new AssertTag($name, $statement, $subject, $operator, $description); } } From c4d3cc77c304e53c61fae03f1356ab6ecc511449 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:18:08 +0300 Subject: [PATCH 112/119] Add assert combinator --- libs/phpdoc/src/Platform/StandardPlatform.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/phpdoc/src/Platform/StandardPlatform.php b/libs/phpdoc/src/Platform/StandardPlatform.php index fbf8d59..af729ce 100644 --- a/libs/phpdoc/src/Platform/StandardPlatform.php +++ b/libs/phpdoc/src/Platform/StandardPlatform.php @@ -6,6 +6,7 @@ use TypeLang\Parser\TypeParser; use TypeLang\Parser\TypeParserInterface; +use TypeLang\PhpDoc\DocBlock\Combinator\AssertSubjectCombinator; use TypeLang\PhpDoc\DocBlock\Combinator\CallableTypeCombinator; use TypeLang\PhpDoc\DocBlock\Combinator\IntegerCombinator; use TypeLang\PhpDoc\DocBlock\Combinator\IssueNameCombinator; @@ -144,6 +145,7 @@ public function getCombinators(): iterable TypeCombinator::NAME => new TypeCombinator($this->typeParser), CallableTypeCombinator::NAME => new CallableTypeCombinator($this->typeParser), VariableCombinator::NAME => new VariableCombinator(), + AssertSubjectCombinator::NAME => new AssertSubjectCombinator(), IntegerCombinator::NAME => new IntegerCombinator(), IssueNameCombinator::NAME => new IssueNameCombinator(), VersionCombinator::NAME => new VersionCombinator(), From d71f88c35464db7df792fdccf03cfdd6d2517c88 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:18:51 +0300 Subject: [PATCH 113/119] Add assertions to the tests --- .../tests/DocBlock/Tag/TypedVariableTagTest.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php index ceda3e8..b59ecc3 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php @@ -91,16 +91,7 @@ public static function typedVariableTagProvider(): iterable yield '@property-write' => ['property-write', PropertyWriteTag::class]; yield '@global' => ['global', GlobalTag::class]; - // The assert family is shared across Psalm, PHPStan and Phan, each - // contributing it under its own vendor-prefixed name. - yield '@psalm-assert' => ['psalm-assert', AssertTag::class]; - yield '@phpstan-assert' => ['phpstan-assert', AssertTag::class]; - yield '@phan-assert' => ['phan-assert', AssertTag::class]; - yield '@psalm-assert-if-true' => ['psalm-assert-if-true', AssertIfTrueTag::class]; - yield '@phpstan-assert-if-true' => ['phpstan-assert-if-true', AssertIfTrueTag::class]; - yield '@phan-assert-if-true' => ['phan-assert-if-true', AssertIfTrueTag::class]; - yield '@psalm-assert-if-false' => ['psalm-assert-if-false', AssertIfFalseTag::class]; - yield '@phpstan-assert-if-false' => ['phpstan-assert-if-false', AssertIfFalseTag::class]; - yield '@phan-assert-if-false' => ['phan-assert-if-false', AssertIfFalseTag::class]; + // The assert family carries a subject rather than a variable, so it is + // covered by the {@see AssertionTagTest} of its own. } } From 606e310d83889f6548eeef3365507e8712ffe033 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:19:09 +0300 Subject: [PATCH 114/119] Add assertions to the docs --- docs/Writerside/topics/phpdoc/combinators.md | 9 ++++++++- .../topics/phpdoc/tags/phan-assert-if-false-tag.md | 9 +++++---- .../topics/phpdoc/tags/phan-assert-if-true-tag.md | 9 +++++---- docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md | 9 +++++---- .../topics/phpdoc/tags/phpstan-assert-if-false-tag.md | 9 +++++---- .../topics/phpdoc/tags/phpstan-assert-if-true-tag.md | 9 +++++---- docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md | 9 +++++---- .../topics/phpdoc/tags/psalm-assert-if-false-tag.md | 9 +++++---- .../topics/phpdoc/tags/psalm-assert-if-true-tag.md | 9 +++++---- docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md | 9 +++++---- 10 files changed, 53 insertions(+), 37 deletions(-) diff --git a/docs/Writerside/topics/phpdoc/combinators.md b/docs/Writerside/topics/phpdoc/combinators.md index 901c5c6..10b3cd5 100644 --- a/docs/Writerside/topics/phpdoc/combinators.md +++ b/docs/Writerside/topics/phpdoc/combinators.md @@ -43,7 +43,7 @@ whatever is left entirely. ## Built-in Combinators -Fourteen combinators cover every built-in tag's grammar between them: +Fifteen combinators cover every built-in tag's grammar between them: @@ -56,6 +56,13 @@ Fourteen combinators cover every built-in tag's grammar between them: + + + + + diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md index b28a6ca..cbb8279 100644 --- a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md @@ -9,15 +9,16 @@ and its counterpart [@phan-assert-if-true](phan-assert-if-true-tag.md). ``` -"@phan-assert-if-false" [ ] +"@phan-assert-if-false" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phan-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} ``` The same `AssertIfFalseTag` is produced by [@psalm-assert-if-false](psalm-assert-if-false-tag.md) and [@phpstan-assert-if-false](phpstan-assert-if-false-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md index 16caa52..33a49a1 100644 --- a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md @@ -9,15 +9,16 @@ and its counterpart [@phan-assert-if-false](phan-assert-if-false-tag.md). ``` -"@phan-assert-if-true" [ ] +"@phan-assert-if-true" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phan-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} ``` The same `AssertIfTrueTag` is produced by [@psalm-assert-if-true](psalm-assert-if-true-tag.md) and [@phpstan-assert-if-true](phpstan-assert-if-true-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md index b1bdf0b..e2b475a 100644 --- a/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md @@ -10,15 +10,16 @@ siblings — see [@phan-assert-if-true](phan-assert-if-true-tag.md) and same assertion family. ``` -"@phan-assert" [ ] +"@phan-assert" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phan-assert` tag produces an `AssertTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertTag extends TypedVariableTag {} +final class AssertTag extends AssertionTag {} ``` The same `AssertTag` is produced by [@psalm-assert](psalm-assert-tag.md) and [@phpstan-assert](phpstan-assert-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md index f53c00c..bf000f8 100644 --- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md @@ -7,15 +7,16 @@ function returns `false`, defined by PHPStan. It is one of the [@phpstan-assert](phpstan-assert-tag.md) family of narrowing tags. ``` -"@phpstan-assert-if-false" [ ] +"@phpstan-assert-if-false" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phpstan-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} ``` The same `AssertIfFalseTag` is produced by [@psalm-assert-if-false](psalm-assert-if-false-tag.md) and [@phan-assert-if-false](phan-assert-if-false-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md index b4fce07..f9149ee 100644 --- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md @@ -7,15 +7,16 @@ function returns `true`, defined by PHPStan. It is one of the [@phpstan-assert](phpstan-assert-tag.md) family of narrowing tags. ``` -"@phpstan-assert-if-true" [ ] +"@phpstan-assert-if-true" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phpstan-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} ``` The same `AssertIfTrueTag` is produced by [@psalm-assert-if-true](psalm-assert-if-true-tag.md) and [@phan-assert-if-true](phan-assert-if-true-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md index 8f0b35c..ac1a469 100644 --- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md @@ -8,15 +8,16 @@ narrowed to a given type after the call, defined by PHPStan. It has boolean-returning function returns a particular value. ``` -"@phpstan-assert" [ ] +"@phpstan-assert" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phpstan-assert` tag produces an `AssertTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertTag extends TypedVariableTag {} +final class AssertTag extends AssertionTag {} ``` The same `AssertTag` is produced by [@psalm-assert](psalm-assert-tag.md) and [@phan-assert](phan-assert-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md index b701939..643d082 100644 --- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md @@ -8,15 +8,16 @@ defined by the static analyzer Psalm, alongside [@psalm-assert](psalm-assert-tag.md). ``` -"@psalm-assert-if-false" [ ] +"@psalm-assert-if-false" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@psalm-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} ``` The same `AssertIfFalseTag` is produced by [@phpstan-assert-if-false](phpstan-assert-if-false-tag.md) and [@phan-assert-if-false](phan-assert-if-false-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md index e60f763..0ba0d99 100644 --- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md @@ -8,15 +8,16 @@ defined by the static analyzer Psalm, alongside [@psalm-assert](psalm-assert-tag.md). ``` -"@psalm-assert-if-true" [ ] +"@psalm-assert-if-true" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@psalm-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} ``` The same `AssertIfTrueTag` is produced by [@phpstan-assert-if-true](phpstan-assert-if-true-tag.md) and [@phan-assert-if-true](phan-assert-if-true-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md index d1a2c27..798f8f0 100644 --- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md @@ -7,15 +7,16 @@ narrowed to a given type after the call returns. It is defined by the static analyzer Psalm. ``` -"@psalm-assert" [ ] +"@psalm-assert" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@psalm-assert` tag produces an `AssertTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertTag extends TypedVariableTag {} +final class AssertTag extends AssertionTag {} ``` The same `AssertTag` is produced by [@phpstan-assert](phpstan-assert-tag.md) and [@phan-assert](phan-assert-tag.md), which restate the same From 3f4d1af1991a0b04a83bcd34cdd443a245e2d544 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:19:51 +0300 Subject: [PATCH 115/119] Add missing assertion tag dependencies --- composer.json | 1 + .../Combinator/AssertSubjectCombinator.php | 83 +++++++++ .../Reference/VariableMethodReference.php | 40 ++++ .../Reference/VariablePropertyReference.php | 40 ++++ .../src/DocBlock/Tag/AssertOperator.php | 42 +++++ libs/phpdoc/src/DocBlock/Tag/AssertionTag.php | 58 ++++++ .../DocBlock/Tag/AssertionTagDefinition.php | 113 ++++++++++++ .../tests/DocBlock/Tag/AssertionTagTest.php | 171 ++++++++++++++++++ 8 files changed, 548 insertions(+) create mode 100644 libs/phpdoc/src/DocBlock/Combinator/AssertSubjectCombinator.php create mode 100644 libs/phpdoc/src/DocBlock/Reference/VariableMethodReference.php create mode 100644 libs/phpdoc/src/DocBlock/Reference/VariablePropertyReference.php create mode 100644 libs/phpdoc/src/DocBlock/Tag/AssertOperator.php create mode 100644 libs/phpdoc/src/DocBlock/Tag/AssertionTag.php create mode 100644 libs/phpdoc/src/DocBlock/Tag/AssertionTagDefinition.php create mode 100644 libs/phpdoc/tests/DocBlock/Tag/AssertionTagTest.php diff --git a/composer.json b/composer.json index 773d18a..202428f 100644 --- a/composer.json +++ b/composer.json @@ -58,6 +58,7 @@ }, "scripts": { "bench": "phpbench run ./bench --report=compare", + "bench:census": "php bench/PhpDoc/bin/census", "test:unit": "phpunit --testdox", "linter:check": "phpstan analyse --configuration phpstan.neon --memory-limit 256M", "linter:baseline": "@linter:check --generate-baseline", diff --git a/libs/phpdoc/src/DocBlock/Combinator/AssertSubjectCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/AssertSubjectCombinator.php new file mode 100644 index 0000000..42b52cf --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Combinator/AssertSubjectCombinator.php @@ -0,0 +1,83 @@ +items + * $this->getItems() + * self::$instances + * ``` + * + * @template-implements CombinatorInterface + */ +final class AssertSubjectCombinator implements CombinatorInterface +{ + public const NAME = 'AssertSubject'; + + public function __invoke(Cursor $cursor): CodeReference + { + $subject = $this->parse($cursor); + + // A subject is a single word: nothing but whitespace may follow it. + if ($subject === null || $cursor->readWord() !== '') { + throw new NoMatchException('Expected a variable, a property or a method'); + } + + return $subject; + } + + private function parse(Cursor $cursor): ?CodeReference + { + // "$value", "$this->items" and "$this->getItems()" + if ($cursor->readLiteral('$')) { + $variable = $cursor->readPhpIdentifier(); + + if ($variable === '') { + return null; + } + + if (!$cursor->readLiteral('->')) { + return new VariableReference($variable); + } + + $member = $cursor->readPhpIdentifier(); + + if ($member === '') { + return null; + } + + return $cursor->readLiteral('()') + ? new VariableMethodReference($variable, $member) + : new VariablePropertyReference($variable, $member); + } + + // "self::$instances" + $class = $cursor->readPhpQualifiedName(); + + if ($class === '' || !$cursor->readLiteral('::$')) { + return null; + } + + $property = $cursor->readPhpIdentifier(); + + return $property === '' ? null : new ClassPropertyReference($class, $property); + } +} diff --git a/libs/phpdoc/src/DocBlock/Reference/VariableMethodReference.php b/libs/phpdoc/src/DocBlock/Reference/VariableMethodReference.php new file mode 100644 index 0000000..ff02f34 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Reference/VariableMethodReference.php @@ -0,0 +1,40 @@ +getItems() + * ``` + * + * The method is the one of the object the variable holds rather than the one + * a class declares, which is what a {@see ClassMethodReference} points to. + */ +final class VariableMethodReference extends CodeReference +{ + public function __construct( + /** + * The name of the variable holding the object, without the leading "$". + * + * @var non-empty-string + */ + public readonly string $variable, + /** + * The name of the referenced method. + * + * @var non-empty-string + */ + public readonly string $name, + ) { + parent::__construct(); + } + + public function __toString(): string + { + return \sprintf('$%s->%s()', $this->variable, $this->name); + } +} diff --git a/libs/phpdoc/src/DocBlock/Reference/VariablePropertyReference.php b/libs/phpdoc/src/DocBlock/Reference/VariablePropertyReference.php new file mode 100644 index 0000000..586c621 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Reference/VariablePropertyReference.php @@ -0,0 +1,40 @@ +items + * ``` + * + * The property is the one of the object the variable holds rather than the one + * a class declares, which is what a {@see ClassPropertyReference} points to. + */ +final class VariablePropertyReference extends CodeReference +{ + public function __construct( + /** + * The name of the variable holding the object, without the leading "$". + * + * @var non-empty-string + */ + public readonly string $variable, + /** + * The name of the referenced property. + * + * @var non-empty-string + */ + public readonly string $name, + ) { + parent::__construct(); + } + + public function __toString(): string + { + return \sprintf('$%s->%s', $this->variable, $this->name); + } +} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertOperator.php b/libs/phpdoc/src/DocBlock/Tag/AssertOperator.php new file mode 100644 index 0000000..5e4ef96 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Tag/AssertOperator.php @@ -0,0 +1,42 @@ + [ ] + * ``` + */ +abstract class AssertionTag extends TypedTag +{ + public function __construct( + string $name, + TypeReference $statement, + /** + * What the assertion is written of. + */ + public readonly ReferenceInterface $subject, + /** + * The way the subject relates to the type. + */ + public readonly AssertOperator $operator = AssertOperator::Is, + ?DescriptionInterface $description = null, + ) { + parent::__construct($name, $statement, $description); + } + + #[\Override] + public function __toString(): string + { + $result = \sprintf( + '@%s %s%s %s', + $this->name, + $this->operator->value, + $this->statement, + $this->subject, + ); + + if ($this->description !== null) { + $result .= ' ' . $this->description; + } + + return $result; + } +} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertionTagDefinition.php new file mode 100644 index 0000000..706f442 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Tag/AssertionTagDefinition.php @@ -0,0 +1,113 @@ + [ ] + * "@assert-if-true" [ "!" | "=" | "!=" ] [ ] + * "@assert-if-false" [ "!" | "=" | "!=" ] [ ] + * ``` + */ +abstract class AssertionTagDefinition extends TagDefinition +{ + /** + * The names the prefix of the type is reported under, one per prefix. + * + * @var non-empty-string + */ + private const OPERATOR_IS_NOT = 'is-not'; + + /** + * @var non-empty-string + */ + private const OPERATOR_EQUALS = 'equals'; + + /** + * @var non-empty-string + */ + private const OPERATOR_NOT_EQUALS = 'not-equals'; + + /** + * @param non-empty-string $name + */ + public function __construct(string $name) + { + parent::__construct( + name: $name, + spec: Spec::sequence( + // A literal reports nothing but its own presence, so each of + // the prefixes is asked about under a name of its own. The + // longest one is offered first, so that the "!" of a "!=" is + // not taken for the whole of it. + Spec::maybe( + Spec::oneOf( + Spec::literal(AssertOperator::NotEquals->value, self::OPERATOR_NOT_EQUALS), + Spec::literal(AssertOperator::IsNot->value, self::OPERATOR_IS_NOT), + Spec::literal(AssertOperator::Equals->value, self::OPERATOR_EQUALS), + ), + ), + Spec::rule(TypeCombinator::NAME, 'type'), + Spec::rule(AssertSubjectCombinator::NAME, 'subject'), + Spec::maybe( + Spec::rule(DescriptionCombinator::NAME, 'description'), + ), + ), + placement: TagPlacement::Block, + ); + } + + final public function create(string $name, TagPayload $result): AssertionTag + { + /** @var TypeReference $type */ + $type = $result->get('type'); + + /** @var CodeReference $subject */ + $subject = $result->get('subject'); + + /** @var DescriptionInterface|null $description */ + $description = $result->find('description'); + + return $this->createAssertion( + name: $name, + statement: $type, + subject: $subject, + operator: match (true) { + $result->find(self::OPERATOR_NOT_EQUALS) !== null => AssertOperator::NotEquals, + $result->find(self::OPERATOR_IS_NOT) !== null => AssertOperator::IsNot, + $result->find(self::OPERATOR_EQUALS) !== null => AssertOperator::Equals, + default => AssertOperator::Is, + }, + description: $description, + ); + } + + /** + * @param non-empty-string $name + */ + abstract protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertionTag; +} diff --git a/libs/phpdoc/tests/DocBlock/Tag/AssertionTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/AssertionTagTest.php new file mode 100644 index 0000000..979281f --- /dev/null +++ b/libs/phpdoc/tests/DocBlock/Tag/AssertionTagTest.php @@ -0,0 +1,171 @@ +type); + self::assertSame(AssertOperator::Is, $tag->operator); + self::assertInstanceOf(VariableReference::class, $tag->subject); + self::assertSame('name', $tag->subject->name); + self::assertSame('The name it carries.', (string) $tag->description); + self::assertSame('@phpstan-assert non-empty-string $name The name it carries.', (string) $tag); + } + + /** + * @param non-empty-string $tag + */ + #[Test] + #[DataProvider('operatorProvider')] + public function readsThePrefixOfTheType(string $tag, AssertOperator $expected): void + { + $result = self::parseTag($tag); + + self::assertInstanceOf(AssertionTag::class, $result); + self::assertSame($expected, $result->operator); + self::assertSame($tag, (string) $result); + } + + /** + * @return iterable + */ + public static function operatorProvider(): iterable + { + yield 'of the type' => ['@phpstan-assert int $value', AssertOperator::Is]; + yield 'anything but the type' => ['@phpstan-assert !null $value', AssertOperator::IsNot]; + yield 'the very value' => ['@phpstan-assert =non-empty-string $value', AssertOperator::Equals]; + yield 'anything but the value' => ['@phpstan-assert !=null $value', AssertOperator::NotEquals]; + + // The prefixes are read by every member of the family alike + yield 'when the call returns true' => ['@phpstan-assert-if-true !false $value', AssertOperator::IsNot]; + yield 'when the call returns false' => ['@psalm-assert-if-false =int $value', AssertOperator::Equals]; + } + + /** + * @param non-empty-string $tag + * @param class-string $expected + */ + #[Test] + #[DataProvider('subjectProvider')] + public function readsWhatTheAssertionIsWrittenOf(string $tag, string $expected, string $printed): void + { + $result = self::parseTag($tag); + + self::assertInstanceOf(AssertionTag::class, $result); + self::assertInstanceOf($expected, $result->subject); + self::assertSame($printed, (string) $result->subject); + self::assertSame($tag, (string) $result); + } + + /** + * @return iterable + */ + public static function subjectProvider(): iterable + { + yield 'a variable' => [ + '@phpstan-assert int $value', + VariableReference::class, + '$value', + ]; + yield 'a property of a variable' => [ + '@phpstan-assert-if-true !null $this->comparisonFailure', + VariablePropertyReference::class, + '$this->comparisonFailure', + ]; + yield 'a method of a variable' => [ + '@phpstan-assert Some\Any $this->getItems()', + VariableMethodReference::class, + '$this->getItems()', + ]; + yield 'a static property of a class' => [ + '@psalm-assert !null self::$instances', + ClassPropertyReference::class, + 'self::$instances', + ]; + } + + /** + * @param non-empty-string $name + * @param class-string $expected + */ + #[Test] + #[DataProvider('assertionTagProvider')] + public function assertionTagIsRecognized(string $name, string $expected): void + { + $tag = self::parseTag(\sprintf('@%s int $x', $name)); + + self::assertInstanceOf($expected, $tag); + self::assertInstanceOf(AssertionTag::class, $tag); + self::assertSame($name, $tag->name); + self::assertInstanceOf(NamedTypeNode::class, $tag->type); + } + + /** + * @return iterable}> + */ + public static function assertionTagProvider(): iterable + { + // The assert family is shared across Psalm, PHPStan and Phan, each + // contributing it under its own vendor-prefixed name. + yield '@psalm-assert' => ['psalm-assert', AssertTag::class]; + yield '@phpstan-assert' => ['phpstan-assert', AssertTag::class]; + yield '@phan-assert' => ['phan-assert', AssertTag::class]; + yield '@psalm-assert-if-true' => ['psalm-assert-if-true', AssertIfTrueTag::class]; + yield '@phpstan-assert-if-true' => ['phpstan-assert-if-true', AssertIfTrueTag::class]; + yield '@phan-assert-if-true' => ['phan-assert-if-true', AssertIfTrueTag::class]; + yield '@psalm-assert-if-false' => ['psalm-assert-if-false', AssertIfFalseTag::class]; + yield '@phpstan-assert-if-false' => ['phpstan-assert-if-false', AssertIfFalseTag::class]; + yield '@phan-assert-if-false' => ['phan-assert-if-false', AssertIfFalseTag::class]; + } + + /** + * @param non-empty-string $tag + */ + #[Test] + #[DataProvider('malformedProvider')] + public function rejectsWhatIsNoAssertion(string $tag): void + { + $result = self::parseTag($tag); + + self::assertInstanceOf(InvalidTag::class, $result); + self::assertInstanceOf(MalformedTagException::class, $result->reason); + } + + /** + * @return iterable + */ + public static function malformedProvider(): iterable + { + yield 'a type alone' => ['@phpstan-assert int']; + yield 'a subject alone' => ['@phpstan-assert $value']; + yield 'a prefix alone' => ['@phpstan-assert !']; + yield 'a property of nothing' => ['@phpstan-assert int $this->']; + yield 'a class of no property' => ['@phpstan-assert int self::']; + } +} From 2c4206fb0f1243ff62fe0281fd389abbf9bcaa3e Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:20:21 +0300 Subject: [PATCH 116/119] Actualize benchmarks --- bench/PhpDoc/Tools/DocBlockParserBench.php | 32 ++++++++++++++++++++++ bench/PhpDoc/Tools/TypeLangParserBench.php | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/bench/PhpDoc/Tools/DocBlockParserBench.php b/bench/PhpDoc/Tools/DocBlockParserBench.php index 0037f43..1d5ac90 100644 --- a/bench/PhpDoc/Tools/DocBlockParserBench.php +++ b/bench/PhpDoc/Tools/DocBlockParserBench.php @@ -32,7 +32,25 @@ abstract class DocBlockParserBench 'monolog/monolog' => '*', 'nikic/php-parser' => '*', 'phpdocumentor/reflection-docblock' => '*', + 'phpstan/extension-installer' => '*', + 'phpstan/php-8-stubs' => '*', 'phpstan/phpdoc-parser' => '*', + 'phpstan/phpstan' => '*', + 'phpstan/phpstan-beberlei-assert' => '*', + 'phpstan/phpstan-deprecation-rules' => '*', + 'phpstan/phpstan-dibi' => '*', + 'phpstan/phpstan-doctrine' => '*', + 'phpstan/phpstan-mockery' => '*', + 'phpstan/phpstan-nette' => '*', + 'phpstan/phpstan-php-parser' => '*', + 'phpstan/phpstan-phpunit' => '*', + 'phpstan/phpstan-strict-rules' => '*', + 'phpstan/phpstan-symfony' => '*', + 'phpstan/phpstan-webmozart-assert' => '*', + 'psalm/attributes' => '*', + 'psalm/plugin-mockery' => '*', + 'psalm/plugin-phpunit' => '*', + 'psalm/plugin-symfony' => '*', 'phpunit/php-code-coverage' => '*', 'phpunit/php-file-iterator' => '*', 'phpunit/php-text-template' => '*', @@ -57,9 +75,22 @@ abstract class DocBlockParserBench 'symfony/service-contracts' => '*', 'symfony/var-dumper' => '*', 'twig/twig' => '*', + 'vimeo/psalm' => '*', 'webmozart/assert' => '*', ]; + /** + * The PHP version the corpus is resolved for. + * + * The corpus is read and never run, so the version it is installed for is + * not the one the benchmark runs on: it is pinned so that the corpus is + * the same everywhere, and so that a package supporting no version as new + * as the one at hand is installed all the same. + * + * @var non-empty-string + */ + protected const CORPUS_PHP_VERSION = '8.3.0'; + /** * Each set contains every DocBlock of a single package of the corpus. * @@ -138,6 +169,7 @@ private static function updateCorpusManifest(): bool 'config' => [ 'preferred-install' => ['*' => 'dist'], 'allow-plugins' => false, + 'platform' => ['php' => self::CORPUS_PHP_VERSION], ], 'minimum-stability' => 'stable', 'prefer-stable' => true, diff --git a/bench/PhpDoc/Tools/TypeLangParserBench.php b/bench/PhpDoc/Tools/TypeLangParserBench.php index 6e7c6f5..9bd2577 100644 --- a/bench/PhpDoc/Tools/TypeLangParserBench.php +++ b/bench/PhpDoc/Tools/TypeLangParserBench.php @@ -22,7 +22,7 @@ final class TypeLangParserBench extends DocBlockParserBench public function prepare(): void { - $this->parser = new DocBlockParser(); + $this->parser = DocBlockParser::createDefault(); } #[ParamProviders('docBlocksDataProvider')] From 996c7606dda0f9f9113d940a19db434619db84b0 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:22:55 +0300 Subject: [PATCH 117/119] Apply phpcs --- libs/parser/src/Internal/CompiledExecutor.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libs/parser/src/Internal/CompiledExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php index 839476b..9782900 100644 --- a/libs/parser/src/Internal/CompiledExecutor.php +++ b/libs/parser/src/Internal/CompiledExecutor.php @@ -8,8 +8,8 @@ namespace TypeLang\Parser\Internal; -use TypeLang\Type; use TypeLang\Parser\Exception; +use TypeLang\Type; \interface_exists(\Phplrt\Contracts\Source\Exception\SourceExceptionInterface::class); \interface_exists(\Phplrt\Contracts\Source\ReadableInterface::class); @@ -2361,6 +2361,7 @@ private function reduceLiteralType(\Phplrt\Parser\Context $ctx, mixed $children) if ($this->features->literals === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset); } + return $children; } @@ -2481,15 +2482,15 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): case $suffix === null: return new Type\NamedTypeNode($children[0], null, null, $offset); - // Some\Any + // Some\Any case $suffix instanceof Type\Template\TemplateArgumentListNode: return new Type\NamedTypeNode($children[0], $suffix, null, $offset); - // Some\Any{name: T, ...} + // Some\Any{name: T, ...} case $suffix instanceof Type\Shape\FieldsListNode: return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); - // Some\Any(T, U): V + // Some\Any(T, U): V case $suffix instanceof Type\Callable\CallableParameterListNode: if ($this->features->callables === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); @@ -2497,7 +2498,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); - // Some\Any(T): U + // Some\Any(T): U case $suffix instanceof Type\Template\TemplateParameterListNode: if ($this->features->callables === false) { throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); @@ -2505,7 +2506,7 @@ private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); - // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* + // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* default: if (\count($children) === 3 && $children[2] instanceof Type\Identifier) { return new Type\ClassConstNode($children[0], $children[2], $offset); From 50cec840f3e1ff2832d2945b4d00c9caaa31a182 Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:31:07 +0300 Subject: [PATCH 118/119] Add in-memory parser reset method --- libs/parser/src/InMemoryTypeParser.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/parser/src/InMemoryTypeParser.php b/libs/parser/src/InMemoryTypeParser.php index dbcf807..5f87b1f 100644 --- a/libs/parser/src/InMemoryTypeParser.php +++ b/libs/parser/src/InMemoryTypeParser.php @@ -45,6 +45,13 @@ public function __construct( $this->sources = $sources ?? SourceFactory::createDefault(); } + public function reset(): void + { + $this->types = []; + $this->sequences = []; + $this->checks = []; + } + /** * @throws ParserExceptionInterface * @throws SourceExceptionInterface @@ -83,7 +90,6 @@ public function validate(#[Language('PHP')] mixed $source): CheckResult /** * @return non-empty-string - * @throws SourceExceptionInterface in case of source content reading error */ private function hash(ReadableInterface $source): string { From b802aa771cd5bd72d63211f315c2c09e7404e9bd Mon Sep 17 00:00:00 2001 From: SerafimArts Date: Sun, 13 Sep 2026 00:31:24 +0300 Subject: [PATCH 119/119] Improve parser's lex method --- libs/parser/src/TypeParser.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php index 6a6cab3..dadb5fe 100644 --- a/libs/parser/src/TypeParser.php +++ b/libs/parser/src/TypeParser.php @@ -5,15 +5,22 @@ namespace TypeLang\Parser; use JetBrains\PhpStorm\Language; +use Phplrt\Contracts\Lexer\TokenInterface; use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; use Phplrt\Contracts\Source\ReadableInterface; use Phplrt\Contracts\Source\SourceFactoryInterface; use Phplrt\Source\SourceFactory; +use TypeLang\Parser\Exception\ParserExceptionInterface; use TypeLang\Parser\Internal\Executor; use TypeLang\Parser\Partial\ParsedResult; use TypeLang\Parser\Validation\CheckResult; use TypeLang\Type\TypeNode; +/** + * @template-contravariant TSource of mixed = mixed + * + * @template-implements TypeParserInterface + */ final class TypeParser implements TypeParserInterface { private readonly SourceFactoryInterface $sources; @@ -45,6 +52,29 @@ public function withFeatures(bool ...$features): self ); } + /** + * Reads the provided source code into the tokens it is written of, + * building nothing out of them. + * + * The tokens are what every other method of this parser reads the source + * through, so this is the source as the grammar sees it: what a token is + * called, what it carries, and where it stands. + * + * ``` + * foreach ($parser->lex('array{ field: result }') as $token) { + * echo $token->name . ' ' . $token->value . \PHP_EOL; + * } + * + * // => T_NAME array + * // => T_BRACE_OPEN { + * // => ... + * ``` + * + * @param TSource $source source code to read + * @return iterable the tokens the source is + * written of + * @throws \Throwable in case of internal error occurs + */ public function lex(#[Language('PHP')] mixed $source): iterable { $executor = $this->getExecutor();
    Visibility Method or property visibility.
    AssertSubjectCodeReferenceWhat an assertion is written of: $value, + $this->items, $this->getItems() + or self::$instances.
    AuthorName string