diff --git a/scripts/test b/scripts/test index 44d59061..367280e2 100755 --- a/scripts/test +++ b/scripts/test @@ -14,6 +14,8 @@ ./vendor/bin/phpunit \ --enforce-time-limit \ - --testdox \ + --display-deprecations \ + --display-errors \ + --display-notices \ --coverage-html build/coverage-report \ --coverage-filter src $@ diff --git a/src/Definition/BinaryOperator.php b/src/Definition/BinaryOperator.php deleted file mode 100644 index a46e840b..00000000 --- a/src/Definition/BinaryOperator.php +++ /dev/null @@ -1,72 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Definition; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -enum BinaryOperator: string -{ - case AND = 'AND'; - case OR = 'OR'; - - case EQUAL = 'EQUAL'; - case NOT_EQUAL = 'NOT_EQUAL'; - case GREATER_THAN = 'GREATER_THAN'; - case GREATER_THAN_OR_EQUAL = 'GREATER_THAN_OR_EQUAL'; - case LESS_THAN = 'LESS_THAN'; - case LESS_THAN_OR_EQUAL = 'LESS_THAN_OR_EQUAL'; - - public static function fromTokenType(TokenType $tokenType): self - { - return match ($tokenType) { - TokenType::OPERATOR_BOOLEAN_AND => self::AND, - TokenType::OPERATOR_BOOLEAN_OR => self::OR, - - TokenType::COMPARATOR_EQUAL => self::EQUAL, - TokenType::COMPARATOR_NOT_EQUAL => self::NOT_EQUAL, - TokenType::COMPARATOR_GREATER_THAN => self::GREATER_THAN, - TokenType::COMPARATOR_GREATER_THAN_OR_EQUAL => self::GREATER_THAN_OR_EQUAL, - TokenType::COMPARATOR_LESS_THAN => self::LESS_THAN, - TokenType::COMPARATOR_LESS_THAN_OR_EQUAL => self::LESS_THAN_OR_EQUAL, - - default => throw new \Exception('@TODO: Unknown Binary Operator') - }; - } - - public function toPrecedence(): Precedence - { - return match ($this) { - self::AND => Precedence::LOGICAL_AND, - - self::OR => Precedence::LOGICAL_OR, - - self::EQUAL, - self::NOT_EQUAL => Precedence::EQUALITY, - - self::GREATER_THAN, - self::GREATER_THAN_OR_EQUAL, - self::LESS_THAN, - self::LESS_THAN_OR_EQUAL => Precedence::COMPARISON - }; - } -} diff --git a/src/Domain/AttributeName/AttributeName.php b/src/Domain/AttributeName/AttributeName.php new file mode 100644 index 00000000..fe01569d --- /dev/null +++ b/src/Domain/AttributeName/AttributeName.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\AttributeName; + +final class AttributeName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } +} diff --git a/src/Domain/ComponentName/ComponentName.php b/src/Domain/ComponentName/ComponentName.php new file mode 100644 index 00000000..5b021d99 --- /dev/null +++ b/src/Domain/ComponentName/ComponentName.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\ComponentName; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; + +final class ComponentName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } + + public function toTypeName(): TypeName + { + return TypeName::from($this->value); + } +} diff --git a/src/Definition/IntegerFormat.php b/src/Domain/EnumMemberName/EnumMemberName.php similarity index 55% rename from src/Definition/IntegerFormat.php rename to src/Domain/EnumMemberName/EnumMemberName.php index 5118d4c7..dc65ddb7 100644 --- a/src/Definition/IntegerFormat.php +++ b/src/Domain/EnumMemberName/EnumMemberName.php @@ -20,26 +20,22 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Definition; +namespace PackageFactory\ComponentEngine\Domain\EnumMemberName; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -enum IntegerFormat: string +final class EnumMemberName { - case BINARY = 'BINARY'; - case OCTAL = 'OCTAL'; - case DECIMAL = 'DECIMAL'; - case HEXADECIMAL = 'HEXADECIMAL'; + /** + * @var array + */ + private static array $instances = []; - public static function fromTokenType(TokenType $tokenType): self - { - return match ($tokenType) { - TokenType::NUMBER_BINARY => self::BINARY, - TokenType::NUMBER_OCTAL => self::OCTAL, - TokenType::NUMBER_DECIMAL => self::DECIMAL, - TokenType::NUMBER_HEXADECIMAL => self::HEXADECIMAL, + private function __construct( + public readonly string $value + ) { + } - default => throw new \Exception('@TODO: Unknown Integer Format: ' . $tokenType->value) - }; + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); } } diff --git a/src/Domain/EnumName/EnumName.php b/src/Domain/EnumName/EnumName.php new file mode 100644 index 00000000..c814f824 --- /dev/null +++ b/src/Domain/EnumName/EnumName.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\EnumName; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; + +final class EnumName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } + + public function toTypeName(): TypeName + { + return TypeName::from($this->value); + } +} diff --git a/src/Domain/PropertyName/PropertyName.php b/src/Domain/PropertyName/PropertyName.php new file mode 100644 index 00000000..e07fc050 --- /dev/null +++ b/src/Domain/PropertyName/PropertyName.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\PropertyName; + +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; + +final class PropertyName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } + + public function toEnumMemberName(): EnumMemberName + { + return EnumMemberName::from($this->value); + } +} diff --git a/src/Domain/StructName/StructName.php b/src/Domain/StructName/StructName.php new file mode 100644 index 00000000..df5eb7a1 --- /dev/null +++ b/src/Domain/StructName/StructName.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\StructName; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; + +final class StructName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } + + public function toTypeName(): TypeName + { + return TypeName::from($this->value); + } +} diff --git a/src/Domain/TagName/TagName.php b/src/Domain/TagName/TagName.php new file mode 100644 index 00000000..cc456689 --- /dev/null +++ b/src/Domain/TagName/TagName.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\TagName; + +final class TagName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } +} diff --git a/src/Domain/TypeName/TypeName.php b/src/Domain/TypeName/TypeName.php new file mode 100644 index 00000000..12d91d9c --- /dev/null +++ b/src/Domain/TypeName/TypeName.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\TypeName; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; + +final class TypeName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } + + public function toVariableName(): VariableName + { + return VariableName::from($this->value); + } +} diff --git a/src/Domain/TypeName/TypeNames.php b/src/Domain/TypeName/TypeNames.php new file mode 100644 index 00000000..b85d5a64 --- /dev/null +++ b/src/Domain/TypeName/TypeNames.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\TypeName; + +final class TypeNames +{ + /** + * @var TypeName[] + */ + public readonly array $items; + + public function __construct(TypeName ...$items) + { + $this->items = $items; + } + + public function toDebugString(): string + { + return join( + '|', + array_map( + static fn (TypeName $typeName) => $typeName->value, + $this->items + ) + ); + } +} diff --git a/src/Domain/VariableName/VariableName.php b/src/Domain/VariableName/VariableName.php new file mode 100644 index 00000000..bad9b091 --- /dev/null +++ b/src/Domain/VariableName/VariableName.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Domain\VariableName; + +final class VariableName +{ + /** + * @var array + */ + private static array $instances = []; + + private function __construct( + public readonly string $value + ) { + } + + public static function from(string $string): self + { + return self::$instances[$string] ??= new self($string); + } +} diff --git a/src/Framework/PHP/Singleton/Singleton.php b/src/Framework/PHP/Singleton/Singleton.php new file mode 100644 index 00000000..58933a05 --- /dev/null +++ b/src/Framework/PHP/Singleton/Singleton.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Framework\PHP\Singleton; + +trait Singleton +{ + private static ?self $instance = null; + + private function __construct() + { + } + + final public static function singleton(): static + { + return static::$instance ??= new static(); + } + + final public function __clone() + { + trigger_error('Cloning ' . __CLASS__ . ' is not allowed.', E_USER_ERROR); + } + + final public function __wakeup() + { + trigger_error('Unserializing ' . __CLASS__ . ' is not allowed.', E_USER_ERROR); + } +} diff --git a/src/Language/AST/ASTException.php b/src/Language/AST/ASTException.php new file mode 100644 index 00000000..516375fb --- /dev/null +++ b/src/Language/AST/ASTException.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST; + +use PackageFactory\ComponentEngine\Parser\Source\Range; + +abstract class ASTException extends \Exception +{ + protected function __construct( + int $code, + string $message, + public readonly ?Range $affectedRangeInSource = null + ) { + parent::__construct($message, $code); + } +} diff --git a/src/Language/AST/Node/Access/AccessKeyNode.php b/src/Language/AST/Node/Access/AccessKeyNode.php new file mode 100644 index 00000000..0dd4858c --- /dev/null +++ b/src/Language/AST/Node/Access/AccessKeyNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Access; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class AccessKeyNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly PropertyName $value + ) { + } +} diff --git a/src/Language/AST/Node/Access/AccessNode.php b/src/Language/AST/Node/Access/AccessNode.php new file mode 100644 index 00000000..50c31717 --- /dev/null +++ b/src/Language/AST/Node/Access/AccessNode.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Access; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class AccessNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly ExpressionNode $parent, + public readonly AccessType $type, + public readonly AccessKeyNode $key + ) { + } +} diff --git a/src/Definition/AccessType.php b/src/Language/AST/Node/Access/AccessType.php similarity index 64% rename from src/Definition/AccessType.php rename to src/Language/AST/Node/Access/AccessType.php index e628b952..ee2fc74b 100644 --- a/src/Definition/AccessType.php +++ b/src/Language/AST/Node/Access/AccessType.php @@ -2,7 +2,7 @@ /** * PackageFactory.ComponentEngine - Universal View Components for PHP - * Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine + * Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,22 +20,10 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Definition; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +namespace PackageFactory\ComponentEngine\Language\AST\Node\Access; enum AccessType: string { case MANDATORY = 'MANDATORY'; case OPTIONAL = 'OPTIONAL'; - - public static function fromTokenType(TokenType $tokenType): self - { - return match ($tokenType) { - TokenType::PERIOD => self::MANDATORY, - TokenType::OPTCHAIN => self::OPTIONAL, - - default => throw new \Exception('@TODO: Unknown AccessType') - }; - } } diff --git a/src/Language/AST/Node/BinaryOperation/BinaryOperationNode.php b/src/Language/AST/Node/BinaryOperation/BinaryOperationNode.php new file mode 100644 index 00000000..cbf4a7ff --- /dev/null +++ b/src/Language/AST/Node/BinaryOperation/BinaryOperationNode.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class BinaryOperationNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly ExpressionNode $leftOperand, + public readonly BinaryOperator $operator, + public readonly ExpressionNode $rightOperand + ) { + } +} diff --git a/src/Language/AST/Node/BinaryOperation/BinaryOperator.php b/src/Language/AST/Node/BinaryOperation/BinaryOperator.php new file mode 100644 index 00000000..128c2536 --- /dev/null +++ b/src/Language/AST/Node/BinaryOperation/BinaryOperator.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation; + +enum BinaryOperator: string +{ + case AND = 'AND'; + case OR = 'OR'; + + case EQUAL = 'EQUAL'; + case NOT_EQUAL = 'NOT_EQUAL'; + case GREATER_THAN = 'GREATER_THAN'; + case GREATER_THAN_OR_EQUAL = 'GREATER_THAN_OR_EQUAL'; + case LESS_THAN = 'LESS_THAN'; + case LESS_THAN_OR_EQUAL = 'LESS_THAN_OR_EQUAL'; +} diff --git a/src/Language/AST/Node/BooleanLiteral/BooleanLiteralNode.php b/src/Language/AST/Node/BooleanLiteral/BooleanLiteralNode.php new file mode 100644 index 00000000..ec6476f2 --- /dev/null +++ b/src/Language/AST/Node/BooleanLiteral/BooleanLiteralNode.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class BooleanLiteralNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly bool $value + ) { + } +} diff --git a/src/Language/AST/Node/ComponentDeclaration/ComponentDeclarationNode.php b/src/Language/AST/Node/ComponentDeclaration/ComponentDeclarationNode.php new file mode 100644 index 00000000..68e31865 --- /dev/null +++ b/src/Language/AST/Node/ComponentDeclaration/ComponentDeclarationNode.php @@ -0,0 +1,39 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ComponentDeclarationNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly ComponentNameNode $name, + public readonly PropertyDeclarationNodes $props, + public readonly ExpressionNode $return + ) { + } +} diff --git a/src/Language/AST/Node/ComponentDeclaration/ComponentNameNode.php b/src/Language/AST/Node/ComponentDeclaration/ComponentNameNode.php new file mode 100644 index 00000000..f3bba98d --- /dev/null +++ b/src/Language/AST/Node/ComponentDeclaration/ComponentNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration; + +use PackageFactory\ComponentEngine\Domain\ComponentName\ComponentName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ComponentNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly ComponentName $value + ) { + } +} diff --git a/src/Language/AST/Node/EnumDeclaration/EnumDeclarationNode.php b/src/Language/AST/Node/EnumDeclaration/EnumDeclarationNode.php new file mode 100644 index 00000000..3ecb6e82 --- /dev/null +++ b/src/Language/AST/Node/EnumDeclaration/EnumDeclarationNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class EnumDeclarationNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly EnumNameNode $name, + public readonly EnumMemberDeclarationNodes $members + ) { + } +} diff --git a/src/Language/AST/Node/EnumDeclaration/EnumMemberDeclarationNode.php b/src/Language/AST/Node/EnumDeclaration/EnumMemberDeclarationNode.php new file mode 100644 index 00000000..261a434d --- /dev/null +++ b/src/Language/AST/Node/EnumDeclaration/EnumMemberDeclarationNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class EnumMemberDeclarationNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly EnumMemberNameNode $name, + public readonly ?EnumMemberValueNode $value + ) { + } +} diff --git a/src/Language/AST/Node/EnumDeclaration/EnumMemberDeclarationNodes.php b/src/Language/AST/Node/EnumDeclaration/EnumMemberDeclarationNodes.php new file mode 100644 index 00000000..f1f2d23c --- /dev/null +++ b/src/Language/AST/Node/EnumDeclaration/EnumMemberDeclarationNodes.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration; + +final class EnumMemberDeclarationNodes +{ + /** + * @var array + */ + public readonly array $items; + + public function __construct(EnumMemberDeclarationNode ...$items) + { + $itemsAsHashMap = []; + foreach ($items as $item) { + $itemsAsHashMap[$item->name->value->value] = $item; + } + + $this->items = $itemsAsHashMap; + } +} diff --git a/src/Language/AST/Node/EnumDeclaration/EnumMemberNameNode.php b/src/Language/AST/Node/EnumDeclaration/EnumMemberNameNode.php new file mode 100644 index 00000000..d7c6dc00 --- /dev/null +++ b/src/Language/AST/Node/EnumDeclaration/EnumMemberNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration; + +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class EnumMemberNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly EnumMemberName $value + ) { + } +} diff --git a/src/Language/AST/Node/EnumDeclaration/EnumMemberValueNode.php b/src/Language/AST/Node/EnumDeclaration/EnumMemberValueNode.php new file mode 100644 index 00000000..4801e044 --- /dev/null +++ b/src/Language/AST/Node/EnumDeclaration/EnumMemberValueNode.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration; + +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class EnumMemberValueNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly StringLiteralNode | IntegerLiteralNode $value + ) { + } +} diff --git a/src/Language/AST/Node/EnumDeclaration/EnumNameNode.php b/src/Language/AST/Node/EnumDeclaration/EnumNameNode.php new file mode 100644 index 00000000..ae82df47 --- /dev/null +++ b/src/Language/AST/Node/EnumDeclaration/EnumNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration; + +use PackageFactory\ComponentEngine\Domain\EnumName\EnumName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class EnumNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly EnumName $value + ) { + } +} diff --git a/src/Language/AST/Node/Export/ExportNode.php b/src/Language/AST/Node/Export/ExportNode.php new file mode 100644 index 00000000..a316bf6d --- /dev/null +++ b/src/Language/AST/Node/Export/ExportNode.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Export; + +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ExportNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly ComponentDeclarationNode | EnumDeclarationNode | StructDeclarationNode $declaration + ) { + } +} diff --git a/src/Language/AST/Node/Expression/ExpressionNode.php b/src/Language/AST/Node/Expression/ExpressionNode.php new file mode 100644 index 00000000..d770a575 --- /dev/null +++ b/src/Language/AST/Node/Expression/ExpressionNode.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Expression; + +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ExpressionNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly AccessNode | BinaryOperationNode | BooleanLiteralNode | IntegerLiteralNode | MatchNode | NullLiteralNode | StringLiteralNode | TagNode | TemplateLiteralNode | TernaryOperationNode | UnaryOperationNode | ValueReferenceNode $root + ) { + } +} diff --git a/src/Language/AST/Node/Expression/ExpressionNodes.php b/src/Language/AST/Node/Expression/ExpressionNodes.php new file mode 100644 index 00000000..d6725791 --- /dev/null +++ b/src/Language/AST/Node/Expression/ExpressionNodes.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Expression; + +final class ExpressionNodes +{ + /** + * @var ExpressionNode[] + */ + public readonly array $items; + + public function __construct(ExpressionNode ...$items) + { + $this->items = $items; + } +} diff --git a/src/Language/AST/Node/Import/ImportNode.php b/src/Language/AST/Node/Import/ImportNode.php new file mode 100644 index 00000000..fbcdaff0 --- /dev/null +++ b/src/Language/AST/Node/Import/ImportNode.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Import; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ImportNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly StringLiteralNode $path, + public readonly ImportedNameNodes $names + ) { + } +} diff --git a/src/Language/AST/Node/Import/ImportNodes.php b/src/Language/AST/Node/Import/ImportNodes.php new file mode 100644 index 00000000..087aa9d1 --- /dev/null +++ b/src/Language/AST/Node/Import/ImportNodes.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Import; + +final class ImportNodes +{ + /** + * @var ImportNode[] + */ + public readonly array $items; + + public function __construct(ImportNode ...$items) + { + $this->items = $items; + } +} diff --git a/src/Language/AST/Node/Import/ImportedNameNode.php b/src/Language/AST/Node/Import/ImportedNameNode.php new file mode 100644 index 00000000..2e88a582 --- /dev/null +++ b/src/Language/AST/Node/Import/ImportedNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Import; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ImportedNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly VariableName $value + ) { + } +} diff --git a/src/Language/AST/Node/Import/ImportedNameNodes.php b/src/Language/AST/Node/Import/ImportedNameNodes.php new file mode 100644 index 00000000..f4fcf2de --- /dev/null +++ b/src/Language/AST/Node/Import/ImportedNameNodes.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Import; + +final class ImportedNameNodes +{ + /** + * @var ImportedNameNode[] + */ + public readonly array $items; + + public function __construct(ImportedNameNode ...$items) + { + if (count($items) === 0) { + throw InvalidImportedNameNodes::becauseTheyWereEmpty(); + } + + $typeNames = []; + foreach ($items as $item) { + if (isset($typeNames[$item->value->value])) { + throw InvalidImportedNameNodes::becauseTheyContainDuplicates( + duplicateImportedNameNode: $item + ); + } + + $typeNames[$item->value->value] = true; + } + + $this->items = $items; + } +} diff --git a/src/Language/AST/Node/Import/InvalidImportedNameNodes.php b/src/Language/AST/Node/Import/InvalidImportedNameNodes.php new file mode 100644 index 00000000..76a368ef --- /dev/null +++ b/src/Language/AST/Node/Import/InvalidImportedNameNodes.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Import; + +use PackageFactory\ComponentEngine\Language\AST\ASTException; + +final class InvalidImportedNameNodes extends ASTException +{ + public static function becauseTheyWereEmpty(): self + { + return new self( + code: 1691163487, + message: 'An import statement must import at least one name.' + ); + } + + public static function becauseTheyContainDuplicates( + ImportedNameNode $duplicateImportedNameNode + ): self { + return new self( + code: 1691163492, + message: 'An import statement must not import duplicate names.', + affectedRangeInSource: $duplicateImportedNameNode->rangeInSource + ); + } +} diff --git a/src/Definition/UnaryOperator.php b/src/Language/AST/Node/IntegerLiteral/IntegerFormat.php similarity index 71% rename from src/Definition/UnaryOperator.php rename to src/Language/AST/Node/IntegerLiteral/IntegerFormat.php index 2e7c6f32..445ffd10 100644 --- a/src/Definition/UnaryOperator.php +++ b/src/Language/AST/Node/IntegerLiteral/IntegerFormat.php @@ -20,19 +20,14 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Definition; +namespace PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; -enum UnaryOperator: string +enum IntegerFormat: string { - case NOT = 'NOT'; - - public static function fromTokenType(TokenType $tokenType): self - { - return match ($tokenType) { - TokenType::OPERATOR_BOOLEAN_NOT => self::NOT, - default => throw new \Exception('@TODO: Unknown Unary Operator') - }; - } + case BINARY = 'BINARY'; + case OCTAL = 'OCTAL'; + case DECIMAL = 'DECIMAL'; + case HEXADECIMAL = 'HEXADECIMAL'; } diff --git a/src/Language/AST/Node/IntegerLiteral/IntegerLiteralNode.php b/src/Language/AST/Node/IntegerLiteral/IntegerLiteralNode.php new file mode 100644 index 00000000..1ab2631a --- /dev/null +++ b/src/Language/AST/Node/IntegerLiteral/IntegerLiteralNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class IntegerLiteralNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly IntegerFormat $format, + public readonly string $value + ) { + } +} diff --git a/src/Language/AST/Node/Match/InvalidMatchArmNodes.php b/src/Language/AST/Node/Match/InvalidMatchArmNodes.php new file mode 100644 index 00000000..8f98e653 --- /dev/null +++ b/src/Language/AST/Node/Match/InvalidMatchArmNodes.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Match; + +use PackageFactory\ComponentEngine\Language\AST\ASTException; + +final class InvalidMatchArmNodes extends ASTException +{ + public static function becauseTheyWereEmpty(): self + { + return new self( + code: 1691150119, + message: 'A match statement must contain at least one match arm.' + ); + } + + public static function becauseTheyContainMoreThanOneDefaultMatchArmNode( + MatchArmNode $secondDefaultMatchArmNode + ): self + { + return new self( + code: 1691150238, + message: 'A match statement must not contain more than one default match arm.', + affectedRangeInSource: $secondDefaultMatchArmNode->rangeInSource + ); + } +} diff --git a/src/Language/AST/Node/Match/MatchArmNode.php b/src/Language/AST/Node/Match/MatchArmNode.php new file mode 100644 index 00000000..735c313b --- /dev/null +++ b/src/Language/AST/Node/Match/MatchArmNode.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Match; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class MatchArmNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly null | ExpressionNodes $left, + public readonly ExpressionNode $right + ) { + } + + public function isDefault(): bool + { + return is_null($this->left); + } +} diff --git a/src/Language/AST/Node/Match/MatchArmNodes.php b/src/Language/AST/Node/Match/MatchArmNodes.php new file mode 100644 index 00000000..46785cd8 --- /dev/null +++ b/src/Language/AST/Node/Match/MatchArmNodes.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Match; + +final class MatchArmNodes +{ + /** + * @var MatchArmNode[] + */ + public readonly array $items; + + private ?MatchArmNode $defaultArm = null; + + public function __construct(MatchArmNode ...$items) + { + if (count($items) === 0) { + throw InvalidMatchArmNodes::becauseTheyWereEmpty(); + } + + foreach ($items as $item) { + if ($item->isDefault()) { + if (is_null($this->defaultArm)) { + $this->defaultArm = $item; + } else { + throw InvalidMatchArmNodes::becauseTheyContainMoreThanOneDefaultMatchArmNode( + secondDefaultMatchArmNode: $item + ); + } + } + } + + $this->items = $items; + } +} diff --git a/src/Language/AST/Node/Match/MatchNode.php b/src/Language/AST/Node/Match/MatchNode.php new file mode 100644 index 00000000..4ee8a357 --- /dev/null +++ b/src/Language/AST/Node/Match/MatchNode.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Match; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class MatchNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly ExpressionNode $subject, + public readonly MatchArmNodes $arms + ) { + } +} diff --git a/src/Language/AST/Node/Module/ModuleNode.php b/src/Language/AST/Node/Module/ModuleNode.php new file mode 100644 index 00000000..468348b4 --- /dev/null +++ b/src/Language/AST/Node/Module/ModuleNode.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Module; + +use PackageFactory\ComponentEngine\Language\AST\Node\Export\ExportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ModuleNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly ImportNodes $imports, + public readonly ExportNode $export, + ) { + } +} diff --git a/src/Language/AST/Node/Node.php b/src/Language/AST/Node/Node.php new file mode 100644 index 00000000..c65f9ce6 --- /dev/null +++ b/src/Language/AST/Node/Node.php @@ -0,0 +1,33 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node; + +use PackageFactory\ComponentEngine\Parser\Source\Range; + +abstract class Node +{ + protected function __construct( + public readonly Range $rangeInSource + ) { + } +} diff --git a/src/Language/AST/Node/NullLiteral/NullLiteralNode.php b/src/Language/AST/Node/NullLiteral/NullLiteralNode.php new file mode 100644 index 00000000..ac096b85 --- /dev/null +++ b/src/Language/AST/Node/NullLiteral/NullLiteralNode.php @@ -0,0 +1,34 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class NullLiteralNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource + ) { + } +} diff --git a/src/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNode.php b/src/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNode.php new file mode 100644 index 00000000..8845b8e4 --- /dev/null +++ b/src/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNode.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class PropertyDeclarationNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly PropertyNameNode $name, + public readonly TypeReferenceNode $type + ) { + } +} diff --git a/src/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNodes.php b/src/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNodes.php new file mode 100644 index 00000000..1d18747f --- /dev/null +++ b/src/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNodes.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration; + +final class PropertyDeclarationNodes +{ + /** + * @var array + */ + public readonly array $items; + + public function __construct(PropertyDeclarationNode ...$items) + { + $itemsAsHashMap = []; + foreach ($items as $item) { + $itemsAsHashMap[$item->name->value->value] = $item; + } + + $this->items = $itemsAsHashMap; + } + + public function isEmpty(): bool + { + return count($this->items) === 0; + } +} diff --git a/src/Language/AST/Node/PropertyDeclaration/PropertyNameNode.php b/src/Language/AST/Node/PropertyDeclaration/PropertyNameNode.php new file mode 100644 index 00000000..d7b59d20 --- /dev/null +++ b/src/Language/AST/Node/PropertyDeclaration/PropertyNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class PropertyNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly PropertyName $value + ) { + } +} diff --git a/src/Language/AST/Node/StringLiteral/StringLiteralNode.php b/src/Language/AST/Node/StringLiteral/StringLiteralNode.php new file mode 100644 index 00000000..c7954867 --- /dev/null +++ b/src/Language/AST/Node/StringLiteral/StringLiteralNode.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class StringLiteralNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly string $value + ) { + } +} diff --git a/src/Language/AST/Node/StructDeclaration/StructDeclarationNode.php b/src/Language/AST/Node/StructDeclaration/StructDeclarationNode.php new file mode 100644 index 00000000..e19a0c78 --- /dev/null +++ b/src/Language/AST/Node/StructDeclaration/StructDeclarationNode.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class StructDeclarationNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly StructNameNode $name, + public readonly PropertyDeclarationNodes $properties + ) { + } +} diff --git a/src/Language/AST/Node/StructDeclaration/StructNameNode.php b/src/Language/AST/Node/StructDeclaration/StructNameNode.php new file mode 100644 index 00000000..95aa76d7 --- /dev/null +++ b/src/Language/AST/Node/StructDeclaration/StructNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration; + +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class StructNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly StructName $value + ) { + } +} diff --git a/src/Language/AST/Node/Tag/AttributeNameNode.php b/src/Language/AST/Node/Tag/AttributeNameNode.php new file mode 100644 index 00000000..181f4101 --- /dev/null +++ b/src/Language/AST/Node/Tag/AttributeNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag; + +use PackageFactory\ComponentEngine\Domain\AttributeName\AttributeName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class AttributeNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly AttributeName $value + ) { + } +} diff --git a/src/Language/AST/Node/Tag/AttributeNode.php b/src/Language/AST/Node/Tag/AttributeNode.php new file mode 100644 index 00000000..1033c40c --- /dev/null +++ b/src/Language/AST/Node/Tag/AttributeNode.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class AttributeNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly AttributeNameNode $name, + public readonly null | ExpressionNode | StringLiteralNode $value + ) { + } +} diff --git a/src/Language/AST/Node/Tag/AttributeNodes.php b/src/Language/AST/Node/Tag/AttributeNodes.php new file mode 100644 index 00000000..25dbc166 --- /dev/null +++ b/src/Language/AST/Node/Tag/AttributeNodes.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag; + +final class AttributeNodes +{ + /** + * @var array + */ + public readonly array $items; + + public function __construct(AttributeNode ...$items) + { + $itemsAsHashMap = []; + foreach ($items as $attribute) { + $itemsAsHashMap[$attribute->name->value->value] = $attribute; + } + + $this->items = $itemsAsHashMap; + } +} diff --git a/src/Language/AST/Node/Tag/ChildNodes.php b/src/Language/AST/Node/Tag/ChildNodes.php new file mode 100644 index 00000000..a05f7250 --- /dev/null +++ b/src/Language/AST/Node/Tag/ChildNodes.php @@ -0,0 +1,39 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; + +final class ChildNodes +{ + /** + * @var (TextNode|ExpressionNode|TagNode)[] + */ + public readonly array $items; + + public function __construct(TextNode | ExpressionNode | TagNode ...$items) + { + $this->items = $items; + } +} diff --git a/src/Language/AST/Node/Tag/TagNameNode.php b/src/Language/AST/Node/Tag/TagNameNode.php new file mode 100644 index 00000000..1bf76835 --- /dev/null +++ b/src/Language/AST/Node/Tag/TagNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag; + +use PackageFactory\ComponentEngine\Domain\TagName\TagName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TagNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly TagName $value + ) { + } +} diff --git a/src/Language/AST/Node/Tag/TagNode.php b/src/Language/AST/Node/Tag/TagNode.php new file mode 100644 index 00000000..aa471551 --- /dev/null +++ b/src/Language/AST/Node/Tag/TagNode.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Tag; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TagNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly TagNameNode $name, + public readonly AttributeNodes $attributes, + public readonly ChildNodes $children, + public readonly bool $isSelfClosing + ) { + } +} diff --git a/src/Language/AST/Node/TemplateLiteral/TemplateLiteralExpressionSegmentNode.php b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralExpressionSegmentNode.php new file mode 100644 index 00000000..7b9cfdb5 --- /dev/null +++ b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralExpressionSegmentNode.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TemplateLiteralExpressionSegmentNode extends Node +{ + + public function __construct( + public readonly Range $rangeInSource, + public readonly ExpressionNode $expression + ) { + } +} diff --git a/src/Language/AST/Node/TemplateLiteral/TemplateLiteralNode.php b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralNode.php new file mode 100644 index 00000000..1fbbabc9 --- /dev/null +++ b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralNode.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TemplateLiteralNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly TemplateLiteralSegments $segments + ) { + } +} diff --git a/src/Language/AST/Node/TemplateLiteral/TemplateLiteralSegments.php b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralSegments.php new file mode 100644 index 00000000..f5c0c066 --- /dev/null +++ b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralSegments.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral; + +final class TemplateLiteralSegments +{ + /** + * @var (TemplateLiteralStringSegmentNode|TemplateLiteralExpressionSegmentNode)[] + */ + public readonly array $items; + + public function __construct( + TemplateLiteralStringSegmentNode | TemplateLiteralExpressionSegmentNode ...$items + ) { + $this->items = $items; + } +} diff --git a/src/Language/AST/Node/TemplateLiteral/TemplateLiteralStringSegmentNode.php b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralStringSegmentNode.php new file mode 100644 index 00000000..5cce5609 --- /dev/null +++ b/src/Language/AST/Node/TemplateLiteral/TemplateLiteralStringSegmentNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TemplateLiteralStringSegmentNode extends Node +{ + + public function __construct( + public readonly Range $rangeInSource, + public readonly string $value + ) { + } +} diff --git a/src/Language/AST/Node/TernaryOperation/TernaryOperationNode.php b/src/Language/AST/Node/TernaryOperation/TernaryOperationNode.php new file mode 100644 index 00000000..9c73556f --- /dev/null +++ b/src/Language/AST/Node/TernaryOperation/TernaryOperationNode.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TernaryOperationNode extends Node +{ + public function __construct( + public readonly ExpressionNode $condition, + public readonly ExpressionNode $trueBranch, + public readonly ExpressionNode $falseBranch + ) { + parent::__construct( + rangeInSource: Range::from( + $condition->rangeInSource->start, + $falseBranch->rangeInSource->end + ) + ); + } +} diff --git a/src/Language/AST/Node/Text/TextNode.php b/src/Language/AST/Node/Text/TextNode.php new file mode 100644 index 00000000..d17cd868 --- /dev/null +++ b/src/Language/AST/Node/Text/TextNode.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\Text; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TextNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly string $value + ) { + } +} diff --git a/src/Language/AST/Node/TypeReference/InvalidTypeNameNodes.php b/src/Language/AST/Node/TypeReference/InvalidTypeNameNodes.php new file mode 100644 index 00000000..9864f8d3 --- /dev/null +++ b/src/Language/AST/Node/TypeReference/InvalidTypeNameNodes.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TypeReference; + +use PackageFactory\ComponentEngine\Language\AST\ASTException; + +final class InvalidTypeNameNodes extends ASTException +{ + public static function becauseTheyWereEmpty(): self + { + return new self( + code: 1690549442, + message: 'A type reference must refer to at least one type name.' + ); + } + + public static function becauseTheyContainDuplicates( + TypeNameNode $duplicateTypeNameNode + ): self { + return new self( + code: 1690551330, + message: 'A type reference must not contain duplicates.', + affectedRangeInSource: $duplicateTypeNameNode->rangeInSource + ); + } +} diff --git a/src/Language/AST/Node/TypeReference/InvalidTypeReferenceNode.php b/src/Language/AST/Node/TypeReference/InvalidTypeReferenceNode.php new file mode 100644 index 00000000..abee1885 --- /dev/null +++ b/src/Language/AST/Node/TypeReference/InvalidTypeReferenceNode.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TypeReference; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; +use PackageFactory\ComponentEngine\Language\AST\ASTException; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class InvalidTypeReferenceNode extends ASTException +{ + public static function becauseItWasOptionalAndArrayAtTheSameTime( + TypeNames $affectedTypeNames, + Range $affectedRangeInSource + ): self { + return new self( + code: 1690538480, + message: sprintf( + 'The type reference to "%s" must not be optional and array at the same time.', + $affectedTypeNames->toDebugString() + ), + affectedRangeInSource: $affectedRangeInSource + ); + } + + public static function becauseItWasUnionAndArrayAtTheSameTime( + TypeNames $affectedTypeNames, + Range $affectedRangeInSource + ): self { + return new self( + code: 1690552344, + message: sprintf( + 'The type reference to "%s" must not be union and array at the same time.', + $affectedTypeNames->toDebugString() + ), + affectedRangeInSource: $affectedRangeInSource + ); + } + + public static function becauseItWasUnionAndOptionalAtTheSameTime( + TypeNames $affectedTypeNames, + Range $affectedRangeInSource + ): self { + return new self( + code: 1690552586, + message: sprintf( + 'The type reference to "%s" must not be union and optional at the same time.', + $affectedTypeNames->toDebugString() + ), + affectedRangeInSource: $affectedRangeInSource + ); + } +} diff --git a/src/Language/AST/Node/TypeReference/TypeNameNode.php b/src/Language/AST/Node/TypeReference/TypeNameNode.php new file mode 100644 index 00000000..bf928439 --- /dev/null +++ b/src/Language/AST/Node/TypeReference/TypeNameNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TypeReference; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TypeNameNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly TypeName $value + ) { + } +} diff --git a/src/Language/AST/Node/TypeReference/TypeNameNodes.php b/src/Language/AST/Node/TypeReference/TypeNameNodes.php new file mode 100644 index 00000000..59db01f8 --- /dev/null +++ b/src/Language/AST/Node/TypeReference/TypeNameNodes.php @@ -0,0 +1,79 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TypeReference; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; + +final class TypeNameNodes +{ + /** + * @var TypeNameNode[] + */ + public readonly array $items; + + private ?TypeNames $cachedTypeNames = null; + + public function __construct(TypeNameNode ...$items) + { + if (count($items) === 0) { + throw InvalidTypeNameNodes::becauseTheyWereEmpty(); + } + + $typeNames = []; + foreach ($items as $item) { + if (isset($typeNames[$item->value->value])) { + throw InvalidTypeNameNodes::becauseTheyContainDuplicates( + duplicateTypeNameNode: $item + ); + } + + $typeNames[$item->value->value] = true; + } + + $this->items = $items; + } + + public function getSize(): int + { + return count($this->items); + } + + public function getLast(): TypeNameNode + { + return $this->items[$this->getSize() - 1]; + } + + public function toTypeNames(): TypeNames + { + if ($this->cachedTypeNames === null) { + $typeNamesAsArray = array_map( + static fn (TypeNameNode $typeNameNode) => $typeNameNode->value, + $this->items + ); + + $this->cachedTypeNames = new TypeNames(...$typeNamesAsArray); + } + + return $this->cachedTypeNames; + } +} diff --git a/src/Language/AST/Node/TypeReference/TypeReferenceNode.php b/src/Language/AST/Node/TypeReference/TypeReferenceNode.php new file mode 100644 index 00000000..10c61808 --- /dev/null +++ b/src/Language/AST/Node/TypeReference/TypeReferenceNode.php @@ -0,0 +1,57 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\TypeReference; + +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class TypeReferenceNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly TypeNameNodes $names, + public readonly bool $isArray, + public readonly bool $isOptional + ) { + if ($isArray === true && $isOptional === true) { + throw InvalidTypeReferenceNode::becauseItWasOptionalAndArrayAtTheSameTime( + affectedTypeNames: $names->toTypeNames(), + affectedRangeInSource: $rangeInSource + ); + } + + if ($names->getSize() > 1 && $isArray === true) { + throw InvalidTypeReferenceNode::becauseItWasUnionAndArrayAtTheSameTime( + affectedTypeNames: $names->toTypeNames(), + affectedRangeInSource: $rangeInSource + ); + } + + if ($names->getSize() > 1 && $isOptional === true) { + throw InvalidTypeReferenceNode::becauseItWasUnionAndOptionalAtTheSameTime( + affectedTypeNames: $names->toTypeNames(), + affectedRangeInSource: $rangeInSource + ); + } + } +} diff --git a/src/Language/AST/Node/UnaryOperation/UnaryOperationNode.php b/src/Language/AST/Node/UnaryOperation/UnaryOperationNode.php new file mode 100644 index 00000000..b55c215c --- /dev/null +++ b/src/Language/AST/Node/UnaryOperation/UnaryOperationNode.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation; + +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class UnaryOperationNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly UnaryOperator $operator, + public readonly ExpressionNode $operand + ) { + } +} diff --git a/src/Language/AST/Node/UnaryOperation/UnaryOperator.php b/src/Language/AST/Node/UnaryOperation/UnaryOperator.php new file mode 100644 index 00000000..37845208 --- /dev/null +++ b/src/Language/AST/Node/UnaryOperation/UnaryOperator.php @@ -0,0 +1,28 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation; + +enum UnaryOperator: string +{ + case NOT = 'NOT'; +} diff --git a/src/Language/AST/Node/ValueReference/ValueReferenceNode.php b/src/Language/AST/Node/ValueReference/ValueReferenceNode.php new file mode 100644 index 00000000..7168bc2c --- /dev/null +++ b/src/Language/AST/Node/ValueReference/ValueReferenceNode.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\AST\Node\ValueReference; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Node; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ValueReferenceNode extends Node +{ + public function __construct( + public readonly Range $rangeInSource, + public readonly VariableName $name + ) { + } +} diff --git a/src/Parser/Ast/BooleanLiteralNode.php b/src/Language/Parser/BooleanLiteral/BooleanLiteralParser.php similarity index 65% rename from src/Parser/Ast/BooleanLiteralNode.php rename to src/Language/Parser/BooleanLiteral/BooleanLiteralParser.php index 0af52a8d..f353929b 100644 --- a/src/Parser/Ast/BooleanLiteralNode.php +++ b/src/Language/Parser/BooleanLiteral/BooleanLiteralParser.php @@ -2,7 +2,7 @@ /** * PackageFactory.ComponentEngine - Universal View Components for PHP - * Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine + * Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,41 +20,34 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Parser\Ast; +namespace PackageFactory\ComponentEngine\Language\Parser\BooleanLiteral; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; -final class BooleanLiteralNode implements \JsonSerializable +final class BooleanLiteralParser { - private function __construct( - public readonly bool $value - ) { - } + use Singleton; /** * @param \Iterator $tokens - * @return self + * @return BooleanLiteralNode */ - public static function fromTokens(\Iterator $tokens): self + public function parse(\Iterator &$tokens): BooleanLiteralNode { Scanner::assertType($tokens, TokenType::KEYWORD_TRUE, TokenType::KEYWORD_FALSE); - $value = Scanner::type($tokens) === TokenType::KEYWORD_TRUE; + $token = $tokens->current(); + $value = $token->type === TokenType::KEYWORD_TRUE; Scanner::skipOne($tokens); - return new self( + return new BooleanLiteralNode( + rangeInSource: $token->boundaries, value: $value ); } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'BooleanLiteralNode', - 'payload' => $this->value - ]; - } } diff --git a/src/Language/Parser/ComponentDeclaration/ComponentDeclarationParser.php b/src/Language/Parser/ComponentDeclaration/ComponentDeclarationParser.php new file mode 100644 index 00000000..2a3f176d --- /dev/null +++ b/src/Language/Parser/ComponentDeclaration/ComponentDeclarationParser.php @@ -0,0 +1,177 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\ComponentDeclaration; + +use PackageFactory\ComponentEngine\Domain\ComponentName\ComponentName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Language\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Language\Parser\PropertyDeclaration\PropertyDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class ComponentDeclarationParser +{ + use Singleton; + + private ?PropertyDeclarationParser $propertyDeclarationParser = null; + private ?ExpressionParser $returnParser = null; + + /** + * @param \Iterator $tokens + * @return ComponentDeclarationNode + */ + public function parse(\Iterator &$tokens): ComponentDeclarationNode + { + $componentKeywordToken = $this->extractComponentKeywordToken($tokens); + $name = $this->parseName($tokens); + + $this->skipOpeningBracketToken($tokens); + + $props = $this->parseProps($tokens); + + $this->skipReturnKeywordToken($tokens); + + $return = $this->parseReturn($tokens); + $closingBracketToken = $this->extractClosingBracketToken($tokens); + + return new ComponentDeclarationNode( + rangeInSource: Range::from( + $componentKeywordToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + name: $name, + props: $props, + return: $return + ); + } + + /** + * @param \Iterator $tokens + * @return Token + */ + private function extractComponentKeywordToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::KEYWORD_COMPONENT); + + $componentKeywordToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return $componentKeywordToken; + } + + /** + * @param \Iterator $tokens + * @return ComponentNameNode + */ + private function parseName(\Iterator &$tokens): ComponentNameNode + { + Scanner::assertType($tokens, TokenType::STRING); + + $componentNameToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return new ComponentNameNode( + rangeInSource: $componentNameToken->boundaries, + value: ComponentName::from($componentNameToken->value) + ); + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipOpeningBracketToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + } + + /** + * @param \Iterator $tokens + * @return PropertyDeclarationNodes + */ + private function parseProps(\Iterator &$tokens): PropertyDeclarationNodes + { + $this->propertyDeclarationParser ??= PropertyDeclarationParser::singleton(); + + $items = []; + while (Scanner::type($tokens) !== TokenType::KEYWORD_RETURN) { + assert($this->propertyDeclarationParser !== null); + $items[] = $this->propertyDeclarationParser->parse($tokens); + + Scanner::skipSpaceAndComments($tokens); + } + + return new PropertyDeclarationNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipReturnKeywordToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::KEYWORD_RETURN); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseReturn(\Iterator &$tokens): ExpressionNode + { + $this->returnParser ??= new ExpressionParser( + stopAt: TokenType::BRACKET_CURLY_CLOSE + ); + + return $this->returnParser->parse($tokens); + } + + /** + * @param \Iterator $tokens + * @return Token + */ + private function extractClosingBracketToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); + + $closingBracketToken = $tokens->current(); + + Scanner::skipOne($tokens); + + return $closingBracketToken; + } +} diff --git a/src/Language/Parser/EnumDeclaration/EnumDeclarationParser.php b/src/Language/Parser/EnumDeclaration/EnumDeclarationParser.php new file mode 100644 index 00000000..2630c0a6 --- /dev/null +++ b/src/Language/Parser/EnumDeclaration/EnumDeclarationParser.php @@ -0,0 +1,256 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\EnumDeclaration; + +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PackageFactory\ComponentEngine\Domain\EnumName\EnumName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberValueNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\IntegerLiteral\IntegerLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class EnumDeclarationParser +{ + use Singleton; + + private ?StringLiteralParser $stringLiteralParser = null; + private ?IntegerLiteralParser $integerLiteralParser = null; + + /** + * @param \Iterator $tokens + * @return EnumDeclarationNode + */ + public function parse(\Iterator &$tokens): EnumDeclarationNode + { + $enumKeyWordToken = $this->extractEnumKeywordToken($tokens); + $enumNameNode = $this->parseEnumName($tokens); + + $this->skipOpeningBracketToken($tokens); + + $enumMemberDeclarations = $this->parseEnumMemberDeclarations($tokens); + $closingBracketToken = $this->extractClosingBracketToken($tokens); + + return new EnumDeclarationNode( + rangeInSource: Range::from( + $enumKeyWordToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + name: $enumNameNode, + members: $enumMemberDeclarations + ); + } + + /** + * @param \Iterator $tokens + * @return Token + */ + private function extractEnumKeywordToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::KEYWORD_ENUM); + + $enumKeyWordToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return $enumKeyWordToken; + } + + /** + * @param \Iterator $tokens + * @return EnumNameNode + */ + private function parseEnumName(\Iterator &$tokens): EnumNameNode + { + Scanner::assertType($tokens, TokenType::STRING); + + $enumKeyNameToken = $tokens->current(); + $enumNameNode = new EnumNameNode( + rangeInSource: $enumKeyNameToken->boundaries, + value: EnumName::from($enumKeyNameToken->value) + ); + + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return $enumNameNode; + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipOpeningBracketToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); + Scanner::skipOne($tokens); + } + + /** + * @param \Iterator $tokens + * @return EnumMemberDeclarationNodes + */ + private function parseEnumMemberDeclarations(\Iterator &$tokens): EnumMemberDeclarationNodes + { + $items = []; + while (true) { + Scanner::skipSpaceAndComments($tokens); + + switch (Scanner::type($tokens)) { + case TokenType::STRING: + $items[] = $this->parseEnumMemberDeclaration($tokens); + break; + case TokenType::BRACKET_CURLY_CLOSE: + break 2; + default: + Scanner::assertType($tokens, TokenType::STRING, TokenType::BRACKET_CURLY_CLOSE); + } + } + + return new EnumMemberDeclarationNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return Token + */ + private function extractClosingBracketToken(\Iterator &$tokens): Token + { + Scanner::skipSpace($tokens); + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); + + $closingBracketToken = $tokens->current(); + + Scanner::skipOne($tokens); + + return $closingBracketToken; + } + + /** + * @param \Iterator $tokens + * @return EnumMemberDeclarationNode + */ + private function parseEnumMemberDeclaration(\Iterator &$tokens): EnumMemberDeclarationNode + { + $enumMemberName = $this->parseEnumMemberName($tokens); + $value = $this->parseEnumMemberValue($tokens); + + return new EnumMemberDeclarationNode( + rangeInSource: Range::from( + $enumMemberName->rangeInSource->start, + $value?->rangeInSource->end + ?? $enumMemberName->rangeInSource->end + ), + name: $enumMemberName, + value: $value + ); + } + + /** + * @param \Iterator $tokens + * @return EnumMemberNameNode + */ + private function parseEnumMemberName(\Iterator &$tokens): EnumMemberNameNode + { + Scanner::assertType($tokens, TokenType::STRING); + + $enumMemberNameToken = $tokens->current(); + $enumMemberNameNode = new EnumMemberNameNode( + rangeInSource: $enumMemberNameToken->boundaries, + value: EnumMemberName::from($enumMemberNameToken->value) + ); + + Scanner::skipOne($tokens); + + return $enumMemberNameNode; + } + + /** + * @param \Iterator $tokens + * @return null|EnumMemberValueNode + */ + private function parseEnumMemberValue(\Iterator &$tokens): ?EnumMemberValueNode + { + if (Scanner::type($tokens) !== TokenType::BRACKET_ROUND_OPEN) { + return null; + } + + $openingBracketToken = $tokens->current(); + Scanner::skipOne($tokens); + + $valueToken = $tokens->current(); + $value = match ($valueToken->type) { + TokenType::STRING_QUOTED => + $this->parseStringLiteral($tokens), + TokenType::NUMBER_BINARY, + TokenType::NUMBER_OCTAL, + TokenType::NUMBER_DECIMAL, + TokenType::NUMBER_HEXADECIMAL => + $this->parseIntegerLiteral($tokens), + default => throw new \Exception('@TODO: Unexpected Token ' . Scanner::type($tokens)->value) + }; + + Scanner::assertType($tokens, TokenType::BRACKET_ROUND_CLOSE); + $closingBracketToken = $tokens->current(); + Scanner::skipOne($tokens); + + return new EnumMemberValueNode( + rangeInSource: Range::from( + $openingBracketToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + value: $value + ); + } + + /** + * @param \Iterator $tokens + * @return StringLiteralNode + */ + private function parseStringLiteral(\Iterator &$tokens): StringLiteralNode + { + $this->stringLiteralParser ??= StringLiteralParser::singleton(); + return $this->stringLiteralParser->parse($tokens); + } + + /** + * @param \Iterator $tokens + * @return IntegerLiteralNode + */ + private function parseIntegerLiteral(\Iterator &$tokens): IntegerLiteralNode + { + $this->integerLiteralParser ??= IntegerLiteralParser::singleton(); + return $this->integerLiteralParser->parse($tokens); + } +} diff --git a/src/Parser/Ast/AccessNode.php b/src/Language/Parser/Export/ExportCouldNotBeParsed.php similarity index 51% rename from src/Parser/Ast/AccessNode.php rename to src/Language/Parser/Export/ExportCouldNotBeParsed.php index dc75bcfd..53da8b64 100644 --- a/src/Parser/Ast/AccessNode.php +++ b/src/Language/Parser/Export/ExportCouldNotBeParsed.php @@ -2,7 +2,7 @@ /** * PackageFactory.ComponentEngine - Universal View Components for PHP - * Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine + * Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,40 +20,27 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Parser\Ast; +namespace PackageFactory\ComponentEngine\Language\Parser\Export; +use PackageFactory\ComponentEngine\Language\Parser\ParserException; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; -final class AccessNode implements \JsonSerializable +final class ExportCouldNotBeParsed extends ParserException { - private function __construct( - public readonly ExpressionNode $root, - public readonly AccessChainSegmentNodes $chain - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(ExpressionNode $root, \Iterator $tokens): self - { - $chain = AccessChainSegmentNodes::fromTokens($tokens); - + public static function becauseOfUnexpectedToken( + TokenTypes $expectedTokenTypes, + Token $actualToken + ): self { return new self( - root: $root, - chain: $chain + code: 1691184282, + message: sprintf( + 'Export could not be parsed because of unexpected token %s. ' + . 'Expected %s instead.', + $actualToken->toDebugString(), + $expectedTokenTypes->toDebugString() + ), + affectedRangeInSource: $actualToken->boundaries ); } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'AccessNode', - 'payload' => [ - 'root' => $this->root, - 'chain' => $this->chain - ] - ]; - } } diff --git a/src/Language/Parser/Export/ExportParser.php b/src/Language/Parser/Export/ExportParser.php new file mode 100644 index 00000000..c0fa9ed0 --- /dev/null +++ b/src/Language/Parser/Export/ExportParser.php @@ -0,0 +1,121 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Export; + +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Export\ExportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\Parser\ComponentDeclaration\ComponentDeclarationParser; +use PackageFactory\ComponentEngine\Language\Parser\EnumDeclaration\EnumDeclarationParser; +use PackageFactory\ComponentEngine\Language\Parser\StructDeclaration\StructDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; + +final class ExportParser +{ + use Singleton; + + private ?ComponentDeclarationParser $componentDeclarationParser = null; + private ?EnumDeclarationParser $enumDeclarationParser = null; + private ?StructDeclarationParser $structDeclarationParser = null; + + /** + * @param \Iterator $tokens + * @return ExportNode + */ + public function parse(\Iterator &$tokens): ExportNode + { + $exportKeywordToken = $this->extractToken($tokens, TokenType::KEYWORD_EXPORT); + $declaration = match (Scanner::type($tokens)) { + TokenType::KEYWORD_COMPONENT => $this->parseComponentDeclaration($tokens), + TokenType::KEYWORD_ENUM => $this->parseEnumDeclaration($tokens), + TokenType::KEYWORD_STRUCT => $this->parseStructDeclaration($tokens), + default => throw ExportCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::KEYWORD_COMPONENT, + TokenType::KEYWORD_ENUM, + TokenType::KEYWORD_STRUCT + ), + actualToken: $tokens->current() + ) + }; + + return new ExportNode( + rangeInSource: Range::from( + $exportKeywordToken->boundaries->start, + $declaration->rangeInSource->end + ), + declaration: $declaration + ); + } + + /** + * @param \Iterator $tokens + * @param TokenType $tokenType + * @return Token + */ + private function extractToken(\Iterator &$tokens, TokenType $tokenType): Token + { + Scanner::assertType($tokens, $tokenType); + $token = $tokens->current(); + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return $token; + } + + /** + * @param \Iterator $tokens + * @return ComponentDeclarationNode + */ + private function parseComponentDeclaration(\Iterator &$tokens): ComponentDeclarationNode + { + $this->componentDeclarationParser ??= ComponentDeclarationParser::singleton(); + return $this->componentDeclarationParser->parse($tokens); + } + + /** + * @param \Iterator $tokens + * @return EnumDeclarationNode + */ + private function parseEnumDeclaration(\Iterator &$tokens): EnumDeclarationNode + { + $this->enumDeclarationParser ??= EnumDeclarationParser::singleton(); + return $this->enumDeclarationParser->parse($tokens); + } + + /** + * @param \Iterator $tokens + * @return StructDeclarationNode + */ + private function parseStructDeclaration(\Iterator &$tokens): StructDeclarationNode + { + $this->structDeclarationParser ??= StructDeclarationParser::singleton(); + return $this->structDeclarationParser->parse($tokens); + } +} diff --git a/src/Language/Parser/Expression/ExpressionCouldNotBeParsed.php b/src/Language/Parser/Expression/ExpressionCouldNotBeParsed.php new file mode 100644 index 00000000..d3d4533c --- /dev/null +++ b/src/Language/Parser/Expression/ExpressionCouldNotBeParsed.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Expression; + +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; + +final class ExpressionCouldNotBeParsed extends ParserException +{ + public static function becauseOfUnexpectedToken( + TokenTypes $expectedTokenTypes, + Token $actualToken + ): self { + return new self( + code: 1691063089, + message: sprintf( + 'Expression could not be parsed because of unexpected token %s. ' + . 'Expected %s instead.', + $actualToken->toDebugString(), + $expectedTokenTypes->toDebugString() + ), + affectedRangeInSource: $actualToken->boundaries + ); + } +} diff --git a/src/Language/Parser/Expression/ExpressionParser.php b/src/Language/Parser/Expression/ExpressionParser.php new file mode 100644 index 00000000..4bad7378 --- /dev/null +++ b/src/Language/Parser/Expression/ExpressionParser.php @@ -0,0 +1,576 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Expression; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessKeyNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessType; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperator; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperator; +use PackageFactory\ComponentEngine\Language\Parser\BooleanLiteral\BooleanLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\IntegerLiteral\IntegerLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\Match\MatchParser; +use PackageFactory\ComponentEngine\Language\Parser\NullLiteral\NullLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\Tag\TagParser; +use PackageFactory\ComponentEngine\Language\Parser\TemplateLiteral\TemplateLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\ValueReference\ValueReferenceParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; +use PhpParser\Parser\Tokens; + +final class ExpressionParser +{ + private ?BooleanLiteralParser $booleanLiteralParser = null; + private ?IntegerLiteralParser $integerLiteralParser = null; + private ?MatchParser $matchParser = null; + private ?NullLiteralParser $nullLiteralParser = null; + private ?StringLiteralParser $stringLiteralParser = null; + private ?TagParser $tagParser = null; + private ?TemplateLiteralParser $templateLiteralParser = null; + private ?ValueReferenceParser $valueReferenceParser = null; + + public function __construct( + private ?TokenType $stopAt = null, + private Precedence $precedence = Precedence::SEQUENCE + ) { + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + public function parse(\Iterator &$tokens): ExpressionNode + { + Scanner::skipSpaceAndComments($tokens); + + $result = $this->parseUnaryStatement($tokens); + + if ($this->shouldStop($tokens)) { + return $result; + } + + $binaryOperationTokens = TokenTypes::from( + TokenType::OPERATOR_BOOLEAN_AND, + TokenType::OPERATOR_BOOLEAN_OR, + TokenType::COMPARATOR_EQUAL, + TokenType::COMPARATOR_NOT_EQUAL, + TokenType::COMPARATOR_GREATER_THAN, + TokenType::COMPARATOR_GREATER_THAN_OR_EQUAL, + TokenType::COMPARATOR_LESS_THAN, + TokenType::COMPARATOR_LESS_THAN_OR_EQUAL + ); + + while ( + !$this->shouldStop($tokens) && + $binaryOperationTokens->contains(Scanner::type($tokens)) + ) { + $result = $this->parseBinaryOperation($tokens, $result); + } + + if ($this->shouldStop($tokens)) { + return $result; + } + + $result = match (Scanner::type($tokens)) { + TokenType::QUESTIONMARK => + $this->parseTernaryOperation($tokens, $result), + default => + throw ExpressionCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from(TokenType::QUESTIONMARK), + actualToken: $tokens->current() + ) + }; + + return $result; + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseUnaryStatement(\Iterator &$tokens): ExpressionNode + { + $result = match (Scanner::type($tokens)) { + TokenType::OPERATOR_BOOLEAN_NOT => + $this->parseUnaryOperation($tokens), + TokenType::KEYWORD_TRUE, + TokenType::KEYWORD_FALSE => + $this->parseBooleanLiteral($tokens), + TokenType::KEYWORD_NULL => + $this->parseNullLiteral($tokens), + TokenType::STRING_QUOTED => + $this->parseStringLiteral($tokens), + TokenType::NUMBER_BINARY, + TokenType::NUMBER_OCTAL, + TokenType::NUMBER_DECIMAL, + TokenType::NUMBER_HEXADECIMAL => + $this->parseIntegerLiteral($tokens), + TokenType::STRING => + $this->parseValueReference($tokens), + TokenType::TAG_START_OPENING => + $this->parseTag($tokens), + TokenType::TEMPLATE_LITERAL_START => + $this->parseTemplateLiteral($tokens), + TokenType::KEYWORD_MATCH => + $this->parseMatch($tokens), + TokenType::BRACKET_ROUND_OPEN => + $this->parseBracketedExpression($tokens), + default => + throw ExpressionCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::KEYWORD_TRUE, + TokenType::KEYWORD_FALSE, + TokenType::KEYWORD_NULL, + TokenType::STRING_QUOTED, + TokenType::NUMBER_BINARY, + TokenType::NUMBER_OCTAL, + TokenType::NUMBER_DECIMAL, + TokenType::NUMBER_HEXADECIMAL, + TokenType::STRING, + TokenType::TAG_START_OPENING, + TokenType::TEMPLATE_LITERAL_START, + TokenType::KEYWORD_MATCH, + TokenType::BRACKET_ROUND_OPEN + ), + actualToken: $tokens->current() + ) + }; + + if (!Scanner::isEnd($tokens)) { + $result = match (Scanner::type($tokens)) { + TokenType::PERIOD, + TokenType::OPTCHAIN => $this->parseAcccess($tokens, $result), + default => $result + }; + } + + return $result; + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseUnaryOperation(\Iterator &$tokens): ExpressionNode + { + $startingToken = $tokens->current(); + + $operator = $this->parseUnaryOperator($tokens); + $operand = $this->parseUnaryStatement($tokens); + + $unaryOperationNode = new UnaryOperationNode( + rangeInSource: Range::from( + $startingToken->boundaries->start, + $operand->rangeInSource->end + ), + operator: $operator, + operand: $operand + ); + + return new ExpressionNode( + rangeInSource: $unaryOperationNode->rangeInSource, + root: $unaryOperationNode + ); + } + + /** + * @param \Iterator $tokens + * @return UnaryOperator + */ + private function parseUnaryOperator(\Iterator &$tokens): UnaryOperator + { + $unaryOperator = match (Scanner::type($tokens)) { + TokenType::OPERATOR_BOOLEAN_NOT => UnaryOperator::NOT, + default => throw ExpressionCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from(TokenType::OPERATOR_BOOLEAN_NOT), + actualToken: $tokens->current() + ) + }; + + Scanner::skipOne($tokens); + + return $unaryOperator; + } + + private function withStopAt(TokenType $stopAt): self + { + $newExpressionParser = clone $this; + $newExpressionParser->stopAt = $stopAt; + + return $newExpressionParser; + } + + private function withPrecedence(Precedence $precedence): self + { + $newExpressionParser = clone $this; + $newExpressionParser->precedence = $precedence; + + return $newExpressionParser; + } + + /** + * @param \Iterator $tokens + * @return boolean + */ + private function shouldStop(\Iterator &$tokens): bool + { + Scanner::skipSpaceAndComments($tokens); + + if (Scanner::isEnd($tokens)) { + return true; + } + + $type = Scanner::type($tokens); + + if ($this->precedence->mustStopAt($type)) { + return true; + } + + if ($this->stopAt && $type === $this->stopAt) { + return true; + } + + return false; + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseBooleanLiteral(\Iterator &$tokens): ExpressionNode + { + $this->booleanLiteralParser ??= BooleanLiteralParser::singleton(); + + $booleanLiteralNode = $this->booleanLiteralParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $booleanLiteralNode->rangeInSource, + root: $booleanLiteralNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseNullLiteral(\Iterator &$tokens): ExpressionNode + { + + $this->nullLiteralParser ??= NullLiteralParser::singleton(); + + $nullLiteralNode = $this->nullLiteralParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $nullLiteralNode->rangeInSource, + root: $nullLiteralNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseStringLiteral(\Iterator &$tokens): ExpressionNode + { + $this->stringLiteralParser ??= StringLiteralParser::singleton(); + + $stringLiteralNode = $this->stringLiteralParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $stringLiteralNode->rangeInSource, + root: $stringLiteralNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseIntegerLiteral(\Iterator &$tokens): ExpressionNode + { + $this->integerLiteralParser ??= IntegerLiteralParser::singleton(); + + $integerLiteralNode = $this->integerLiteralParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $integerLiteralNode->rangeInSource, + root: $integerLiteralNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseValueReference(\Iterator &$tokens): ExpressionNode + { + $this->valueReferenceParser ??= ValueReferenceParser::singleton(); + + $valueReferenceNode = $this->valueReferenceParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $valueReferenceNode->rangeInSource, + root: $valueReferenceNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseTag(\Iterator &$tokens): ExpressionNode + { + $this->tagParser ??= TagParser::singleton(); + + $tagNode = $this->tagParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $tagNode->rangeInSource, + root: $tagNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseTemplateLiteral(\Iterator &$tokens): ExpressionNode + { + $this->templateLiteralParser ??= TemplateLiteralParser::singleton(); + + $templateLiteralNode = $this->templateLiteralParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $templateLiteralNode->rangeInSource, + root: $templateLiteralNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseMatch(\Iterator &$tokens): ExpressionNode + { + $this->matchParser ??= MatchParser::singleton(); + + $matchNode = $this->matchParser->parse($tokens); + + return new ExpressionNode( + rangeInSource: $matchNode->rangeInSource, + root: $matchNode + ); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseBracketedExpression(\Iterator &$tokens): ExpressionNode + { + Scanner::assertType($tokens, TokenType::BRACKET_ROUND_OPEN); + + $openingBracketToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + + $innerExpressionNode = $this->withStopAt(TokenType::BRACKET_ROUND_CLOSE)->parse($tokens); + + Scanner::assertType($tokens, TokenType::BRACKET_ROUND_CLOSE); + + $closingBracketToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + + return new ExpressionNode( + rangeInSource: Range::from( + $openingBracketToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + root: $innerExpressionNode->root + ); + } + + /** + * @param \Iterator $tokens + * @param ExpressionNode $parent + * @return ExpressionNode + */ + private function parseAcccess(\Iterator &$tokens, ExpressionNode $parent): ExpressionNode + { + $accessTokenTypes = TokenTypes::from(TokenType::PERIOD, TokenType::OPTCHAIN); + + while (!Scanner::isEnd($tokens) && $accessTokenTypes->contains(Scanner::type($tokens))) { + $type = $this->parseAccessType($tokens); + + Scanner::assertType($tokens, TokenType::STRING); + $keyToken = $tokens->current(); + Scanner::skipOne($tokens); + + $rangeInSource = Range::from( + $parent->rangeInSource->start, + $keyToken->boundaries->end + ); + + $parent = new ExpressionNode( + rangeInSource: $rangeInSource, + root: new AccessNode( + rangeInSource: $rangeInSource, + parent: $parent, + type: $type, + key: new AccessKeyNode( + rangeInSource: $keyToken->boundaries, + value: PropertyName::from($keyToken->value) + ) + ) + ); + } + + return $parent; + } + + /** + * @param \Iterator $tokens + * @return AccessType + */ + private function parseAccessType(\Iterator &$tokens): AccessType + { + $accessType = match (Scanner::type($tokens)) { + TokenType::PERIOD => AccessType::MANDATORY, + TokenType::OPTCHAIN => AccessType::OPTIONAL, + default => throw ExpressionCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from(TokenType::PERIOD, TokenType::OPTCHAIN), + actualToken: $tokens->current() + ) + }; + + Scanner::skipOne($tokens); + + return $accessType; + } + + /** + * @param \Iterator $tokens + * @param ExpressionNode $leftOperand + * @return ExpressionNode + */ + private function parseBinaryOperation(\Iterator &$tokens, ExpressionNode $leftOperand): ExpressionNode + { + $operator = $this->parseBinaryOperator($tokens); + $rightOperand = $this + ->withPrecedence(Precedence::forBinaryOperator($operator)) + ->parse($tokens); + $rangeInSource = Range::from( + $leftOperand->rangeInSource->start, + $rightOperand->rangeInSource->end + ); + + return new ExpressionNode( + rangeInSource: $rangeInSource, + root: new BinaryOperationNode( + rangeInSource: $rangeInSource, + leftOperand: $leftOperand, + operator: $operator, + rightOperand: $rightOperand + ) + ); + } + + /** + * @param \Iterator $tokens + * @return BinaryOperator + */ + private function parseBinaryOperator(\Iterator &$tokens): BinaryOperator + { + $operator = match (Scanner::type($tokens)) { + TokenType::OPERATOR_BOOLEAN_AND => BinaryOperator::AND, + TokenType::OPERATOR_BOOLEAN_OR => BinaryOperator::OR, + TokenType::COMPARATOR_EQUAL => BinaryOperator::EQUAL, + TokenType::COMPARATOR_NOT_EQUAL => BinaryOperator::NOT_EQUAL, + TokenType::COMPARATOR_GREATER_THAN => BinaryOperator::GREATER_THAN, + TokenType::COMPARATOR_GREATER_THAN_OR_EQUAL => BinaryOperator::GREATER_THAN_OR_EQUAL, + TokenType::COMPARATOR_LESS_THAN => BinaryOperator::LESS_THAN, + TokenType::COMPARATOR_LESS_THAN_OR_EQUAL => BinaryOperator::LESS_THAN_OR_EQUAL, + default => throw ExpressionCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::OPERATOR_BOOLEAN_AND, + TokenType::OPERATOR_BOOLEAN_OR, + TokenType::COMPARATOR_EQUAL, + TokenType::COMPARATOR_NOT_EQUAL, + TokenType::COMPARATOR_GREATER_THAN, + TokenType::COMPARATOR_GREATER_THAN_OR_EQUAL, + TokenType::COMPARATOR_LESS_THAN, + TokenType::COMPARATOR_LESS_THAN_OR_EQUAL + ), + actualToken: $tokens->current() + ) + }; + + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + + return $operator; + } + + /** + * @param \Iterator $tokens + * @param ExpressionNode $condition + * @return ExpressionNode + */ + private function parseTernaryOperation(\Iterator &$tokens, ExpressionNode $condition): ExpressionNode + { + Scanner::assertType($tokens, TokenType::QUESTIONMARK); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + + $trueBranch = $this->withStopAt(TokenType::COLON)->parse($tokens); + + Scanner::assertType($tokens, TokenType::COLON); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + + $falseBranch = $this->parse($tokens); + + $root = new TernaryOperationNode( + condition: $condition, + trueBranch: $trueBranch, + falseBranch: $falseBranch + ); + + return new ExpressionNode( + rangeInSource: $root->rangeInSource, + root: $root + ); + } +} diff --git a/src/Definition/Precedence.php b/src/Language/Parser/Expression/Precedence.php similarity index 77% rename from src/Definition/Precedence.php rename to src/Language/Parser/Expression/Precedence.php index 3f48773b..a96d1553 100644 --- a/src/Definition/Precedence.php +++ b/src/Language/Parser/Expression/Precedence.php @@ -20,12 +20,11 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Definition; +namespace PackageFactory\ComponentEngine\Language\Parser\Expression; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperator; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - - enum Precedence: int { // @@ -72,6 +71,22 @@ public static function forTokenType(TokenType $tokenType): self }; } + public static function forBinaryOperator(BinaryOperator $binaryOperator): self + { + return match ($binaryOperator) { + BinaryOperator::AND => self::LOGICAL_AND, + BinaryOperator::OR => self::LOGICAL_OR, + + BinaryOperator::EQUAL, + BinaryOperator::NOT_EQUAL => self::EQUALITY, + + BinaryOperator::GREATER_THAN, + BinaryOperator::GREATER_THAN_OR_EQUAL, + BinaryOperator::LESS_THAN, + BinaryOperator::LESS_THAN_OR_EQUAL => self::COMPARISON + }; + } + public function mustStopAt(TokenType $tokenType): bool { return self::forTokenType($tokenType)->value <= $this->value; diff --git a/src/Language/Parser/Import/ImportCouldNotBeParsed.php b/src/Language/Parser/Import/ImportCouldNotBeParsed.php new file mode 100644 index 00000000..38dd1036 --- /dev/null +++ b/src/Language/Parser/Import/ImportCouldNotBeParsed.php @@ -0,0 +1,44 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Import; + +use PackageFactory\ComponentEngine\Language\AST\Node\Import\InvalidImportedNameNodes; +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class ImportCouldNotBeParsed extends ParserException +{ + public static function becauseOfInvalidImportedNameNodes( + InvalidImportedNameNodes $cause, + Range $affectedRangeInSource + ): self { + return new self( + code: 1691181627, + message: sprintf( + 'Import could not be parsed, because of invalid imported names: %s', + $cause->getMessage() + ), + affectedRangeInSource: $cause->affectedRangeInSource ?? $affectedRangeInSource + ); + } +} diff --git a/src/Language/Parser/Import/ImportParser.php b/src/Language/Parser/Import/ImportParser.php new file mode 100644 index 00000000..b10a2cc4 --- /dev/null +++ b/src/Language/Parser/Import/ImportParser.php @@ -0,0 +1,148 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Import; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\InvalidImportedNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class ImportParser +{ + use Singleton; + + private ?StringLiteralParser $pathParser = null; + + /** + * @param \Iterator $tokens + * @return ImportNode + */ + public function parse(\Iterator &$tokens): ImportNode + { + $fromKeywordToken = $this->extractToken($tokens, TokenType::KEYWORD_FROM); + $path = $this->parsePath($tokens); + + $this->skipToken($tokens, TokenType::KEYWORD_IMPORT); + $openingBracketToken = $this->extractToken($tokens, TokenType::BRACKET_CURLY_OPEN); + + try { + $names = $this->parseNames($tokens); + $closingBracketToken = $this->extractToken($tokens, TokenType::BRACKET_CURLY_CLOSE); + + return new ImportNode( + rangeInSource: Range::from( + $fromKeywordToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + path: $path, + names: $names + ); + } catch (InvalidImportedNameNodes $e) { + throw ImportCouldNotBeParsed::becauseOfInvalidImportedNameNodes( + cause: $e, + affectedRangeInSource: $openingBracketToken->boundaries + ); + } + } + + /** + * @param \Iterator $tokens + * @param TokenType $tokenType + * @return Token + */ + private function extractToken(\Iterator &$tokens, TokenType $tokenType): Token + { + Scanner::assertType($tokens, $tokenType); + $token = $tokens->current(); + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return $token; + } + + /** + * @param \Iterator $tokens + * @param TokenType $tokenType + * @return void + */ + private function skipToken(\Iterator &$tokens, TokenType $tokenType): void + { + Scanner::assertType($tokens, $tokenType); + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + } + + /** + * @param \Iterator $tokens + * @return StringLiteralNode + */ + private function parsePath(\Iterator &$tokens): StringLiteralNode + { + $this->pathParser ??= StringLiteralParser::singleton(); + + $path = $this->pathParser->parse($tokens); + Scanner::skipSpace($tokens); + + return $path; + } + + /** + * @param \Iterator $tokens + * @return ImportedNameNodes + */ + private function parseNames(\Iterator &$tokens): ImportedNameNodes + { + $items = []; + while (Scanner::type($tokens) !== TokenType::BRACKET_CURLY_CLOSE) { + $items[] = $this->parseName($tokens); + + if (Scanner::type($tokens) !== TokenType::BRACKET_CURLY_CLOSE) { + $this->skipToken($tokens, TokenType::COMMA); + } + } + + return new ImportedNameNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return ImportedNameNode + */ + private function parseName(\Iterator &$tokens): ImportedNameNode + { + $nameToken = $this->extractToken($tokens, TokenType::STRING); + + return new ImportedNameNode( + rangeInSource: $nameToken->boundaries, + value: VariableName::from($nameToken->value) + ); + } +} diff --git a/src/Language/Parser/IntegerLiteral/IntegerLiteralCouldNotBeParsed.php b/src/Language/Parser/IntegerLiteral/IntegerLiteralCouldNotBeParsed.php new file mode 100644 index 00000000..b1d5dd37 --- /dev/null +++ b/src/Language/Parser/IntegerLiteral/IntegerLiteralCouldNotBeParsed.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\IntegerLiteral; + +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; + +final class IntegerLiteralCouldNotBeParsed extends ParserException +{ + public static function becauseOfUnexpectedEndOfFile(): self + { + return new self( + code: 1691238474, + message: 'Integer literal could not be parsed because of unexpected end of file.' + ); + } + + public static function becauseOfUnexpectedToken( + TokenTypes $expectedTokenTypes, + Token $actualToken + ): self { + return new self( + code: 1691238491, + message: sprintf( + 'Integer literal could not be parsed because of unexpected token %s. ' + . 'Expected %s instead.', + $actualToken->toDebugString(), + $expectedTokenTypes->toDebugString() + ), + affectedRangeInSource: $actualToken->boundaries + ); + } +} diff --git a/src/Language/Parser/IntegerLiteral/IntegerLiteralParser.php b/src/Language/Parser/IntegerLiteral/IntegerLiteralParser.php new file mode 100644 index 00000000..4f34d1be --- /dev/null +++ b/src/Language/Parser/IntegerLiteral/IntegerLiteralParser.php @@ -0,0 +1,77 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\IntegerLiteral; + +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerFormat; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; + +final class IntegerLiteralParser +{ + use Singleton; + + /** + * @param \Iterator $tokens + * @return IntegerLiteralNode + */ + public function parse(\Iterator &$tokens): IntegerLiteralNode + { + if (Scanner::isEnd($tokens)) { + throw IntegerLiteralCouldNotBeParsed::becauseOfUnexpectedEndOfFile(); + } + + $token = $tokens->current(); + + Scanner::skipOne($tokens); + + return new IntegerLiteralNode( + rangeInSource: $token->boundaries, + format: $this->getIntegerFormatFromToken($token), + value: $token->value + ); + } + + private function getIntegerFormatFromToken(Token $token): IntegerFormat + { + return match ($token->type) { + TokenType::NUMBER_BINARY => IntegerFormat::BINARY, + TokenType::NUMBER_OCTAL => IntegerFormat::OCTAL, + TokenType::NUMBER_DECIMAL => IntegerFormat::DECIMAL, + TokenType::NUMBER_HEXADECIMAL => IntegerFormat::HEXADECIMAL, + + default => throw IntegerLiteralCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::NUMBER_BINARY, + TokenType::NUMBER_OCTAL, + TokenType::NUMBER_DECIMAL, + TokenType::NUMBER_HEXADECIMAL + ), + actualToken: $token + ) + }; + } +} diff --git a/src/Language/Parser/Match/MatchCouldNotBeParsed.php b/src/Language/Parser/Match/MatchCouldNotBeParsed.php new file mode 100644 index 00000000..9b6efc68 --- /dev/null +++ b/src/Language/Parser/Match/MatchCouldNotBeParsed.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Match; + +use PackageFactory\ComponentEngine\Language\AST\Node\Match\InvalidMatchArmNodes; +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Parser\Source\Range; + +final class MatchCouldNotBeParsed extends ParserException +{ + public static function becauseOfInvalidMatchArmNodes( + InvalidMatchArmNodes $cause, + ?Range $affectedRangeInSource = null + ): self { + return new self( + code: 1691152175, + message: sprintf( + 'Match could not be parsed because of invalid match arm nodes: %s.', + $cause->getMessage() + ), + affectedRangeInSource: $cause->affectedRangeInSource ?? $affectedRangeInSource, + cause: $cause + ); + } +} diff --git a/src/Language/Parser/Match/MatchParser.php b/src/Language/Parser/Match/MatchParser.php new file mode 100644 index 00000000..dab66788 --- /dev/null +++ b/src/Language/Parser/Match/MatchParser.php @@ -0,0 +1,238 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Match; + +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\InvalidMatchArmNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; +use PackageFactory\ComponentEngine\Language\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class MatchParser +{ + use Singleton; + + private ?ExpressionParser $subjectParser = null; + private ?ExpressionParser $matchArmLeftParser = null; + private ?ExpressionParser $matchArmRightParser = null; + + /** + * @param \Iterator $tokens + * @return MatchNode + */ + public function parse(\Iterator &$tokens): MatchNode + { + $matchKeywordToken = $this->extractMatchKeywordToken($tokens); + $subject = $this->parseSubject($tokens); + + $this->skipOpeningBracketToken($tokens); + + try { + $arms = $this->parseArms($tokens); + + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); + $closingBracketToken = $tokens->current(); + Scanner::skipOne($tokens); + + return new MatchNode( + rangeInSource: Range::from( + $matchKeywordToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + subject: $subject, + arms: $arms + ); + } catch (InvalidMatchArmNodes $e) { + throw MatchCouldNotBeParsed::becauseOfInvalidMatchArmNodes( + cause: $e, + affectedRangeInSource: $matchKeywordToken->boundaries + ); + } + } + + /** + * @param \Iterator $tokens + * @return Token + */ + private function extractMatchKeywordToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::KEYWORD_MATCH); + + $matchKeywordToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return $matchKeywordToken; + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseSubject(\Iterator &$tokens): ExpressionNode + { + $this->subjectParser ??= new ExpressionParser( + stopAt: TokenType::BRACKET_CURLY_OPEN + ); + + return $this->subjectParser->parse($tokens); + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipOpeningBracketToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + } + + /** + * @param \Iterator $tokens + * @return MatchArmNodes + */ + private function parseArms(\Iterator &$tokens): MatchArmNodes + { + $items = []; + while (Scanner::type($tokens) !== TokenType::BRACKET_CURLY_CLOSE) { + $items[] = $this->parseArm($tokens); + } + + return new MatchArmNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return MatchArmNode + */ + private function parseArm(\Iterator &$tokens): MatchArmNode + { + $defaultKeywordToken = $this->extractDefaultKeywordToken($tokens); + $left = is_null($defaultKeywordToken) ? $this->parseArmLeft($tokens) : null; + + $this->skipArrowSingleToken($tokens); + + $right = $this->parseArmRight($tokens); + + if (is_null($defaultKeywordToken)) { + assert($left !== null); + $start = $left->items[0]->rangeInSource->start; + } else { + $start = $defaultKeywordToken->boundaries->start; + } + + return new MatchArmNode( + rangeInSource: Range::from( + $start, + $right->rangeInSource->end + ), + left: $left, + right: $right + ); + } + + /** + * @param \Iterator $tokens + * @return null|Token + */ + private function extractDefaultKeywordToken(\Iterator &$tokens): ?Token + { + if (Scanner::type($tokens) === TokenType::KEYWORD_DEFAULT) { + $defaultKeywordToken = $tokens->current(); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + + return $defaultKeywordToken; + } + + return null; + } + + /** + * @param \Iterator $tokens + * @return ExpressionNodes + */ + private function parseArmLeft(\Iterator &$tokens): ExpressionNodes + { + $this->matchArmLeftParser ??= new ExpressionParser( + stopAt: TokenType::ARROW_SINGLE + ); + + $items = []; + while (Scanner::type($tokens) !== TokenType::ARROW_SINGLE) { + assert($this->matchArmLeftParser !== null); + $items[] = $this->matchArmLeftParser->parse($tokens); + + if (Scanner::type($tokens) !== TokenType::ARROW_SINGLE) { + $this->skipCommaToken($tokens); + } + } + + return new ExpressionNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipCommaToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::COMMA); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipArrowSingleToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::ARROW_SINGLE); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseArmRight(\Iterator &$tokens): ExpressionNode + { + $this->matchArmRightParser ??= new ExpressionParser( + stopAt: TokenType::BRACKET_CURLY_CLOSE + ); + + return $this->matchArmRightParser->parse($tokens); + } +} diff --git a/src/Language/Parser/Module/ModuleCouldNotBeParsed.php b/src/Language/Parser/Module/ModuleCouldNotBeParsed.php new file mode 100644 index 00000000..fac71860 --- /dev/null +++ b/src/Language/Parser/Module/ModuleCouldNotBeParsed.php @@ -0,0 +1,42 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Module; + +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; + +final class ModuleCouldNotBeParsed extends ParserException +{ + public static function becauseOfUnexpectedExceedingToken( + Token $exceedingToken + ): self { + return new self( + code: 1691235933, + message: sprintf( + 'Module could not be parsed because of unexpected exceeding token %s.', + $exceedingToken->toDebugString() + ), + affectedRangeInSource: $exceedingToken->boundaries + ); + } +} diff --git a/src/Language/Parser/Module/ModuleParser.php b/src/Language/Parser/Module/ModuleParser.php new file mode 100644 index 00000000..d45f64cf --- /dev/null +++ b/src/Language/Parser/Module/ModuleParser.php @@ -0,0 +1,113 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Module; + +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\Export\ExportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; +use PackageFactory\ComponentEngine\Language\Parser\Export\ExportParser; +use PackageFactory\ComponentEngine\Language\Parser\Import\ImportParser; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class ModuleParser +{ + use Singleton; + + private ?ImportParser $importParser = null; + private ?ExportParser $exportParser = null; + + /** + * @param \Iterator $tokens + * @return ModuleNode + */ + public function parse(\Iterator &$tokens): ModuleNode + { + Scanner::skipSpaceAndComments($tokens); + + $imports = $this->parseImports($tokens); + $export = $this->parseExport($tokens); + + if (!Scanner::isEnd($tokens)) { + throw ModuleCouldNotBeParsed::becauseOfUnexpectedExceedingToken( + exceedingToken: $tokens->current() + ); + } + + return new ModuleNode( + rangeInSource: Range::from( + new Position(0, 0), + $export->rangeInSource->end + ), + imports: $imports, + export: $export + ); + } + + /** + * @param \Iterator $tokens + * @return ImportNodes + */ + private function parseImports(\Iterator &$tokens): ImportNodes + { + $items = []; + while (Scanner::type($tokens) !== TokenType::KEYWORD_EXPORT) { + $items[] = $this->parseImport($tokens); + } + + return new ImportNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return ImportNode + */ + private function parseImport(\Iterator &$tokens): ImportNode + { + $this->importParser ??= ImportParser::singleton(); + + $import = $this->importParser->parse($tokens); + Scanner::skipSpaceAndComments($tokens); + + return $import; + } + + /** + * @param \Iterator $tokens + * @return ExportNode + */ + private function parseExport(\Iterator &$tokens): ExportNode + { + $this->exportParser ??= ExportParser::singleton(); + + $export = $this->exportParser->parse($tokens); + Scanner::skipSpaceAndComments($tokens); + + return $export; + } +} diff --git a/src/Parser/Ast/NullLiteralNode.php b/src/Language/Parser/NullLiteral/NullLiteralParser.php similarity index 67% rename from src/Parser/Ast/NullLiteralNode.php rename to src/Language/Parser/NullLiteral/NullLiteralParser.php index 15c2bb10..69f17b2f 100644 --- a/src/Parser/Ast/NullLiteralNode.php +++ b/src/Language/Parser/NullLiteral/NullLiteralParser.php @@ -2,7 +2,7 @@ /** * PackageFactory.ComponentEngine - Universal View Components for PHP - * Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine + * Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,34 +20,32 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Parser\Ast; +namespace PackageFactory\ComponentEngine\Language\Parser\NullLiteral; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; -final class NullLiteralNode implements \JsonSerializable +final class NullLiteralParser { - private function __construct() - { - } + use Singleton; /** * @param \Iterator $tokens - * @return self + * @return NullLiteralNode */ - public static function fromTokens(\Iterator $tokens): self + public function parse(\Iterator &$tokens): NullLiteralNode { Scanner::assertType($tokens, TokenType::KEYWORD_NULL); - Scanner::skipOne($tokens); - return new self(); - } + $token = $tokens->current(); - public function jsonSerialize(): mixed - { - return [ - 'type' => 'NullLiteralNode' - ]; + Scanner::skipOne($tokens); + + return new NullLiteralNode( + rangeInSource: $token->boundaries + ); } } diff --git a/src/Language/Parser/ParserException.php b/src/Language/Parser/ParserException.php new file mode 100644 index 00000000..e58224f9 --- /dev/null +++ b/src/Language/Parser/ParserException.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser; + +use PackageFactory\ComponentEngine\Parser\Source\Range; + +abstract class ParserException extends \Exception +{ + final protected function __construct( + int $code, + string $message, + public readonly ?Range $affectedRangeInSource = null, + ?\Exception $cause = null + ) { + parent::__construct($message, $code, $cause); + } +} diff --git a/src/Language/Parser/PropertyDeclaration/PropertyDeclarationParser.php b/src/Language/Parser/PropertyDeclaration/PropertyDeclarationParser.php new file mode 100644 index 00000000..9a41d0ce --- /dev/null +++ b/src/Language/Parser/PropertyDeclaration/PropertyDeclarationParser.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\PropertyDeclaration; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyNameNode; +use PackageFactory\ComponentEngine\Language\Parser\TypeReference\TypeReferenceParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class PropertyDeclarationParser +{ + use Singleton; + + private ?TypeReferenceParser $typeReferenceParser = null; + + /** + * @param \Iterator $tokens + * @return PropertyDeclarationNode + */ + public function parse(\Iterator &$tokens): PropertyDeclarationNode + { + Scanner::assertType($tokens, TokenType::STRING); + $propertyNameToken = $tokens->current(); + + Scanner::skipOne($tokens); + + Scanner::assertType($tokens, TokenType::COLON); + Scanner::skipOne($tokens); + + Scanner::skipSpace($tokens); + + $this->typeReferenceParser ??= TypeReferenceParser::singleton(); + $typeReferenceNode = $this->typeReferenceParser->parse($tokens); + + return new PropertyDeclarationNode( + rangeInSource: Range::from( + $propertyNameToken->boundaries->start, + $typeReferenceNode->rangeInSource->end + ), + name: new PropertyNameNode( + rangeInSource: $propertyNameToken->boundaries, + value: PropertyName::from($propertyNameToken->value) + ), + type: $typeReferenceNode + ); + } +} diff --git a/src/Parser/Ast/MatchArmNodes.php b/src/Language/Parser/StringLiteral/StringLiteralParser.php similarity index 57% rename from src/Parser/Ast/MatchArmNodes.php rename to src/Language/Parser/StringLiteral/StringLiteralParser.php index 6c750048..fe6f1cb0 100644 --- a/src/Parser/Ast/MatchArmNodes.php +++ b/src/Language/Parser/StringLiteral/StringLiteralParser.php @@ -2,7 +2,7 @@ /** * PackageFactory.ComponentEngine - Universal View Components for PHP - * Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine + * Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,43 +20,33 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Parser\Ast; +namespace PackageFactory\ComponentEngine\Language\Parser\StringLiteral; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; -final class MatchArmNodes implements \JsonSerializable +final class StringLiteralParser { - /** - * @var MatchArmNode[] - */ - public readonly array $items; - - private function __construct( - MatchArmNode ...$items - ) { - $this->items = $items; - } + use Singleton; /** * @param \Iterator $tokens - * @return self + * @return StringLiteralNode */ - public static function fromTokens(\Iterator $tokens): self + public function parse(\Iterator &$tokens): StringLiteralNode { - $items = []; + Scanner::assertType($tokens, TokenType::STRING_QUOTED); - while (Scanner::type($tokens) !== TokenType::BRACKET_CURLY_CLOSE) { - Scanner::skipSpaceAndComments($tokens); - $items[] = MatchArmNode::fromTokens($tokens); - } + $token = $tokens->current(); - return new self(...$items); - } + Scanner::skipOne($tokens); - public function jsonSerialize(): mixed - { - return $this->items; + return new StringLiteralNode( + rangeInSource: $token->boundaries, + value: $token->value + ); } } diff --git a/src/Language/Parser/StructDeclaration/StructDeclarationParser.php b/src/Language/Parser/StructDeclaration/StructDeclarationParser.php new file mode 100644 index 00000000..37c1496e --- /dev/null +++ b/src/Language/Parser/StructDeclaration/StructDeclarationParser.php @@ -0,0 +1,142 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\StructDeclaration; + +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructNameNode; +use PackageFactory\ComponentEngine\Language\Parser\PropertyDeclaration\PropertyDeclarationParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class StructDeclarationParser +{ + use Singleton; + + private ?PropertyDeclarationParser $propertyDeclarationParser = null; + + /** + * @param \Iterator $tokens + * @return StructDeclarationNode + */ + public function parse(\Iterator &$tokens): StructDeclarationNode + { + $structKeywordToken = $this->extractStructKeywordToken($tokens); + $structNameNode = $this->parseStructName($tokens); + $this->skipOpeningBracketToken($tokens); + $propertyDeclarationNodes = $this->parsePropertyDeclarations($tokens); + $closingBracketToken = $this->extractClosingBracketToken($tokens); + + return new StructDeclarationNode( + rangeInSource: Range::from( + $structKeywordToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + name: $structNameNode, + properties: $propertyDeclarationNodes + ); + } + + /** + * @param \Iterator $tokens + * @return Token + */ + public function extractStructKeywordToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::KEYWORD_STRUCT); + + $structKeywordToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpace($tokens); + + return $structKeywordToken; + } + + /** + * @param \Iterator $tokens + * @return StructNameNode + */ + public function parseStructName(\Iterator &$tokens): StructNameNode + { + Scanner::assertType($tokens, TokenType::STRING); + + $structNameToken = $tokens->current(); + + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + + return new StructNameNode( + rangeInSource: $structNameToken->boundaries, + value: StructName::from($structNameToken->value) + ); + } + + /** + * @param \Iterator $tokens + * @return void + */ + public function skipOpeningBracketToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); + Scanner::skipOne($tokens); + Scanner::skipSpaceAndComments($tokens); + } + + /** + * @param \Iterator $tokens + * @return PropertyDeclarationNodes + */ + public function parsePropertyDeclarations(\Iterator &$tokens): PropertyDeclarationNodes + { + $this->propertyDeclarationParser ??= PropertyDeclarationParser::singleton(); + + $items = []; + while (Scanner::type($tokens) === TokenType::STRING) { + assert($this->propertyDeclarationParser !== null); + $items[] = $this->propertyDeclarationParser->parse($tokens); + Scanner::skipSpaceAndComments($tokens); + } + + return new PropertyDeclarationNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return Token + */ + public function extractClosingBracketToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); + + $closingBracketToken = $tokens->current(); + + Scanner::skipOne($tokens); + + return $closingBracketToken; + } +} diff --git a/src/Language/Parser/Tag/TagCouldNotBeParsed.php b/src/Language/Parser/Tag/TagCouldNotBeParsed.php new file mode 100644 index 00000000..91408753 --- /dev/null +++ b/src/Language/Parser/Tag/TagCouldNotBeParsed.php @@ -0,0 +1,64 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Tag; + +use PackageFactory\ComponentEngine\Domain\TagName\TagName; +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; + +final class TagCouldNotBeParsed extends ParserException +{ + public static function becauseOfClosingTagNameMismatch( + TagName $expectedTagName, + string $actualTagName, + Range $affectedRangeInSource + ): self { + return new self( + code: 1690976372, + message: sprintf( + 'Tag could not be parsed, because the closing tag name "%s" did not match the opening tag name "%s".', + $actualTagName, + $expectedTagName->value + ), + affectedRangeInSource: $affectedRangeInSource + ); + } + + public static function becauseOfUnexpectedToken( + TokenTypes $expectedTokenTypes, + Token $actualToken + ): self { + return new self( + code: 1691156112, + message: sprintf( + 'Tag could not be parsed because of unexpected token %s. ' + . 'Expected %s instead.', + $actualToken->toDebugString(), + $expectedTokenTypes->toDebugString() + ), + affectedRangeInSource: $actualToken->boundaries + ); + } +} diff --git a/src/Language/Parser/Tag/TagParser.php b/src/Language/Parser/Tag/TagParser.php new file mode 100644 index 00000000..ce63ed09 --- /dev/null +++ b/src/Language/Parser/Tag/TagParser.php @@ -0,0 +1,356 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Tag; + +use PackageFactory\ComponentEngine\Domain\AttributeName\AttributeName; +use PackageFactory\ComponentEngine\Domain\TagName\TagName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\ChildNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Language\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\Text\TextParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; + +final class TagParser +{ + use Singleton; + + private ?StringLiteralParser $stringLiteralParser = null; + private ?TextParser $textParser = null; + private ?ExpressionParser $expressionParser = null; + + /** + * @param \Iterator $tokens + * @return TagNode + */ + public function parse(\Iterator &$tokens): TagNode + { + $tagStartOpeningToken = $this->extractTagStartOpeningToken($tokens); + $tagNameNode = $this->parseTagName($tokens); + $attributeNodes = $this->parseAttributes($tokens); + + if ($tagSelfCloseToken = $this->extractTagSelfCloseToken($tokens)) { + return new TagNode( + rangeInSource: Range::from( + $tagStartOpeningToken->boundaries->start, + $tagSelfCloseToken->boundaries->end + ), + name: $tagNameNode, + attributes: $attributeNodes, + children: new ChildNodes(), + isSelfClosing: true + ); + } else { + $this->skipTagEndToken($tokens); + $children = $this->parseChildren($tokens); + $this->skipTagStartClosingToken($tokens); + $this->assertAndSkipClosingTagName($tokens, $tagNameNode); + $closingTagEndToken = $this->extractTagEndToken($tokens); + + return new TagNode( + rangeInSource: Range::from( + $tagStartOpeningToken->boundaries->start, + $closingTagEndToken->boundaries->end + ), + name: $tagNameNode, + attributes: $attributeNodes, + children: $children, + isSelfClosing: false + ); + } + } + + /** + * @param \Iterator $tokens + * @return Token + */ + private function extractTagStartOpeningToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::TAG_START_OPENING); + $tagStartOpeningToken = $tokens->current(); + Scanner::skipOne($tokens); + + return $tagStartOpeningToken; + } + + /** + * @param \Iterator $tokens + * @return TagNameNode + */ + private function parseTagName(\Iterator &$tokens): TagNameNode + { + Scanner::assertType($tokens, TokenType::STRING); + $tagNameToken = $tokens->current(); + Scanner::skipOne($tokens); + + return new TagNameNode( + rangeInSource: $tagNameToken->boundaries, + value: TagName::from($tagNameToken->value) + ); + } + + /** + * @param \Iterator $tokens + * @return AttributeNodes + */ + private function parseAttributes(\Iterator &$tokens): AttributeNodes + { + $items = []; + while (!$this->isTagEnd($tokens)) { + Scanner::skipSpace($tokens); + + $items[] = $this->parseAttribute($tokens); + + Scanner::skipSpace($tokens); + } + + return new AttributeNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return boolean + */ + private function isTagEnd(\Iterator $tokens): bool + { + return ( + Scanner::type($tokens) === TokenType::TAG_END || + Scanner::type($tokens) === TokenType::TAG_SELF_CLOSE + ); + } + + /** + * @param \Iterator $tokens + * @return AttributeNode + */ + private function parseAttribute(\Iterator &$tokens): AttributeNode + { + $attributeNameNode = $this->parseAttributeName($tokens); + $attributeValueNode = $this->parseAttributeValue($tokens); + + return new AttributeNode( + rangeInSource: Range::from( + $attributeNameNode->rangeInSource->start, + $attributeValueNode?->rangeInSource->end ?? + $attributeNameNode->rangeInSource->end + ), + name: $attributeNameNode, + value: $attributeValueNode + ); + } + + /** + * @param \Iterator $tokens + * @return AttributeNameNode + */ + private function parseAttributeName(\Iterator &$tokens): AttributeNameNode + { + Scanner::assertType($tokens, TokenType::STRING); + $attributeNameToken = $tokens->current(); + Scanner::skipOne($tokens); + + return new AttributeNameNode( + rangeInSource: $attributeNameToken->boundaries, + value: AttributeName::from($attributeNameToken->value) + ); + } + + /** + * @param \Iterator $tokens + * @return null|StringLiteralNode|ExpressionNode + */ + private function parseAttributeValue(\Iterator &$tokens): null|StringLiteralNode|ExpressionNode + { + if (Scanner::type($tokens) === TokenType::EQUALS) { + Scanner::skipOne($tokens); + + return match (Scanner::type($tokens)) { + TokenType::STRING_QUOTED => + $this->parseString($tokens), + TokenType::BRACKET_CURLY_OPEN => + $this->parseExpression($tokens), + default => throw TagCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::STRING_QUOTED, + TokenType::BRACKET_CURLY_OPEN + ), + actualToken: $tokens->current() + ) + }; + } + + return null; + } + + /** + * @param \Iterator $tokens + * @return StringLiteralNode + */ + private function parseString(\Iterator &$tokens): StringLiteralNode + { + $this->stringLiteralParser ??= StringLiteralParser::singleton(); + return $this->stringLiteralParser->parse($tokens); + } + + /** + * @param \Iterator $tokens + * @return ExpressionNode + */ + private function parseExpression(\Iterator &$tokens): ExpressionNode + { + $this->expressionParser ??= new ExpressionParser( + stopAt: TokenType::BRACKET_CURLY_CLOSE + ); + + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); + Scanner::skipOne($tokens); + + $expressionNode = $this->expressionParser->parse($tokens); + + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); + Scanner::skipOne($tokens); + + return $expressionNode; + } + + /** + * @param \Iterator $tokens + * @return null|Token + */ + private function extractTagSelfCloseToken(\Iterator &$tokens): ?Token + { + if (Scanner::type($tokens) === TokenType::TAG_SELF_CLOSE) { + $tagSelfCloseToken = $tokens->current(); + Scanner::skipOne($tokens); + + return $tagSelfCloseToken; + } + + return null; + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipTagEndToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::TAG_END); + Scanner::skipOne($tokens); + } + + /** + * @param \Iterator $tokens + * @return ChildNodes + */ + private function parseChildren(\Iterator &$tokens): ChildNodes + { + $items = []; + $preserveLeadingSpace = false; + while (Scanner::type($tokens) !== TokenType::TAG_START_CLOSING) { + $this->textParser ??= TextParser::singleton(); + if ($textNode = $this->textParser->parse($tokens, $preserveLeadingSpace)) { + $items[] = $textNode; + } + + if (Scanner::type($tokens) === TokenType::TAG_START_OPENING) { + $items[] = $this->parse($tokens); + $preserveLeadingSpace = Scanner::type($tokens) !== TokenType::END_OF_LINE; + continue; + } + + if (Scanner::type($tokens) === TokenType::BRACKET_CURLY_OPEN) { + $items[] = $this->parseExpression($tokens); + $preserveLeadingSpace = Scanner::type($tokens) !== TokenType::END_OF_LINE; + continue; + } + + if (Scanner::type($tokens) !== TokenType::TAG_START_CLOSING) { + throw TagCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::TAG_START_OPENING, + TokenType::TAG_START_CLOSING, + TokenType::BRACKET_CURLY_OPEN + ), + actualToken: $tokens->current() + ); + } + } + + return new ChildNodes(...$items); + } + + /** + * @param \Iterator $tokens + * @return void + */ + private function skipTagStartClosingToken(\Iterator &$tokens): void + { + Scanner::assertType($tokens, TokenType::TAG_START_CLOSING); + Scanner::skipOne($tokens); + } + + /** + * @param \Iterator $tokens + * @param TagNameNode $openingTagNameNode + * @return void + */ + private function assertAndSkipClosingTagName(\Iterator &$tokens, TagNameNode $openingTagNameNode): void + { + Scanner::assertType($tokens, TokenType::STRING); + $tagNameToken = $tokens->current(); + Scanner::skipOne($tokens); + + if ($tagNameToken->value !== $openingTagNameNode->value->value) { + throw TagCouldNotBeParsed::becauseOfClosingTagNameMismatch( + expectedTagName: $openingTagNameNode->value, + actualTagName: $tagNameToken->value, + affectedRangeInSource: $tagNameToken->boundaries + ); + } + } + + /** + * @param \Iterator $tokens + * @return Token + */ + private function extractTagEndToken(\Iterator &$tokens): Token + { + Scanner::assertType($tokens, TokenType::TAG_END); + $tagEndToken = $tokens->current(); + Scanner::skipOne($tokens); + + return $tagEndToken; + } +} diff --git a/src/Language/Parser/TemplateLiteral/TemplateLiteralParser.php b/src/Language/Parser/TemplateLiteral/TemplateLiteralParser.php new file mode 100644 index 00000000..509f0175 --- /dev/null +++ b/src/Language/Parser/TemplateLiteral/TemplateLiteralParser.php @@ -0,0 +1,132 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\TemplateLiteral; + +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralExpressionSegmentNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralSegments; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralStringSegmentNode; +use PackageFactory\ComponentEngine\Language\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class TemplateLiteralParser +{ + use Singleton; + + private ?ExpressionParser $expressionParser = null; + + /** + * @param \Iterator $tokens + * @return TemplateLiteralNode + */ + public function parse(\Iterator &$tokens): TemplateLiteralNode + { + Scanner::assertType($tokens, TokenType::TEMPLATE_LITERAL_START); + $startingDelimiterToken = $tokens->current(); + Scanner::skipOne($tokens); + + $segments = $this->parseSegments($tokens); + + Scanner::assertType($tokens, TokenType::TEMPLATE_LITERAL_END); + $finalDelimiterToken = $tokens->current(); + Scanner::skipOne($tokens); + + return new TemplateLiteralNode( + rangeInSource: Range::from( + $startingDelimiterToken->boundaries->start, + $finalDelimiterToken->boundaries->end + ), + segments: $segments + ); + } + + /** + * @param \Iterator $tokens + * @return TemplateLiteralSegments + */ + public function parseSegments(\Iterator &$tokens): TemplateLiteralSegments + { + $items = []; + while (Scanner::type($tokens) !== TokenType::TEMPLATE_LITERAL_END) { + $items[] = match (Scanner::type($tokens)) { + TokenType::STRING_QUOTED => $this->parseStringSegment($tokens), + TokenType::DOLLAR => $this->parseExpressionSegment($tokens), + default => throw new \Exception(__METHOD__ . ' for ' . Scanner::type($tokens)->value . ' is not implemented yet!') + }; + } + + return new TemplateLiteralSegments(...$items); + } + + /** + * @param \Iterator $tokens + * @return TemplateLiteralStringSegmentNode + */ + public function parseStringSegment(\Iterator &$tokens): TemplateLiteralStringSegmentNode + { + Scanner::assertType($tokens, TokenType::STRING_QUOTED); + $stringToken = $tokens->current(); + Scanner::skipOne($tokens); + + return new TemplateLiteralStringSegmentNode( + rangeInSource: $stringToken->boundaries, + value: $stringToken->value + ); + } + + /** + * @param \Iterator $tokens + * @return TemplateLiteralExpressionSegmentNode + */ + public function parseExpressionSegment(\Iterator &$tokens): TemplateLiteralExpressionSegmentNode + { + $this->expressionParser ??= new ExpressionParser( + stopAt: TokenType::BRACKET_CURLY_CLOSE + ); + + Scanner::assertType($tokens, TokenType::DOLLAR); + $dollarToken = $tokens->current(); + Scanner::skipOne($tokens); + + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); + Scanner::skipOne($tokens); + + $expression = $this->expressionParser->parse($tokens); + + Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); + $closingBracketToken = $tokens->current(); + Scanner::skipOne($tokens); + + return new TemplateLiteralExpressionSegmentNode( + rangeInSource: Range::from( + $dollarToken->boundaries->start, + $closingBracketToken->boundaries->end + ), + expression: $expression + ); + } +} diff --git a/src/Language/Parser/Text/TextParser.php b/src/Language/Parser/Text/TextParser.php new file mode 100644 index 00000000..25ef70c8 --- /dev/null +++ b/src/Language/Parser/Text/TextParser.php @@ -0,0 +1,159 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\Text; + +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class TextParser +{ + use Singleton; + + private string $value; + + private ?Token $startingToken; + private ?Token $finalToken; + + private bool $trimLeadingSpace; + private bool $trimTrailingSpace; + private bool $currentlyCapturingSpace; + private bool $trailingSpaceContainsLineBreak; + private bool $terminated; + + /** + * @param \Iterator $tokens + * @param boolean $preserveLeadingSpace + * @return null|TextNode + */ + public function parse(\Iterator &$tokens, bool $preserveLeadingSpace = false): ?TextNode + { + $this->reset($preserveLeadingSpace); + + while (!Scanner::isEnd($tokens) && !$this->terminated) { + $this->startingToken ??= $tokens->current(); + + match (Scanner::type($tokens)) { + TokenType::BRACKET_CURLY_OPEN, + TokenType::TAG_START_OPENING => + $this->terminateAtAdjacentChildNode(), + TokenType::TAG_START_CLOSING => + $this->terminateAtClosingTag(), + TokenType::SPACE => + $this->captureSpace($tokens->current()), + TokenType::END_OF_LINE => + $this->captureLineBreak($tokens->current()), + default => + $this->captureText($tokens->current()), + }; + + if (!$this->terminated) { + Scanner::skipOne($tokens); + } + } + + return $this->build(); + } + + private function reset(bool $preserveLeadingSpace): void + { + $this->value = ''; + + $this->startingToken = null; + $this->finalToken = null; + + $this->trimLeadingSpace = !$preserveLeadingSpace; + $this->trimTrailingSpace = true; + $this->currentlyCapturingSpace = false; + $this->trailingSpaceContainsLineBreak = false; + $this->terminated = false; + } + + private function terminateAtAdjacentChildNode(): void + { + $this->terminated = true; + $this->trimTrailingSpace = $this->trailingSpaceContainsLineBreak; + } + + private function terminateAtClosingTag(): void + { + $this->terminated = true; + } + + private function captureSpace(Token $token): void + { + $this->finalToken = $token; + + if ($this->currentlyCapturingSpace) { + return; + } + + $this->currentlyCapturingSpace = true; + $this->value .= ' '; + } + + private function captureLineBreak(Token $token): void + { + $this->captureSpace($token); + $this->trailingSpaceContainsLineBreak = true; + } + + private function captureText(Token $token): void + { + $this->finalToken = $token; + $this->currentlyCapturingSpace = false; + $this->trailingSpaceContainsLineBreak = false; + + $this->value .= $token->value; + } + + private function build(): ?TextNode + { + if (is_null($this->startingToken) || is_null($this->finalToken)) { + return null; + } + + if ($this->trimLeadingSpace) { + $this->value = ltrim($this->value); + } + + if ($this->trimTrailingSpace) { + $this->value = rtrim($this->value); + } + + if ($this->value === '' || $this->value === ' ') { + return null; + } + + return new TextNode( + rangeInSource: Range::from( + $this->startingToken->boundaries->start, + $this->finalToken->boundaries->end + ), + value: $this->value + ); + } +} diff --git a/src/Language/Parser/TypeReference/TypeReferenceCouldNotBeParsed.php b/src/Language/Parser/TypeReference/TypeReferenceCouldNotBeParsed.php new file mode 100644 index 00000000..7bf42746 --- /dev/null +++ b/src/Language/Parser/TypeReference/TypeReferenceCouldNotBeParsed.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\TypeReference; + +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\ParserException; + +final class TypeReferenceCouldNotBeParsed extends ParserException +{ + public static function becauseOfInvalidTypeReferenceNode( + InvalidTypeReferenceNode $cause + ): self { + return new self( + code: 1690542466, + message: sprintf( + 'TypeReferenceNode could not be parsed, because the result would be invalid: %s', + $cause->getMessage() + ), + affectedRangeInSource: $cause->affectedRangeInSource, + cause: $cause + ); + } + + public static function becauseOfInvalidTypeTypeNameNodes( + InvalidTypeNameNodes $cause + ): self { + return new self( + code: 1690833898, + message: sprintf( + 'TypeReferenceNode could not be parsed, because the list of type names was invalid: %s', + $cause->getMessage() + ), + affectedRangeInSource: $cause->affectedRangeInSource, + cause: $cause + ); + } +} diff --git a/src/Language/Parser/TypeReference/TypeReferenceParser.php b/src/Language/Parser/TypeReference/TypeReferenceParser.php new file mode 100644 index 00000000..f4fa7343 --- /dev/null +++ b/src/Language/Parser/TypeReference/TypeReferenceParser.php @@ -0,0 +1,151 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Language\Parser\TypeReference; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; + +final class TypeReferenceParser +{ + use Singleton; + + /** + * @param \Iterator $tokens + * @return TypeReferenceNode + */ + public function parse(\Iterator &$tokens): TypeReferenceNode + { + $startingToken = $tokens->current(); + $questionmarkToken = $this->extractQuestionmarkToken($tokens); + $isOptional = !is_null($questionmarkToken); + + $typeNameNodes = $this->parseTypeNames($tokens); + + $closingArrayToken = $this->extractClosingArrayToken($tokens); + $isArray = !is_null($closingArrayToken); + + $rangeInSource = Range::from( + $startingToken->boundaries->start, + $closingArrayToken?->boundaries->end + ?? $typeNameNodes->getLast()->rangeInSource->end + ); + + try { + return new TypeReferenceNode( + rangeInSource: $rangeInSource, + names: $typeNameNodes, + isArray: $isArray, + isOptional: $isOptional + ); + } catch (InvalidTypeReferenceNode $e) { + throw TypeReferenceCouldNotBeParsed::becauseOfInvalidTypeReferenceNode($e); + } + } + + /** + * @param \Iterator $tokens + * @return Token + */ + public function extractQuestionmarkToken(\Iterator &$tokens): ?Token + { + if (Scanner::type($tokens) === TokenType::QUESTIONMARK) { + $questionmarkToken = $tokens->current(); + Scanner::skipOne($tokens); + + return $questionmarkToken; + } + + return null; + } + + /** + * @param \Iterator $tokens + * @return TypeNameNodes + */ + public function parseTypeNames(\Iterator &$tokens): TypeNameNodes + { + $items = []; + while (true) { + $items[] = $this->parseTypeName($tokens); + + if (Scanner::isEnd($tokens) || Scanner::type($tokens) !== TokenType::PIPE) { + break; + } + + Scanner::skipOne($tokens); + } + + try { + return new TypeNameNodes(...$items); + } catch (InvalidTypeNameNodes $e) { + throw TypeReferenceCouldNotBeParsed::becauseOfInvalidTypeTypeNameNodes($e); + } + } + + /** + * @param \Iterator $tokens + * @return TypeNameNode + */ + public function parseTypeName(\Iterator &$tokens): TypeNameNode + { + Scanner::assertType($tokens, TokenType::STRING); + + $typeNameToken = $tokens->current(); + + Scanner::skipOne($tokens); + + return new TypeNameNode( + rangeInSource: $typeNameToken->boundaries, + value: TypeName::from($typeNameToken->value) + ); + } + + /** + * @param \Iterator $tokens + * @return Token + */ + public function extractClosingArrayToken(\Iterator &$tokens): ?Token + { + if (!Scanner::isEnd($tokens) && Scanner::type($tokens) === TokenType::BRACKET_SQUARE_OPEN) { + Scanner::skipOne($tokens); + Scanner::assertType($tokens, TokenType::BRACKET_SQUARE_CLOSE); + + $closingArrayToken = $tokens->current(); + + Scanner::skipOne($tokens); + + return $closingArrayToken; + } + + return null; + } +} diff --git a/src/Parser/Ast/PropertyDeclarationNode.php b/src/Language/Parser/ValueReference/ValueReferenceParser.php similarity index 55% rename from src/Parser/Ast/PropertyDeclarationNode.php rename to src/Language/Parser/ValueReference/ValueReferenceParser.php index b1e2ab38..f955647c 100644 --- a/src/Parser/Ast/PropertyDeclarationNode.php +++ b/src/Language/Parser/ValueReference/ValueReferenceParser.php @@ -2,7 +2,7 @@ /** * PackageFactory.ComponentEngine - Universal View Components for PHP - * Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine + * Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,49 +20,34 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Parser\Ast; +namespace PackageFactory\ComponentEngine\Language\Parser\ValueReference; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; -final class PropertyDeclarationNode implements \JsonSerializable +final class ValueReferenceParser { - private function __construct( - public readonly string $name, - public readonly TypeReferenceNode $type - ) { - } + use Singleton; /** * @param \Iterator $tokens - * @return self + * @return ValueReferenceNode */ - public static function fromTokens(\Iterator $tokens): self + public function parse(\Iterator &$tokens): ValueReferenceNode { - Scanner::skipSpaceAndComments($tokens); Scanner::assertType($tokens, TokenType::STRING); - $name = Scanner::value($tokens); + $token = $tokens->current(); Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::COLON); - Scanner::skipOne($tokens); - - $type = TypeReferenceNode::fromTokens($tokens); - return new self( - name: $name, - type: $type + return new ValueReferenceNode( + rangeInSource: $token->boundaries, + name: VariableName::from($token->value) ); } - - public function jsonSerialize(): mixed - { - return [ - 'name' => $this->name, - 'type' => $this->type - ]; - } } diff --git a/src/Module/Loader/ModuleFile/Module.php b/src/Module/Loader/ModuleFile/Module.php new file mode 100644 index 00000000..cd62d50e --- /dev/null +++ b/src/Module/Loader/ModuleFile/Module.php @@ -0,0 +1,85 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Module\Loader\ModuleFile; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Module\ModuleId; +use PackageFactory\ComponentEngine\Module\ModuleInterface; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; +use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; +use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Properties; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Property; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; +use PackageFactory\ComponentEngine\TypeSystem\TypeReference; + +final class Module implements ModuleInterface +{ + public function __construct( + private readonly ModuleId $moduleId, + private readonly ModuleNode $moduleNode + ) { + } + + public function getTypeOf(VariableName $exportName): AtomicTypeInterface + { + $exportNode = $this->moduleNode->export; + + return match ($exportNode->declaration::class) { + ComponentDeclarationNode::class => ComponentType::fromComponentDeclarationNode($exportNode->declaration), + EnumDeclarationNode::class => EnumStaticType::fromModuleIdAndDeclaration( + $this->moduleId, + $exportNode->declaration, + ), + StructDeclarationNode::class => + $this->createStructTypeFromStructDeclarationNode($exportNode->declaration) + }; + } + + private function createStructTypeFromStructDeclarationNode( + StructDeclarationNode $structDeclarationNode + ): StructType { + return new StructType( + name: $structDeclarationNode->name->value, + properties: new Properties( + ...array_map( + fn (PropertyDeclarationNode $propertyDeclarationNode) => + new Property( + name: $propertyDeclarationNode->name->value, + type: new TypeReference( + names: $propertyDeclarationNode->type->names->toTypeNames(), + isOptional: $propertyDeclarationNode->type->isOptional, + isArray: $propertyDeclarationNode->type->isArray + ) + ), + $structDeclarationNode->properties->items + ) + ) + ); + } +} diff --git a/src/Module/Loader/ModuleFile/ModuleFileLoader.php b/src/Module/Loader/ModuleFile/ModuleFileLoader.php index 7581f0b3..859fb413 100644 --- a/src/Module/Loader/ModuleFile/ModuleFileLoader.php +++ b/src/Module/Loader/ModuleFile/ModuleFileLoader.php @@ -22,48 +22,39 @@ namespace PackageFactory\ComponentEngine\Module\Loader\ModuleFile; +use PackageFactory\ComponentEngine\Language\Parser\Module\ModuleParser; use PackageFactory\ComponentEngine\Module\LoaderInterface; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ImportNode; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Module\ModuleInterface; use PackageFactory\ComponentEngine\Parser\Source\Path; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; -use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; -use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; -use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class ModuleFileLoader implements LoaderInterface { - public function resolveTypeOfImport(ImportNode $importNode): TypeInterface + public function __construct( + private readonly Path $sourcePath + ) { + } + + public function loadModule(string $pathToModule): ModuleInterface { - $pathToImportFrom = $importNode->source->path->resolveRelationTo( - Path::fromString($importNode->path) + $pathToImportFrom = $this->sourcePath->resolveRelationTo( + Path::fromString($pathToModule) ); $source = Source::fromFile($pathToImportFrom->value); $tokenizer = Tokenizer::fromSource($source); - $module = ModuleNode::fromTokens($tokenizer->getIterator()); - $export = $module->exports->get($importNode->name->value); + $tokens = $tokenizer->getIterator(); - if ($export === null) { - throw new \Exception( - '@TODO: Module "' . $importNode->path . '" has no exported member "' . $importNode->name->value . '".' - ); - } + $moduleParser = ModuleParser::singleton(); $moduleId = ModuleId::fromSource($source); + $moduleNode = $moduleParser->parse($tokens); - return match ($export->declaration::class) { - ComponentDeclarationNode::class => ComponentType::fromComponentDeclarationNode($export->declaration), - EnumDeclarationNode::class => EnumStaticType::fromModuleIdAndDeclaration( - $moduleId, - $export->declaration, - ), - StructDeclarationNode::class => StructType::fromStructDeclarationNode($export->declaration) - }; + + return new Module( + moduleId: $moduleId, + moduleNode: $moduleNode + ); } } diff --git a/src/Module/LoaderInterface.php b/src/Module/LoaderInterface.php index 7cab3ee5..afba9335 100644 --- a/src/Module/LoaderInterface.php +++ b/src/Module/LoaderInterface.php @@ -22,10 +22,7 @@ namespace PackageFactory\ComponentEngine\Module; -use PackageFactory\ComponentEngine\Parser\Ast\ImportNode; -use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; - interface LoaderInterface { - public function resolveTypeOfImport(ImportNode $importNode): TypeInterface; -} \ No newline at end of file + public function loadModule(string $pathToModule): ModuleInterface; +} diff --git a/src/Module/ModuleInterface.php b/src/Module/ModuleInterface.php new file mode 100644 index 00000000..14468ecf --- /dev/null +++ b/src/Module/ModuleInterface.php @@ -0,0 +1,31 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Module; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; + +interface ModuleInterface +{ + public function getTypeOf(VariableName $exportName): AtomicTypeInterface; +} diff --git a/src/Parser/Ast/AccessChainSegmentNode.php b/src/Parser/Ast/AccessChainSegmentNode.php deleted file mode 100644 index bad435b6..00000000 --- a/src/Parser/Ast/AccessChainSegmentNode.php +++ /dev/null @@ -1,66 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Definition\AccessType; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class AccessChainSegmentNode implements \JsonSerializable -{ - private function __construct( - public readonly AccessType $accessType, - public readonly IdentifierNode $accessor - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::PERIOD, TokenType::OPTCHAIN); - - $accessType = AccessType::fromTokenType(Scanner::type($tokens)); - - Scanner::skipOne($tokens); - - $accessor = IdentifierNode::fromTokens($tokens); - - return new self( - accessType: $accessType, - accessor: $accessor - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'accessType' => $this->accessType->value, - 'accessor' => $this->accessor - ]; - } -} diff --git a/src/Parser/Ast/AccessChainSegmentNodes.php b/src/Parser/Ast/AccessChainSegmentNodes.php deleted file mode 100644 index 76f8e7de..00000000 --- a/src/Parser/Ast/AccessChainSegmentNodes.php +++ /dev/null @@ -1,73 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class AccessChainSegmentNodes implements \JsonSerializable -{ - /** - * @var AccessChainSegmentNode[] - */ - public readonly array $items; - - private function __construct( - AccessChainSegmentNode ...$items - ) { - $this->items = $items; - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $items = []; - while (true) { - Scanner::skipSpaceAndComments($tokens); - - if (Scanner::isEnd($tokens)) { - break; - } - - switch (Scanner::type($tokens)) { - case TokenType::PERIOD: - case TokenType::OPTCHAIN: - $items[] = AccessChainSegmentNode::fromTokens($tokens); - break; - default: - break 2; - } - } - - return new self(...$items); - } - - public function jsonSerialize(): mixed - { - return $this->items; - } -} diff --git a/src/Parser/Ast/AttributeNode.php b/src/Parser/Ast/AttributeNode.php deleted file mode 100644 index 7c140eb1..00000000 --- a/src/Parser/Ast/AttributeNode.php +++ /dev/null @@ -1,90 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class AttributeNode implements \JsonSerializable -{ - private function __construct( - public readonly string $name, - public readonly ExpressionNode | StringLiteralNode $value - ) { - } - - public static function fromString(string $attributeAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($attributeAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::STRING); - - $name = Scanner::value($tokens); - - Scanner::skipOne($tokens); - Scanner::assertType($tokens, TokenType::EQUALS); - Scanner::skipOne($tokens); - - if (Scanner::type($tokens) === TokenType::STRING_QUOTED) { - $value = StringLiteralNode::fromTokens($tokens); - } else { - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); - Scanner::skipOne($tokens); - - $value = ExpressionNode::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - } - - - return new self( - name: $name, - value: $value - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'name' => $this->name, - 'value' => $this->value - ]; - } -} diff --git a/src/Parser/Ast/AttributeNodes.php b/src/Parser/Ast/AttributeNodes.php deleted file mode 100644 index b829373c..00000000 --- a/src/Parser/Ast/AttributeNodes.php +++ /dev/null @@ -1,77 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class AttributeNodes implements \JsonSerializable -{ - /** - * @var array - */ - public readonly array $items; - - private function __construct( - AttributeNode ...$items - ) { - $itemsAsHashMap = []; - foreach ($items as $attribute) { - $itemsAsHashMap[$attribute->name] = $attribute; - } - - $this->items = $itemsAsHashMap; - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $items = []; - - while (true) { - Scanner::skipSpaceAndComments($tokens); - - switch (Scanner::type($tokens)) { - case TokenType::TAG_END: - case TokenType::TAG_SELF_CLOSE: - break 2; - case TokenType::STRING: - $items[] = AttributeNode::fromTokens($tokens); - break; - default: - Scanner::assertType($tokens, TokenType::TAG_END, TokenType::TAG_SELF_CLOSE, TokenType::STRING); - } - } - - return new self(...$items); - } - - public function jsonSerialize(): mixed - { - return array_values($this->items); - } -} diff --git a/src/Parser/Ast/BinaryOperationNode.php b/src/Parser/Ast/BinaryOperationNode.php deleted file mode 100644 index f3dcd3bd..00000000 --- a/src/Parser/Ast/BinaryOperationNode.php +++ /dev/null @@ -1,75 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Definition\BinaryOperator; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; - -final class BinaryOperationNode implements \JsonSerializable -{ - private function __construct( - public readonly ExpressionNode $left, - public readonly BinaryOperator $operator, - public readonly ExpressionNode $right - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(ExpressionNode $left, \Iterator $tokens): self - { - Scanner::skipSpace($tokens); - - $operator = BinaryOperator::fromTokenType(Scanner::type($tokens)); - - Scanner::skipOne($tokens); - - $precedence = $operator->toPrecedence(); - - Scanner::skipSpaceAndComments($tokens); - - $right = ExpressionNode::fromTokens($tokens, $precedence); - - Scanner::skipSpaceAndComments($tokens); - - return new self( - left: $left, - operator: $operator, - right: $right - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'BinaryOperationNode', - 'payload' => [ - 'operator' => $this->operator, - 'operands' => [$this->left, $this->right] - ] - ]; - } -} diff --git a/src/Parser/Ast/ComponentDeclarationNode.php b/src/Parser/Ast/ComponentDeclarationNode.php deleted file mode 100644 index f5f8c8e4..00000000 --- a/src/Parser/Ast/ComponentDeclarationNode.php +++ /dev/null @@ -1,102 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class ComponentDeclarationNode implements \JsonSerializable -{ - private function __construct( - public readonly string $componentName, - public readonly PropertyDeclarationNodes $propertyDeclarations, - public readonly ExpressionNode $returnExpression - ) { - } - - public static function fromString(string $componentDeclarationAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($componentDeclarationAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_COMPONENT); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::STRING); - - $componentName = Scanner::value($tokens); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - - $propertyDeclarations = PropertyDeclarationNodes::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_RETURN); - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - - $returnExpression = ExpressionNode::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - - return new self( - componentName: $componentName, - propertyDeclarations: $propertyDeclarations, - returnExpression: $returnExpression - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'ComponentDeclarationNode', - 'payload' => [ - 'componentName' => $this->componentName, - 'propertyDeclarations' => $this->propertyDeclarations, - 'returnExpression' => $this->returnExpression - ] - ]; - } -} diff --git a/src/Parser/Ast/EnumDeclarationNode.php b/src/Parser/Ast/EnumDeclarationNode.php deleted file mode 100644 index ba27a873..00000000 --- a/src/Parser/Ast/EnumDeclarationNode.php +++ /dev/null @@ -1,90 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class EnumDeclarationNode implements \JsonSerializable -{ - private function __construct( - public readonly string $enumName, - public readonly EnumMemberDeclarationNodes $memberDeclarations - ) { - } - - public static function fromString(string $enumDeclarationAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($enumDeclarationAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_ENUM); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::STRING); - - $enumName = Scanner::value($tokens); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); - Scanner::skipOne($tokens); - - $memberDeclarations = EnumMemberDeclarationNodes::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - - return new self( - enumName: $enumName, - memberDeclarations: $memberDeclarations - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'EnumDeclarationNode', - 'payload' => [ - 'enumName' => $this->enumName, - 'memberDeclarations' => $this->memberDeclarations - ] - ]; - } -} diff --git a/src/Parser/Ast/EnumMemberDeclarationNode.php b/src/Parser/Ast/EnumMemberDeclarationNode.php deleted file mode 100644 index ccc03929..00000000 --- a/src/Parser/Ast/EnumMemberDeclarationNode.php +++ /dev/null @@ -1,78 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class EnumMemberDeclarationNode implements \JsonSerializable -{ - private function __construct( - public readonly string $name, - public readonly null|StringLiteralNode|IntegerLiteralNode $value - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::STRING); - - $name = Scanner::value($tokens); - - Scanner::skipOne($tokens); - - $value = null; - if (Scanner::type($tokens) === TokenType::BRACKET_ROUND_OPEN) { - Scanner::skipOne($tokens); - $value = match (Scanner::type($tokens)) { - /** @phpstan-ignore-next-line */ - TokenType::STRING_QUOTED => StringLiteralNode::fromTokens($tokens), - /** @phpstan-ignore-next-line */ - TokenType::NUMBER_DECIMAL => IntegerLiteralNode::fromTokens($tokens), - default => throw new \Exception('@TODO: Unexpected Token ' . Scanner::type($tokens)->value) - }; - /** @phpstan-ignore-next-line */ - Scanner::assertType($tokens, TokenType::BRACKET_ROUND_CLOSE); - Scanner::skipOne($tokens); - } - - return new self( - name: $name, - value: $value - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'name' => $this->name, - 'value' => $this->value, - ]; - } -} diff --git a/src/Parser/Ast/EnumMemberDeclarationNodes.php b/src/Parser/Ast/EnumMemberDeclarationNodes.php deleted file mode 100644 index ad00c190..00000000 --- a/src/Parser/Ast/EnumMemberDeclarationNodes.php +++ /dev/null @@ -1,75 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class EnumMemberDeclarationNodes implements \JsonSerializable -{ - /** - * @var array - */ - public readonly array $items; - - private function __construct( - EnumMemberDeclarationNode ...$items - ) { - $itemsAsHashMap = []; - foreach ($items as $item) { - $itemsAsHashMap[$item->name] = $item; - } - - $this->items = $itemsAsHashMap; - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $items = []; - while (true) { - Scanner::skipSpaceAndComments($tokens); - - switch (Scanner::type($tokens)) { - case TokenType::STRING: - $items[] = EnumMemberDeclarationNode::fromTokens($tokens); - break; - case TokenType::BRACKET_CURLY_CLOSE: - break 2; - default: - Scanner::assertType($tokens, TokenType::STRING, TokenType::BRACKET_CURLY_CLOSE); - } - } - - return new self(...$items); - } - - public function jsonSerialize(): mixed - { - return $this->items; - } -} diff --git a/src/Parser/Ast/ExportNode.php b/src/Parser/Ast/ExportNode.php deleted file mode 100644 index 0d060a00..00000000 --- a/src/Parser/Ast/ExportNode.php +++ /dev/null @@ -1,70 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class ExportNode implements \JsonSerializable -{ - private function __construct( - public readonly ComponentDeclarationNode | EnumDeclarationNode | StructDeclarationNode $declaration, - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_EXPORT); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - - $declaration = match ($tokens->current()->type) { - TokenType::KEYWORD_COMPONENT => - ComponentDeclarationNode::fromTokens($tokens), - - TokenType::KEYWORD_STRUCT => - StructDeclarationNode::fromTokens($tokens), - - TokenType::KEYWORD_ENUM => - EnumDeclarationNode::fromTokens($tokens), - - default => throw new \Exception('@TODO: Unexpected Token (outside of Scanner class)') - }; - - return new self( - declaration: $declaration - ); - } - - public function jsonSerialize(): mixed - { - return $this->declaration; - } -} diff --git a/src/Parser/Ast/ExportNodes.php b/src/Parser/Ast/ExportNodes.php deleted file mode 100644 index 8b8b90e7..00000000 --- a/src/Parser/Ast/ExportNodes.php +++ /dev/null @@ -1,74 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\InterfaceDeclarationNode; - -final class ExportNodes implements \JsonSerializable -{ - /** - * @var array - */ - public readonly array $items; - - /** - * @param array $items - */ - private function __construct( - array $items - ) { - $this->items = $items; - } - - public static function empty(): self - { - return new self([]); - } - - public function withAddedExport(ExportNode $export): self - { - $name = match ($export->declaration::class) { - ComponentDeclarationNode::class => $export->declaration->componentName, - StructDeclarationNode::class => $export->declaration->structName, - EnumDeclarationNode::class => $export->declaration->enumName - }; - - if (array_key_exists($name, $this->items)) { - throw new \Exception('@TODO: Duplicate Export ' . $name); - } - - return new self([...$this->items, ...[$name => $export]]); - } - - public function get(string $name): ?ExportNode - { - return $this->items[$name] ?? null; - } - - public function jsonSerialize(): mixed - { - return array_values($this->items); - } -} diff --git a/src/Parser/Ast/ExpressionNode.php b/src/Parser/Ast/ExpressionNode.php deleted file mode 100644 index 3d6bde7a..00000000 --- a/src/Parser/Ast/ExpressionNode.php +++ /dev/null @@ -1,169 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Definition\Precedence; -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\LookAhead; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class ExpressionNode implements \JsonSerializable -{ - private function __construct( - public readonly IdentifierNode | IntegerLiteralNode | BinaryOperationNode | UnaryOperationNode | AccessNode | TernaryOperationNode | TagNode | StringLiteralNode | MatchNode | TemplateLiteralNode | BooleanLiteralNode | NullLiteralNode $root - ) { - } - - public static function fromString(string $expressionAsString): self - { - $tokens = Tokenizer::fromSource( - Source::fromString($expressionAsString) - )->getIterator(); - return self::fromTokens( - $tokens - ); - } - - /** - * @param \Iterator $tokens - * @param Precedence $precedence - * @return self - */ - public static function fromTokens(\Iterator &$tokens, Precedence $precedence = Precedence::SEQUENCE): self - { - Scanner::skipSpaceAndComments($tokens); - - $root = null; - switch (Scanner::type($tokens)) { - case TokenType::BRACKET_ROUND_OPEN: - $lookAhead = LookAhead::fromTokens($tokens); - $lookAhead->shift(); - - while (true) { - switch ($lookAhead->type()) { - case TokenType::STRING: - case TokenType::COLON: - case TokenType::COMMA: - case TokenType::BRACKET_SQUARE_OPEN: - case TokenType::BRACKET_SQUARE_CLOSE: - case TokenType::BRACKET_ROUND_CLOSE: - case TokenType::SPACE: - case TokenType::END_OF_LINE: - $lookAhead->shift(); - break; - default: - $tokens = $lookAhead->getIterator(); - Scanner::skipOne($tokens); - $root = self::fromTokens($tokens)->root; - Scanner::assertType($tokens, TokenType::BRACKET_ROUND_CLOSE); - Scanner::skipOne($tokens); - break 2; - } - } - break; - case TokenType::NUMBER_BINARY: - case TokenType::NUMBER_OCTAL: - case TokenType::NUMBER_DECIMAL: - case TokenType::NUMBER_HEXADECIMAL: - case TokenType::PERIOD: - $root = IntegerLiteralNode::fromTokens($tokens); - break; - case TokenType::KEYWORD_TRUE: - case TokenType::KEYWORD_FALSE: - $root = BooleanLiteralNode::fromTokens($tokens); - break; - case TokenType::KEYWORD_NULL: - $root = NullLiteralNode::fromTokens($tokens); - break; - case TokenType::KEYWORD_MATCH: - $root = MatchNode::fromTokens($tokens); - break; - case TokenType::TAG_START_OPENING: - $root = TagNode::fromTokens($tokens); - break; - case TokenType::STRING_QUOTED: - $root = StringLiteralNode::fromTokens($tokens); - break; - case TokenType::TEMPLATE_LITERAL_START: - $root = TemplateLiteralNode::fromTokens($tokens); - break; - case TokenType::OPERATOR_BOOLEAN_NOT: - $root = UnaryOperationNode::fromTokens($tokens); - break; - default: - $root = IdentifierNode::fromTokens($tokens); - break; - } - - if ($root === null) { - throw new \Exception('@TODO: What on earth went wrong here?'); - } - - Scanner::skipSpaceAndComments($tokens); - if (Scanner::isEnd($tokens) || $precedence->mustStopAt(Scanner::type($tokens))) { - return new self( - root: $root - ); - } - - while (!Scanner::isEnd($tokens) && !$precedence->mustStopAt(Scanner::type($tokens))) { - Scanner::skipSpaceAndComments($tokens); - switch (Scanner::type($tokens)) { - case TokenType::OPERATOR_BOOLEAN_AND: - case TokenType::OPERATOR_BOOLEAN_OR: - case TokenType::COMPARATOR_EQUAL: - case TokenType::COMPARATOR_NOT_EQUAL: - case TokenType::COMPARATOR_GREATER_THAN: - case TokenType::COMPARATOR_GREATER_THAN_OR_EQUAL: - case TokenType::COMPARATOR_LESS_THAN: - case TokenType::COMPARATOR_LESS_THAN_OR_EQUAL: - $root = BinaryOperationNode::fromTokens(new self(root: $root), $tokens); - break; - case TokenType::PERIOD: - case TokenType::OPTCHAIN: - $root = AccessNode::fromTokens(new self(root: $root), $tokens); - break; - case TokenType::QUESTIONMARK: - $root = TernaryOperationNode::fromTokens(new self(root: $root), $tokens); - break; - case TokenType::ARROW_SINGLE: - default: - break 2; - } - - Scanner::skipSpaceAndComments($tokens); - } - - return new self( - root: $root - ); - } - - public function jsonSerialize(): mixed - { - return $this->root; - } -} diff --git a/src/Parser/Ast/ExpressionNodes.php b/src/Parser/Ast/ExpressionNodes.php deleted file mode 100644 index e16b0e75..00000000 --- a/src/Parser/Ast/ExpressionNodes.php +++ /dev/null @@ -1,88 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class ExpressionNodes implements \JsonSerializable -{ - /** - * @var ExpressionNode[] - */ - public readonly array $items; - - private function __construct( - ExpressionNode ...$items - ) { - $this->items = $items; - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $items = []; - Scanner::skipSpaceAndComments($tokens); - - switch (Scanner::type($tokens)) { - case TokenType::BRACKET_ROUND_CLOSE: - case TokenType::BRACKET_CURLY_CLOSE: - case TokenType::BRACKET_SQUARE_CLOSE: - case TokenType::ARROW_SINGLE: - return new self(); - default: - break; - } - - while (true) { - $items[] = ExpressionNode::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - - switch (Scanner::type($tokens)) { - case TokenType::BRACKET_ROUND_CLOSE: - case TokenType::BRACKET_CURLY_CLOSE: - case TokenType::BRACKET_SQUARE_CLOSE: - case TokenType::ARROW_SINGLE: - break 2; - default: - Scanner::assertType($tokens, TokenType::COMMA); - Scanner::skipOne($tokens); - break; - } - - Scanner::skipSpaceAndComments($tokens); - } - - return new self(...$items); - } - - public function jsonSerialize(): mixed - { - return $this->items; - } -} diff --git a/src/Parser/Ast/IdentifierNode.php b/src/Parser/Ast/IdentifierNode.php deleted file mode 100644 index 5683bba0..00000000 --- a/src/Parser/Ast/IdentifierNode.php +++ /dev/null @@ -1,72 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class IdentifierNode implements \JsonSerializable -{ - private function __construct( - public readonly string $value - ) { - } - - public static function fromString(string $identifierAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($identifierAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::STRING); - - $value = Scanner::value($tokens); - - Scanner::skipOne($tokens); - - return new self( - value: $value - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'Identifier', - 'payload' => $this->value - ]; - } -} diff --git a/src/Parser/Ast/ImportNode.php b/src/Parser/Ast/ImportNode.php deleted file mode 100644 index 32446b37..00000000 --- a/src/Parser/Ast/ImportNode.php +++ /dev/null @@ -1,90 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class ImportNode implements \JsonSerializable -{ - private function __construct( - public readonly Source $source, - public readonly string $path, - public readonly IdentifierNode $name - ) { - } - - /** - * @param \Iterator $tokens - * @return \Iterator - */ - public static function fromTokens(\Iterator $tokens): \Iterator - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_FROM); - - $source = Scanner::source($tokens); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - - $path = Scanner::value($tokens); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_IMPORT); - Scanner::skipOne($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); - Scanner::skipOne($tokens); - - while (true) { - $identifier = IdentifierNode::fromTokens($tokens); - yield new self($source, $path, $identifier); - - Scanner::skipSpaceAndComments($tokens); - if (Scanner::type($tokens) === TokenType::COMMA) { - Scanner::skipOne($tokens); - continue; - } - - break; - } - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - } - - public function jsonSerialize(): mixed - { - return [ - 'path' => $this->path, - 'name' => $this->name - ]; - } -} diff --git a/src/Parser/Ast/IntegerLiteralNode.php b/src/Parser/Ast/IntegerLiteralNode.php deleted file mode 100644 index d3e26643..00000000 --- a/src/Parser/Ast/IntegerLiteralNode.php +++ /dev/null @@ -1,64 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Definition\IntegerFormat; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; - -final class IntegerLiteralNode implements \JsonSerializable -{ - private function __construct( - public readonly string $value, - public readonly IntegerFormat $format - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $format = IntegerFormat::fromTokenType(Scanner::type($tokens)); - $value = Scanner::value($tokens); - - Scanner::skipOne($tokens); - - return new self( - value: $value, - format: $format - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'IntegerLiteralNode', - 'payload' => [ - 'value' => $this->value, - 'format' => $this->format->value - ] - ]; - } -} diff --git a/src/Parser/Ast/MatchArmNode.php b/src/Parser/Ast/MatchArmNode.php deleted file mode 100644 index 5d96a814..00000000 --- a/src/Parser/Ast/MatchArmNode.php +++ /dev/null @@ -1,76 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class MatchArmNode implements \JsonSerializable -{ - private function __construct( - public readonly null | ExpressionNodes $left, - public readonly ExpressionNode $right - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - - if (Scanner::type($tokens) === TokenType::KEYWORD_DEFAULT) { - $left = null; - Scanner::skipOne($tokens); - } else { - $left = ExpressionNodes::fromTokens($tokens); - } - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::ARROW_SINGLE); - Scanner::skipOne($tokens); - - Scanner::skipSpaceAndComments($tokens); - - $right = ExpressionNode::fromTokens($tokens); - - return new self( - left: $left, - right: $right - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'MatchArmNode', - 'payload' => [ - 'left' => $this->left, - 'right' => $this->right - ] - ]; - } -} diff --git a/src/Parser/Ast/MatchNode.php b/src/Parser/Ast/MatchNode.php deleted file mode 100644 index b975a020..00000000 --- a/src/Parser/Ast/MatchNode.php +++ /dev/null @@ -1,83 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class MatchNode implements \JsonSerializable -{ - private function __construct( - public readonly ExpressionNode $subject, - public readonly MatchArmNodes $arms - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_MATCH); - Scanner::skipOne($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_ROUND_OPEN); - Scanner::skipOne($tokens); - - $subject = ExpressionNode::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_ROUND_CLOSE); - Scanner::skipOne($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); - Scanner::skipOne($tokens); - - $arms = MatchArmNodes::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - - return new self( - subject: $subject, - arms: $arms - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'MatchNode', - 'payload' => [ - 'subject' => $this->subject, - 'arms' => $this->arms - ] - ]; - } -} diff --git a/src/Parser/Ast/ModuleNode.php b/src/Parser/Ast/ModuleNode.php deleted file mode 100644 index e49b03f9..00000000 --- a/src/Parser/Ast/ModuleNode.php +++ /dev/null @@ -1,92 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; - -final class ModuleNode implements \JsonSerializable -{ - private function __construct( - public readonly ImportNodes $imports, - public readonly ExportNodes $exports, - ) { - } - - public static function fromString(string $moduleAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($moduleAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $imports = ImportNodes::empty(); - $exports = ExportNodes::empty(); - - while ($tokens->valid()) { - Scanner::skipSpaceAndComments($tokens); - if ($tokens->valid()) { - switch ($tokens->current()->type) { - case TokenType::KEYWORD_FROM: - foreach (ImportNode::fromTokens($tokens) as $import) { - $imports = $imports->withAddedImport($import); - } - break; - case TokenType::KEYWORD_EXPORT: - $exports = $exports->withAddedExport(ExportNode::fromTokens($tokens)); - break; - default: - Scanner::assertType($tokens, TokenType::KEYWORD_FROM, TokenType::KEYWORD_EXPORT); - break; - } - } - } - - return new self( - imports: $imports, - exports: $exports - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'ModuleNode', - 'payload' => [ - 'imports' => $this->imports, - 'exports' => $this->exports - ] - ]; - } -} diff --git a/src/Parser/Ast/PropertyDeclarationNodes.php b/src/Parser/Ast/PropertyDeclarationNodes.php deleted file mode 100644 index 12279ed5..00000000 --- a/src/Parser/Ast/PropertyDeclarationNodes.php +++ /dev/null @@ -1,105 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class PropertyDeclarationNodes implements \JsonSerializable -{ - /** - * @var array - */ - public readonly array $items; - - /** - * @param array $items - */ - private function __construct( - array $items - ) { - $this->items = $items; - } - - public static function empty(): self - { - return new self([]); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $result = self::empty(); - - while (true) { - Scanner::skipSpaceAndComments($tokens); - - switch (Scanner::type($tokens)) { - case TokenType::KEYWORD_RETURN: - break 2; - case TokenType::BRACKET_CURLY_CLOSE: - break 2; - case TokenType::STRING: - $result = $result->withAddedPropertyDeclarationNode( - PropertyDeclarationNode::fromTokens($tokens) - ); - break; - default: - Scanner::assertType($tokens, TokenType::KEYWORD_RETURN, TokenType::BRACKET_CURLY_CLOSE, TokenType::STRING); - } - } - - return $result; - } - - public function withAddedPropertyDeclarationNode( - PropertyDeclarationNode $propertyDeclarationNode - ): self { - $name = $propertyDeclarationNode->name; - - if (array_key_exists($name, $this->items)) { - throw new \Exception('@TODO: Duplicate Property Declaration ' . $name); - } - - return new self([...$this->items, ...[$name => $propertyDeclarationNode]]); - } - - public function getPropertyDeclarationNodeOfName(string $name): ?PropertyDeclarationNode - { - return $this->items[$name] ?? null; - } - - public function isEmpty(): bool - { - return count($this->items) === 0; - } - - public function jsonSerialize(): mixed - { - return array_values($this->items); - } -} diff --git a/src/Parser/Ast/StringLiteralNode.php b/src/Parser/Ast/StringLiteralNode.php deleted file mode 100644 index 53af12b4..00000000 --- a/src/Parser/Ast/StringLiteralNode.php +++ /dev/null @@ -1,71 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class StringLiteralNode implements \JsonSerializable -{ - private function __construct( - public readonly string $value - ) { - } - - public static function fromString(string $stringLiteralAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($stringLiteralAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::assertType($tokens, TokenType::STRING_QUOTED); - - $value = Scanner::value($tokens); - - Scanner::skipOne($tokens); - - return new self( - value: $value - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'StringLiteralNode', - 'payload' => $this->value - ]; - } -} diff --git a/src/Parser/Ast/StructDeclarationNode.php b/src/Parser/Ast/StructDeclarationNode.php deleted file mode 100644 index 24073f38..00000000 --- a/src/Parser/Ast/StructDeclarationNode.php +++ /dev/null @@ -1,92 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class StructDeclarationNode implements \JsonSerializable -{ - private function __construct( - public readonly string $structName, - public readonly PropertyDeclarationNodes $propertyDeclarations - ) { - } - - public static function fromString(string $structDeclarationAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($structDeclarationAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::KEYWORD_STRUCT); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::STRING); - - $structName = Scanner::value($tokens); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); - - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - - $propertyDeclarations = PropertyDeclarationNodes::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - - return new self( - structName: $structName, - propertyDeclarations: $propertyDeclarations - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'Struct', - 'payload' => [ - 'structName' => $this->structName, - 'propertyDeclarations' => $this->propertyDeclarations - ] - ]; - } -} diff --git a/src/Parser/Ast/TagContentNode.php b/src/Parser/Ast/TagContentNode.php deleted file mode 100644 index 31097089..00000000 --- a/src/Parser/Ast/TagContentNode.php +++ /dev/null @@ -1,66 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class TagContentNode implements \JsonSerializable -{ - private function __construct( - public readonly TextNode|ExpressionNode|TagNode $root - ) { - } - - /** - * @param \Iterator $tokens - * @return null|self - */ - public static function fromTokens(\Iterator $tokens): ?self - { - switch (Scanner::type($tokens)) { - case TokenType::TAG_START_OPENING: - $result = TagNode::fromTokens($tokens); - break; - case TokenType::BRACKET_CURLY_OPEN: - Scanner::skipOne($tokens); - $result = ExpressionNode::fromTokens($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - Scanner::skipSpace($tokens); - break; - default: - $result = TextNode::fromTokens($tokens); - break; - } - - return $result ? new self($result) : null; - } - - public function jsonSerialize(): mixed - { - return $this->root; - } -} diff --git a/src/Parser/Ast/TagContentNodes.php b/src/Parser/Ast/TagContentNodes.php deleted file mode 100644 index b04a0355..00000000 --- a/src/Parser/Ast/TagContentNodes.php +++ /dev/null @@ -1,71 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class TagContentNodes implements \JsonSerializable -{ - /** - * @var TagContentNode[] - */ - public readonly array $items; - - private function __construct( - TagContentNode ...$items - ) { - $this->items = $items; - } - - public static function empty(): self - { - return new self(); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - $contents = []; - while (true) { - if (Scanner::type($tokens) === TokenType::TAG_START_CLOSING) { - break; - } - - if ($node = TagContentNode::fromTokens($tokens)) { - $contents[] = $node; - } - } - - return new self(...$contents); - } - - public function jsonSerialize(): mixed - { - return $this->items; - } -} diff --git a/src/Parser/Ast/TagNode.php b/src/Parser/Ast/TagNode.php deleted file mode 100644 index 317a466c..00000000 --- a/src/Parser/Ast/TagNode.php +++ /dev/null @@ -1,105 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class TagNode implements \JsonSerializable -{ - private function __construct( - public readonly string $tagName, - public readonly AttributeNodes $attributes, - public readonly TagContentNodes $children, - public readonly bool $isSelfClosing - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::TAG_START_OPENING); - - Scanner::skipOne($tokens); - Scanner::assertType($tokens, TokenType::STRING); - - $tagName = Scanner::value($tokens); - - Scanner::skipOne($tokens); - - $attributes = AttributeNodes::fromTokens($tokens); - - Scanner::skipSpaceAndComments($tokens); - - if (Scanner::type($tokens) === TokenType::TAG_SELF_CLOSE) { - Scanner::skipOne($tokens); - - return new self( - tagName: $tagName, - attributes: $attributes, - children: TagContentNodes::empty(), - isSelfClosing: true - ); - } - - Scanner::assertType($tokens, TokenType::TAG_END); - Scanner::skipOne($tokens); - Scanner::skipSpace($tokens); - - $children = TagContentNodes::fromTokens($tokens); - - Scanner::assertType($tokens, TokenType::TAG_START_CLOSING); - Scanner::skipOne($tokens); - Scanner::assertType($tokens, TokenType::STRING); - Scanner::assertValue($tokens, $tagName); - Scanner::skipOne($tokens); - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::TAG_END); - Scanner::skipOne($tokens); - - return new self( - tagName: $tagName, - attributes: $attributes, - children: $children, - isSelfClosing: false - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'TagNode', - 'payload' => [ - 'tagName' => $this->tagName, - 'attributes' => $this->attributes, - 'children' => $this->children, - 'isSelfClosing' => $this->isSelfClosing - ] - ]; - } -} diff --git a/src/Parser/Ast/TemplateLiteralNode.php b/src/Parser/Ast/TemplateLiteralNode.php deleted file mode 100644 index 83d38c11..00000000 --- a/src/Parser/Ast/TemplateLiteralNode.php +++ /dev/null @@ -1,100 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class TemplateLiteralNode implements \JsonSerializable -{ - /** - * @var (StringLiteralNode|ExpressionNode)[] - */ - public readonly array $segments; - - private function __construct( - StringLiteralNode|ExpressionNode ...$segments - ) { - $this->segments = $segments; - } - - public static function fromString(string $stringLiteralAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($stringLiteralAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - Scanner::assertType($tokens, TokenType::TEMPLATE_LITERAL_START); - Scanner::skipOne($tokens); - - /** @var array $segments */ - $segments = []; - - while (true) { - switch (Scanner::type($tokens)) { - case TokenType::STRING_QUOTED: - $segments[] = StringLiteralNode::fromTokens($tokens); - break; - case TokenType::DOLLAR: - Scanner::skipOne($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_OPEN); - Scanner::skipOne($tokens); - - $segments[] = ExpressionNode::fromTokens($tokens); - - Scanner::assertType($tokens, TokenType::BRACKET_CURLY_CLOSE); - Scanner::skipOne($tokens); - break; - case TokenType::TEMPLATE_LITERAL_END: - Scanner::skipOne($tokens); - break 2; - default: - Scanner::assertType($tokens, TokenType::STRING_QUOTED, TokenType::DOLLAR, TokenType::TEMPLATE_LITERAL_END); - break; - } - } - - return new self(...$segments); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'TemplateLiteralNode', - 'payload' => $this->segments - ]; - } -} diff --git a/src/Parser/Ast/TernaryOperationNode.php b/src/Parser/Ast/TernaryOperationNode.php deleted file mode 100644 index f5a8f35f..00000000 --- a/src/Parser/Ast/TernaryOperationNode.php +++ /dev/null @@ -1,75 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Definition\Precedence; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class TernaryOperationNode implements \JsonSerializable -{ - private function __construct( - public readonly ExpressionNode $condition, - public readonly ExpressionNode $true, - public readonly ExpressionNode $false - ) { - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(ExpressionNode $condition, \Iterator $tokens): self - { - Scanner::skipSpace($tokens); - Scanner::assertType($tokens, TokenType::QUESTIONMARK); - Scanner::skipOne($tokens); - - $true = ExpressionNode::fromTokens($tokens, Precedence::TERNARY); - - Scanner::skipSpace($tokens); - Scanner::assertType($tokens, TokenType::COLON); - Scanner::skipOne($tokens); - - $false = ExpressionNode::fromTokens($tokens, Precedence::TERNARY); - - return new self( - condition: $condition, - true: $true, - false: $false - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'TernaryOperationNode', - 'payload' => [ - 'condition' => $this->condition, - 'true' => $this->true, - 'false' => $this->false - ] - ]; - } -} diff --git a/src/Parser/Ast/TextNode.php b/src/Parser/Ast/TextNode.php deleted file mode 100644 index 3a13fc5b..00000000 --- a/src/Parser/Ast/TextNode.php +++ /dev/null @@ -1,86 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class TextNode implements \JsonSerializable -{ - private function __construct( - public readonly string $value - ) { - } - - public static function fromString(string $textAsString): ?self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($textAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return null|self - */ - public static function fromTokens(\Iterator $tokens): ?self - { - $value = ''; - while (!Scanner::isEnd($tokens)) { - switch (Scanner::type($tokens)) { - case TokenType::BRACKET_CURLY_OPEN: - case TokenType::TAG_START_OPENING: - break 2; - case TokenType::TAG_START_CLOSING: - $value = rtrim($value); - break 2; - case TokenType::SPACE: - case TokenType::END_OF_LINE: - $value .= ' '; - Scanner::skipSpace($tokens); - break; - default: - $value .= Scanner::value($tokens); - Scanner::skipOne($tokens); - break; - } - } - - return $value && !ctype_space($value) ? new self($value) : null; - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'TextNode', - 'payload' => [ - 'value' => $this->value - ] - ]; - } -} diff --git a/src/Parser/Ast/TypeReferenceNode.php b/src/Parser/Ast/TypeReferenceNode.php deleted file mode 100644 index f4db1e7c..00000000 --- a/src/Parser/Ast/TypeReferenceNode.php +++ /dev/null @@ -1,96 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; - -final class TypeReferenceNode implements \JsonSerializable -{ - private function __construct( - public readonly string $name, - public readonly bool $isArray, - public readonly bool $isOptional - ) { - } - - public static function fromString(string $typeReferenceAsString): self - { - return self::fromTokens( - Tokenizer::fromSource( - Source::fromString($typeReferenceAsString) - )->getIterator() - ); - } - - /** - * @param \Iterator $tokens - * @return self - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpaceAndComments($tokens); - - if (Scanner::type($tokens) === TokenType::QUESTIONMARK) { - $isOptional = true; - Scanner::skipOne($tokens); - } else { - $isOptional = false; - } - - Scanner::assertType($tokens, TokenType::STRING); - - $name = Scanner::value($tokens); - - Scanner::skipOne($tokens); - - $isArray = !Scanner::isEnd($tokens) && Scanner::type($tokens) === TokenType::BRACKET_SQUARE_OPEN; - if ($isArray) { - Scanner::skipOne($tokens); - Scanner::skipSpace($tokens); - Scanner::assertType($tokens, TokenType::BRACKET_SQUARE_CLOSE); - Scanner::skipOne($tokens); - } - - return new self( - name: $name, - isArray: $isArray, - isOptional: $isOptional - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'TypeReferenceNode', - 'payload' => [ - 'name' => $this->name, - 'isArray' => $this->isArray, - 'isOptional' => $this->isOptional - ] - ]; - } -} diff --git a/src/Parser/Ast/UnaryOperationNode.php b/src/Parser/Ast/UnaryOperationNode.php deleted file mode 100644 index df8112ad..00000000 --- a/src/Parser/Ast/UnaryOperationNode.php +++ /dev/null @@ -1,67 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Parser\Ast; - -use PackageFactory\ComponentEngine\Definition\Precedence; -use PackageFactory\ComponentEngine\Definition\UnaryOperator; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Scanner; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; - -final class UnaryOperationNode implements \JsonSerializable -{ - private function __construct( - public readonly UnaryOperator $operator, - public readonly ExpressionNode $argument - ) { - } - - /** - * @param \Iterator $tokens - */ - public static function fromTokens(\Iterator $tokens): self - { - Scanner::skipSpace($tokens); - - $operator = UnaryOperator::fromTokenType(Scanner::type($tokens)); - - Scanner::skipOne($tokens); - - $argument = ExpressionNode::fromTokens($tokens, Precedence::UNARY); - - return new self( - operator: $operator, - argument: $argument - ); - } - - public function jsonSerialize(): mixed - { - return [ - 'type' => 'UnaryOperationNode', - 'payload' => [ - 'operator' => $this->operator, - 'argument' => $this->argument - ] - ]; - } -} diff --git a/src/Parser/Source/Path.php b/src/Parser/Source/Path.php index 4880e0e5..a3217a9d 100644 --- a/src/Parser/Source/Path.php +++ b/src/Parser/Source/Path.php @@ -113,6 +113,11 @@ public function resolveRelationTo(Path $other): self } } + public function equals(Path $other): bool + { + return $this->value === $other->value; + } + public function __toString(): string { return $this->value; diff --git a/src/Parser/Source/Position.php b/src/Parser/Source/Position.php index aa6561c0..fd15a833 100644 --- a/src/Parser/Source/Position.php +++ b/src/Parser/Source/Position.php @@ -22,54 +22,23 @@ namespace PackageFactory\ComponentEngine\Parser\Source; -final class Position implements \JsonSerializable +final class Position { - private function __construct( - public readonly int $index, - public readonly int $rowIndex, - public readonly int $columnIndex - ) { - } - - public static function create( - int $index, - int $rowIndex, - int $columnIndex - ): Position { - return new Position( - index: $index, - rowIndex: $rowIndex, - columnIndex: $columnIndex - ); - } + private static ?self $zero; - public function equals(Position $other): bool - { - return $this->index === $other->index; - } - - public function gt(Position $other): bool - { - return $this->index > $other->index; - } - - public function gte(Position $other): bool - { - return $this->gt($other) || $this->equals($other); - } - - public function lt(Position $other): bool - { - return $this->index < $other->index; + public function __construct( + public readonly int $lineNumber, + public readonly int $columnNumber + ) { } - public function lte(Position $other): bool + public static function zero(): self { - return $this->lt($other) || $this->equals($other); + return self::$zero ??= new self(0, 0); } - public function jsonSerialize(): mixed + public function toDebugString(): string { - return $this->index; + return sprintf('line %s, column %s', $this->lineNumber, $this->columnNumber); } } diff --git a/src/Parser/Source/Boundaries.php b/src/Parser/Source/Range.php similarity index 66% rename from src/Parser/Source/Boundaries.php rename to src/Parser/Source/Range.php index 5a5919c8..7704fa12 100644 --- a/src/Parser/Source/Boundaries.php +++ b/src/Parser/Source/Range.php @@ -22,35 +22,23 @@ namespace PackageFactory\ComponentEngine\Parser\Source; -final class Boundaries implements \JsonSerializable +final class Range { + private static ?self $zero; + private function __construct( public readonly Position $start, public readonly Position $end, ) { } - public static function fromPositions(Position $start, Position $end): self + public static function from(Position $start, Position $end): self { return new self($start, $end); } - public function expandedTo(Boundaries $other): self - { - return new self($this->start, $other->end); - } - - public function equals(Boundaries $other): bool - { - return ($this->start->equals($other->start) - && $this->end->equals($other->end)); - } - - public function jsonSerialize(): mixed + public static function zero(): self { - return [ - 'start' => $this->start, - 'end' => $this->end, - ]; + return self::$zero ??= new self(Position::zero(), Position::zero()); } } diff --git a/src/Parser/Source/Source.php b/src/Parser/Source/Source.php index 5b46cb33..ba143ca7 100644 --- a/src/Parser/Source/Source.php +++ b/src/Parser/Source/Source.php @@ -57,8 +57,8 @@ public function equals(Source $other): bool */ public function getIterator(): \Iterator { - $rowIndex = 0; - $columnIndex = 0; + $lineNumber = 0; + $columnNumber = 0; $length = strlen($this->contents); for ($index = 0; $index < $length; $index++) { @@ -66,16 +66,16 @@ public function getIterator(): \Iterator yield Fragment::create( $character, - Position::create($index, $rowIndex, $columnIndex), - Position::create($index, $rowIndex, $columnIndex), + new Position($lineNumber, $columnNumber), + new Position($lineNumber, $columnNumber), $this ); if ($character === "\n") { - $rowIndex++; - $columnIndex = 0; + $lineNumber++; + $columnNumber = 0; } else { - $columnIndex++; + $columnNumber++; } } } diff --git a/src/Parser/Tokenizer/LookAhead.php b/src/Parser/Tokenizer/LookAhead.php index 6dfb7252..4f7c3251 100644 --- a/src/Parser/Tokenizer/LookAhead.php +++ b/src/Parser/Tokenizer/LookAhead.php @@ -36,7 +36,7 @@ final class LookAhead implements \IteratorAggregate * @param \Iterator $tokens */ private function __construct( - public readonly \Iterator $tokens + public \Iterator $tokens ) { } diff --git a/src/Parser/Tokenizer/Scanner.php b/src/Parser/Tokenizer/Scanner.php index 5aa89761..3e11f3e3 100644 --- a/src/Parser/Tokenizer/Scanner.php +++ b/src/Parser/Tokenizer/Scanner.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Parser\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Source\Path; final class Scanner { @@ -57,9 +57,9 @@ public static function assertType(\Iterator $tokens, TokenType ...$types): void "@TODO: Unexpected token: " . $actualType->value . " at " - . ($tokens->current()->boundaries->start->rowIndex + 1) + . ($tokens->current()->boundaries->start->lineNumber + 1) . ":" - . ($tokens->current()->boundaries->start->columnIndex + 1) + . ($tokens->current()->boundaries->start->columnNumber + 1) ); } @@ -86,7 +86,7 @@ public static function assertValue(\Iterator $tokens, string ...$values): void * @param \Iterator $tokens * @return \Iterator */ - public static function skipOne(\Iterator $tokens): \Iterator + public static function skipOne(\Iterator &$tokens): \Iterator { $tokens->next(); return $tokens; @@ -149,12 +149,12 @@ public static function type(\Iterator $tokens): TokenType /** * @param \Iterator $tokens - * @return Source + * @return Path */ - public static function source(\Iterator $tokens): Source + public static function sourcePath(\Iterator $tokens): Path { self::assertValid($tokens); - return $tokens->current()->source; + return $tokens->current()->sourcePath; } /** diff --git a/src/Parser/Tokenizer/Token.php b/src/Parser/Tokenizer/Token.php index 7e4cbf45..66b5eb08 100644 --- a/src/Parser/Tokenizer/Token.php +++ b/src/Parser/Tokenizer/Token.php @@ -22,17 +22,17 @@ namespace PackageFactory\ComponentEngine\Parser\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Source\Boundaries; -use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Source\Range; use PackageFactory\ComponentEngine\Parser\Source\Fragment; +use PackageFactory\ComponentEngine\Parser\Source\Path; final class Token { - private function __construct( + public function __construct( public readonly TokenType $type, public readonly string $value, - public readonly Boundaries $boundaries, - public readonly Source $source + public readonly Range $boundaries, + public readonly Path $sourcePath ) { } @@ -43,8 +43,8 @@ public static function fromFragment( return new Token( $type, $fragment->value, - Boundaries::fromPositions($fragment->start, $fragment->end), - $fragment->source + Range::from($fragment->start, $fragment->end), + $fragment->source->path ); } @@ -56,22 +56,18 @@ public static function emptyFromDelimitingFragments( return new Token( $type, '', - Boundaries::fromPositions($startFragment->start, $endFragment->end), - $startFragment->source + Range::from($startFragment->start, $endFragment->end), + $startFragment->source->path ); } - public function equals(Token $other): bool + public function __toString(): string { - return ($this->type === $other->type - && $this->value === $other->value - && $this->boundaries->equals($other->boundaries) - && $this->source->equals($other->source) - ); + return $this->value; } - public function __toString(): string + public function toDebugString(): string { - return $this->value; + return sprintf('%s ("%s")', $this->type->value, $this->value); } } diff --git a/src/Parser/Tokenizer/TokenType.php b/src/Parser/Tokenizer/TokenType.php index 12d76b29..0c2b8fae 100644 --- a/src/Parser/Tokenizer/TokenType.php +++ b/src/Parser/Tokenizer/TokenType.php @@ -86,6 +86,7 @@ enum TokenType: string case EQUALS = 'EQUALS'; case SLASH_FORWARD = 'SLASH_FORWARD'; case DOLLAR = 'DOLLAR'; + case PIPE = 'PIPE'; case OPTCHAIN = 'OPTCHAIN'; case NULLISH_COALESCE = 'NULLISH_COALESCE'; @@ -162,4 +163,64 @@ public function matchesString(string $string): bool default => false }; } + + public function toDebugString(): string + { + return $this->value . match ($this) { + self::COMMENT => ' (e.g. "# ...")', + self::KEYWORD_FROM => ' ("from")', + self::KEYWORD_IMPORT => ' ("import")', + self::KEYWORD_EXPORT => ' ("export")', + self::KEYWORD_ENUM => ' ("enum")', + self::KEYWORD_STRUCT => ' ("struct")', + self::KEYWORD_COMPONENT => ' ("component")', + self::KEYWORD_MATCH => ' ("match")', + self::KEYWORD_DEFAULT => ' ("default")', + self::KEYWORD_RETURN => ' ("return")', + self::KEYWORD_TRUE => ' ("true")', + self::KEYWORD_FALSE => ' ("false")', + self::KEYWORD_NULL => ' ("null")', + self::CONSTANT => '', + self::STRING => '', + self::STRING_QUOTED => '', + self::NUMBER_BINARY => ' (e.g. "0b1001")', + self::NUMBER_OCTAL => ' (e.g. "0o644")', + self::NUMBER_DECIMAL => ' (e.g. "42")', + self::NUMBER_HEXADECIMAL => ' (e.g. "0xABC")', + self::TEMPLATE_LITERAL_START => ' ("`")', + self::TEMPLATE_LITERAL_END => ' ("`")', + self::OPERATOR_BOOLEAN_AND => ' ("&&")', + self::OPERATOR_BOOLEAN_OR => ' ("||")', + self::OPERATOR_BOOLEAN_NOT => ' ("!")', + self::COMPARATOR_EQUAL => ' ("===")', + self::COMPARATOR_NOT_EQUAL => ' ("!==")', + self::COMPARATOR_GREATER_THAN => ' (">")', + self::COMPARATOR_GREATER_THAN_OR_EQUAL => ' (">=")', + self::COMPARATOR_LESS_THAN => ' ("<")', + self::COMPARATOR_LESS_THAN_OR_EQUAL => ' ("<=")', + self::ARROW_SINGLE => ' ("->")', + self::BRACKET_CURLY_OPEN => ' ("{")', + self::BRACKET_CURLY_CLOSE => ' ("}")', + self::BRACKET_ROUND_OPEN => ' ("(")', + self::BRACKET_ROUND_CLOSE => ' (")")', + self::BRACKET_SQUARE_OPEN => ' ("[")', + self::BRACKET_SQUARE_CLOSE => ' ("]")', + self::TAG_START_OPENING => ' ("<")', + self::TAG_START_CLOSING => ' (" ' ("/>")', + self::TAG_END => ' (">")', + self::PERIOD => ' (".")', + self::COLON => ' (":")', + self::QUESTIONMARK => ' ("?")', + self::COMMA => ' (",")', + self::EQUALS => ' ("=")', + self::SLASH_FORWARD => ' ("/")', + self::DOLLAR => ' ("$")', + self::PIPE => ' ("|")', + self::OPTCHAIN => ' ("?.")', + self::NULLISH_COALESCE => ' ("??")', + self::SPACE => '', + self::END_OF_LINE => '' + }; + } } diff --git a/src/Parser/Ast/ImportNodes.php b/src/Parser/Tokenizer/TokenTypes.php similarity index 50% rename from src/Parser/Ast/ImportNodes.php rename to src/Parser/Tokenizer/TokenTypes.php index f898ff88..135e1d77 100644 --- a/src/Parser/Ast/ImportNodes.php +++ b/src/Parser/Tokenizer/TokenTypes.php @@ -20,47 +20,47 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Parser\Ast; +namespace PackageFactory\ComponentEngine\Parser\Tokenizer; -final class ImportNodes implements \JsonSerializable +final class TokenTypes { /** - * @var array + * @var TokenType[] */ - public readonly array $items; + private readonly array $items; - /** - * @param array $items - */ - private function __construct( - array $items - ) { - $this->items = $items; - } - - public static function empty(): self + private function __construct(TokenType ...$items) { - return new self([]); + assert(count($items) > 0); + + $this->items = $items; } - public function withAddedImport(ImportNode $import): self + public static function from(TokenType ...$items): self { - $name = $import->name->value; - - if (array_key_exists($name, $this->items)) { - throw new \Exception('@TODO: Duplicate Import ' . $name); - } + $items = array_unique($items, SORT_REGULAR); + $items = array_values($items); - return new self([...$this->items, ...[$name => $import]]); + return new self(...$items); } - public function get(string $name): ?ImportNode + public function contains(TokenType $needle): bool { - return $this->items[$name] ?? null; + return in_array($needle, $this->items); } - public function jsonSerialize(): mixed + public function toDebugString(): string { - return array_values($this->items); + if (count($this->items) === 1) { + return $this->items[0]->toDebugString(); + } + + $leadingItems = array_slice($this->items, 0, -1); + $trailingItem = array_slice($this->items, -1)[0]; + + return join(', ', array_map( + static fn (TokenType $tokenType) => $tokenType->toDebugString(), + $leadingItems + )) . ' or ' . $trailingItem->toDebugString(); } } diff --git a/src/Parser/Tokenizer/Tokenizer.php b/src/Parser/Tokenizer/Tokenizer.php index c08eaa15..c670e207 100644 --- a/src/Parser/Tokenizer/Tokenizer.php +++ b/src/Parser/Tokenizer/Tokenizer.php @@ -56,6 +56,10 @@ public function getIterator(): \Iterator */ private static function block(\Iterator $fragments): \Iterator { + if (!$fragments->valid()) { + return; + } + $bracket = TokenType::tryBracketOpenFromFragment($fragments->current()); $buffer = Buffer::empty(); @@ -277,6 +281,7 @@ public static function symbol(\Iterator $fragments, ?Buffer $buffer = null): \It '=' => $buffer->flush(TokenType::EQUALS), '?' => $buffer->flush(TokenType::QUESTIONMARK), '$' => $buffer->flush(TokenType::DOLLAR), + '|' => $buffer->flush(TokenType::PIPE), default => self::flushRemainder($buffer) }; } diff --git a/src/Target/Php/Transpiler/Access/AccessTranspiler.php b/src/Target/Php/Transpiler/Access/AccessTranspiler.php index f29d7929..2adbafa7 100644 --- a/src/Target/Php/Transpiler/Access/AccessTranspiler.php +++ b/src/Target/Php/Transpiler/Access/AccessTranspiler.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Access; -use PackageFactory\ComponentEngine\Definition\AccessType; -use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessType; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -43,22 +43,15 @@ public function transpile(AccessNode $accessNode): string $expressionTypeResolver = new ExpressionTypeResolver( scope: $this->scope ); - $typeOfRoot = $expressionTypeResolver->resolveTypeOf($accessNode->root); - $result = $expressionTranspiler->transpile($accessNode->root); - - $isFirstElement = true; - foreach ($accessNode->chain->items as $accessChainNode) { - if ($typeOfRoot instanceof EnumStaticType && $isFirstElement) { - $result .= '::'; - } elseif ($accessChainNode->accessType === AccessType::OPTIONAL) { - $result .= '?->'; - } else { - $result .= '->'; - } - $result .= $accessChainNode->accessor->value; - $isFirstElement = false; + $parentType = $expressionTypeResolver->resolveTypeOf($accessNode->parent); + $parent = $expressionTranspiler->transpile($accessNode->parent); + + if ($parentType instanceof EnumStaticType) { + return $parent . '::' . $accessNode->key->value->value; + } elseif ($accessNode->type === AccessType::OPTIONAL) { + return $parent . '?->' . $accessNode->key->value->value; + } else { + return $parent . '->' . $accessNode->key->value->value; } - - return $result; } } diff --git a/src/Target/Php/Transpiler/Attribute/AttributeTranspiler.php b/src/Target/Php/Transpiler/Attribute/AttributeTranspiler.php index 45ccc0af..6d990be1 100644 --- a/src/Target/Php/Transpiler/Attribute/AttributeTranspiler.php +++ b/src/Target/Php/Transpiler/Attribute/AttributeTranspiler.php @@ -22,9 +22,9 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Attribute; -use PackageFactory\ComponentEngine\Parser\Ast\AttributeNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\StringLiteral\StringLiteralTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -37,18 +37,22 @@ public function __construct(private readonly ScopeInterface $scope) public function transpile(AttributeNode $attributeNode): string { - return sprintf( - '%s="%s"', - $attributeNode->name, - match ($attributeNode->value::class) { - ExpressionNode::class => sprintf( - '\' . %s . \'', - (new ExpressionTranspiler( - scope: $this->scope - ))->transpile($attributeNode->value) - ), - StringLiteralNode::class => (new StringLiteralTranspiler())->transpile($attributeNode->value) - } - ); + if ($attributeNode->value) { + return sprintf( + '%s="%s"', + $attributeNode->name->value->value, + match ($attributeNode->value::class) { + ExpressionNode::class => sprintf( + '\' . %s . \'', + (new ExpressionTranspiler( + scope: $this->scope + ))->transpile($attributeNode->value) + ), + StringLiteralNode::class => (new StringLiteralTranspiler())->transpile($attributeNode->value) + } + ); + } else { + return $attributeNode->name->value->value; + } } } diff --git a/src/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspiler.php b/src/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspiler.php index abc7ff72..d39d3a08 100644 --- a/src/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspiler.php +++ b/src/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspiler.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\BinaryOperation; -use PackageFactory\ComponentEngine\Definition\BinaryOperator; -use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperator; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -54,9 +54,9 @@ public function transpile(BinaryOperationNode $binaryOperationNode): string shouldAddQuotesIfNecessary: true ); - $left = $expressionTranspiler->transpile($binaryOperationNode->left); + $left = $expressionTranspiler->transpile($binaryOperationNode->leftOperand); $operator = $this->transpileBinaryOperator($binaryOperationNode->operator); - $right = $expressionTranspiler->transpile($binaryOperationNode->right); + $right = $expressionTranspiler->transpile($binaryOperationNode->rightOperand); return sprintf('(%s %s %s)', $left, $operator, $right); } diff --git a/src/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspiler.php b/src/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspiler.php index f951f048..6cd2a826 100644 --- a/src/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspiler.php +++ b/src/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\BooleanLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; final class BooleanLiteralTranspiler { diff --git a/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationStrategyInterface.php b/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationStrategyInterface.php index aca55f4d..2d0268b9 100644 --- a/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationStrategyInterface.php +++ b/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationStrategyInterface.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\ComponentDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\TargetSpecific\ClassName; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceStrategyInterface; diff --git a/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspiler.php b/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspiler.php index 7e046a6a..059d8659 100644 --- a/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspiler.php +++ b/src/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspiler.php @@ -22,9 +22,8 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\ComponentDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Ast\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceTranspiler; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; @@ -61,7 +60,9 @@ public function transpile(ComponentDeclarationNode $componentDeclarationNode): s foreach ($this->module->imports->items as $importNode) { // @TODO: Generate Namespaces + Name via TypeReferenceStrategyInterface Dynamically - $lines[] = 'use Vendor\\Project\\Component\\' . $importNode->name->value . ';'; + foreach ($importNode->names->items as $name) { + $lines[] = 'use Vendor\\Project\\Component\\' . $name->value->value . ';'; + } } $lines[] = ''; @@ -70,7 +71,7 @@ public function transpile(ComponentDeclarationNode $componentDeclarationNode): s : 'final class ' . $className->getShortClassName(); $lines[] = '{'; - if (!$componentDeclarationNode->propertyDeclarations->isEmpty()) { + if (!$componentDeclarationNode->props->isEmpty()) { $lines[] = ' public function __construct('; $lines[] = $this->writeConstructorPropertyDeclarations($componentDeclarationNode); $lines[] = ' ) {'; @@ -94,11 +95,11 @@ public function writeConstructorPropertyDeclarations(ComponentDeclarationNode $c scope: $this->scope, strategy: $this->strategy->getTypeReferenceStrategyFor($componentDeclarationNode) ); - $propertyDeclarations = $componentDeclarationNode->propertyDeclarations; + $propertyDeclarations = $componentDeclarationNode->props; $lines = []; foreach ($propertyDeclarations->items as $propertyDeclaration) { - $lines[] = ' public readonly ' . $typeReferenceTranspiler->transpile($propertyDeclaration->type) . ' $' . $propertyDeclaration->name . ','; + $lines[] = ' public readonly ' . $typeReferenceTranspiler->transpile($propertyDeclaration->type) . ' $' . $propertyDeclaration->name->value->value . ','; } if ($length = count($lines)) { @@ -122,8 +123,8 @@ public function writeReturnExpression(ComponentDeclarationNode $componentDeclara shouldAddQuotesIfNecessary: true ); - $returnExpression = $componentDeclarationNode->returnExpression; - $returnTypeIsString = StringType::get()->is( + $returnExpression = $componentDeclarationNode->return; + $returnTypeIsString = StringType::singleton()->is( $expressionTypeResolver->resolveTypeOf($returnExpression) ); $transpiledReturnExpression = $expressionTranspiler->transpile($returnExpression); diff --git a/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationStrategyInterface.php b/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationStrategyInterface.php index 07db6f28..ebad3936 100644 --- a/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationStrategyInterface.php +++ b/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationStrategyInterface.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\EnumDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\TargetSpecific\ClassName; interface EnumDeclarationStrategyInterface diff --git a/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspiler.php b/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspiler.php index a73b9361..94129a3b 100644 --- a/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspiler.php +++ b/src/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspiler.php @@ -22,10 +22,10 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\EnumDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumMemberDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\IntegerLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; final class EnumDeclarationTranspiler { @@ -49,8 +49,8 @@ public function transpile(EnumDeclarationNode $enumDeclarationNode): string $lines[] = 'enum ' . $className->getShortClassName() . ' : ' . $this->transpileBackingType($enumDeclarationNode); $lines[] = '{'; - foreach ($enumDeclarationNode->memberDeclarations->items as $memberDeclarationNode) { - $lines[] = ' case ' . $memberDeclarationNode->name . ' = ' . $this->transpileMemberValue($memberDeclarationNode) . ';'; + foreach ($enumDeclarationNode->members->items as $memberDeclarationNode) { + $lines[] = ' case ' . $memberDeclarationNode->name->value->value . ' = ' . $this->transpileMemberValue($memberDeclarationNode) . ';'; } $lines[] = '}'; @@ -61,8 +61,8 @@ public function transpile(EnumDeclarationNode $enumDeclarationNode): string private function transpileBackingType(EnumDeclarationNode $enumDeclarationNode): string { - foreach ($enumDeclarationNode->memberDeclarations->items as $memberDeclarationNode) { - if ($memberDeclarationNode->value instanceof IntegerLiteralNode) { + foreach ($enumDeclarationNode->members->items as $memberDeclarationNode) { + if ($memberDeclarationNode->value?->value instanceof IntegerLiteralNode) { return 'int'; } else { return 'string'; @@ -74,12 +74,12 @@ private function transpileBackingType(EnumDeclarationNode $enumDeclarationNode): private function transpileMemberValue(EnumMemberDeclarationNode $enumMemberDeclarationNode): string { - if ($enumMemberDeclarationNode->value instanceof IntegerLiteralNode) { - return $enumMemberDeclarationNode->value->value; - } else if ($enumMemberDeclarationNode->value instanceof StringLiteralNode) { - return '\'' . $enumMemberDeclarationNode->value->value . '\''; + if ($enumMemberDeclarationNode->value?->value instanceof IntegerLiteralNode) { + return $enumMemberDeclarationNode->value->value->value; + } else if ($enumMemberDeclarationNode->value?->value instanceof StringLiteralNode) { + return '\'' . $enumMemberDeclarationNode->value->value->value . '\''; } else { - return '\'' . $enumMemberDeclarationNode->name . '\''; + return '\'' . $enumMemberDeclarationNode->name->value->value . '\''; } } } diff --git a/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php b/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php index 450ff657..50c27f9a 100644 --- a/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php +++ b/src/Target/Php/Transpiler/Expression/ExpressionTranspiler.php @@ -22,23 +22,23 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression; -use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; -use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; -use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; -use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\IntegerLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\TagNode; -use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; -use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Access\AccessTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\BinaryOperation\BinaryOperationTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\BooleanLiteral\BooleanLiteralTranspiler; -use PackageFactory\ComponentEngine\Target\Php\Transpiler\Identifier\IdentifierTranspiler; +use PackageFactory\ComponentEngine\Target\Php\Transpiler\ValueReference\ValueReferenceTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Match\MatchTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\NullLiteral\NullLiteralTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\IntegerLiteral\IntegerLiteralTranspiler; @@ -63,7 +63,7 @@ public function transpile(ExpressionNode $expressionNode): string AccessNode::class => new AccessTranspiler( scope: $this->scope ), - IdentifierNode::class => new IdentifierTranspiler( + ValueReferenceNode::class => new ValueReferenceTranspiler( scope: $this->scope ), TernaryOperationNode::class => new TernaryOperationTranspiler( diff --git a/src/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspiler.php b/src/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspiler.php index 14a45676..46c66f3d 100644 --- a/src/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspiler.php +++ b/src/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspiler.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\IntegerLiteral; -use PackageFactory\ComponentEngine\Definition\IntegerFormat; -use PackageFactory\ComponentEngine\Parser\Ast\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerFormat; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; final class IntegerLiteralTranspiler { diff --git a/src/Target/Php/Transpiler/Match/MatchTranspiler.php b/src/Target/Php/Transpiler/Match/MatchTranspiler.php index 55a90b0c..e267ecaf 100644 --- a/src/Target/Php/Transpiler/Match/MatchTranspiler.php +++ b/src/Target/Php/Transpiler/Match/MatchTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Match; -use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; diff --git a/src/Target/Php/Transpiler/Module/ModuleStrategyInterface.php b/src/Target/Php/Transpiler/Module/ModuleStrategyInterface.php index cd541873..771369aa 100644 --- a/src/Target/Php/Transpiler/Module/ModuleStrategyInterface.php +++ b/src/Target/Php/Transpiler/Module/ModuleStrategyInterface.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Module; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\ComponentDeclaration\ComponentDeclarationStrategyInterface; use PackageFactory\ComponentEngine\Target\Php\Transpiler\EnumDeclaration\EnumDeclarationStrategyInterface; use PackageFactory\ComponentEngine\Target\Php\Transpiler\StructDeclaration\StructDeclarationStrategyInterface; @@ -32,4 +32,4 @@ interface ModuleStrategyInterface public function getComponentDeclarationStrategyFor(ModuleNode $moduleNode): ComponentDeclarationStrategyInterface; public function getEnumDeclarationStrategyFor(ModuleNode $moduleNode): EnumDeclarationStrategyInterface; public function getStructDeclarationStrategyFor(ModuleNode $moduleNode): StructDeclarationStrategyInterface; -} \ No newline at end of file +} diff --git a/src/Target/Php/Transpiler/Module/ModuleTranspiler.php b/src/Target/Php/Transpiler/Module/ModuleTranspiler.php index f29ed678..5207b0b2 100644 --- a/src/Target/Php/Transpiler/Module/ModuleTranspiler.php +++ b/src/Target/Php/Transpiler/Module/ModuleTranspiler.php @@ -22,11 +22,11 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Module; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; use PackageFactory\ComponentEngine\Module\LoaderInterface; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\ComponentDeclaration\ComponentDeclarationTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\EnumDeclaration\EnumDeclarationTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\StructDeclaration\StructDeclarationTranspiler; @@ -44,31 +44,29 @@ public function __construct( public function transpile(ModuleNode $moduleNode): string { - foreach ($moduleNode->exports->items as $exportNode) { - return match ($exportNode->declaration::class) { - ComponentDeclarationNode::class => (new ComponentDeclarationTranspiler( - scope: new ModuleScope( - loader: $this->loader, - moduleNode: $moduleNode, - parentScope: $this->globalScope - ), - module: $moduleNode, - strategy: $this->strategy->getComponentDeclarationStrategyFor($moduleNode) - ))->transpile($exportNode->declaration), - EnumDeclarationNode::class => (new EnumDeclarationTranspiler( - strategy: $this->strategy->getEnumDeclarationStrategyFor($moduleNode) - ))->transpile($exportNode->declaration), - StructDeclarationNode::class => (new StructDeclarationTranspiler( - scope: new ModuleScope( - loader: $this->loader, - moduleNode: $moduleNode, - parentScope: $this->globalScope - ), - strategy: $this->strategy->getStructDeclarationStrategyFor($moduleNode) - ))->transpile($exportNode->declaration) - }; - } + $declarationNode = $moduleNode->export->declaration; - return ''; + return match ($declarationNode::class) { + ComponentDeclarationNode::class => (new ComponentDeclarationTranspiler( + scope: new ModuleScope( + loader: $this->loader, + moduleNode: $moduleNode, + parentScope: $this->globalScope + ), + module: $moduleNode, + strategy: $this->strategy->getComponentDeclarationStrategyFor($moduleNode) + ))->transpile($declarationNode), + EnumDeclarationNode::class => (new EnumDeclarationTranspiler( + strategy: $this->strategy->getEnumDeclarationStrategyFor($moduleNode) + ))->transpile($declarationNode), + StructDeclarationNode::class => (new StructDeclarationTranspiler( + scope: new ModuleScope( + loader: $this->loader, + moduleNode: $moduleNode, + parentScope: $this->globalScope + ), + strategy: $this->strategy->getStructDeclarationStrategyFor($moduleNode) + ))->transpile($declarationNode) + }; } } diff --git a/src/Target/Php/Transpiler/NullLiteral/NullLiteralTranspiler.php b/src/Target/Php/Transpiler/NullLiteral/NullLiteralTranspiler.php index 453a5a70..3aaff2fd 100644 --- a/src/Target/Php/Transpiler/NullLiteral/NullLiteralTranspiler.php +++ b/src/Target/Php/Transpiler/NullLiteral/NullLiteralTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\NullLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; final class NullLiteralTranspiler { diff --git a/src/Target/Php/Transpiler/StringLiteral/StringLiteralTranspiler.php b/src/Target/Php/Transpiler/StringLiteral/StringLiteralTranspiler.php index c318c371..8476b729 100644 --- a/src/Target/Php/Transpiler/StringLiteral/StringLiteralTranspiler.php +++ b/src/Target/Php/Transpiler/StringLiteral/StringLiteralTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\StringLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; final class StringLiteralTranspiler { diff --git a/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationStrategyInterface.php b/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationStrategyInterface.php index 102f4a45..79a8e3f2 100644 --- a/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationStrategyInterface.php +++ b/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationStrategyInterface.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\StructDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\TargetSpecific\ClassName; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceStrategyInterface; diff --git a/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspiler.php b/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspiler.php index e9eccaea..41e9b7ac 100644 --- a/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspiler.php +++ b/src/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\StructDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -58,7 +58,7 @@ public function transpile(StructDeclarationNode $structDeclarationNode): string : 'final class ' . $className->getShortClassName(); $lines[] = '{'; - if (!$structDeclarationNode->propertyDeclarations->isEmpty()) { + if (!$structDeclarationNode->properties->isEmpty()) { $lines[] = ' public function __construct('; $lines[] = $this->writeConstructorPropertyDeclarations($structDeclarationNode); $lines[] = ' ) {'; @@ -77,11 +77,11 @@ public function writeConstructorPropertyDeclarations(StructDeclarationNode $stru scope: $this->scope, strategy: $this->strategy->getTypeReferenceStrategyFor($structDeclarationNode) ); - $propertyDeclarations = $structDeclarationNode->propertyDeclarations; + $propertyDeclarations = $structDeclarationNode->properties; $lines = []; foreach ($propertyDeclarations->items as $propertyDeclaration) { - $lines[] = ' public readonly ' . $typeReferenceTranspiler->transpile($propertyDeclaration->type) . ' $' . $propertyDeclaration->name . ','; + $lines[] = ' public readonly ' . $typeReferenceTranspiler->transpile($propertyDeclaration->type) . ' $' . $propertyDeclaration->name->value->value . ','; } if ($length = count($lines)) { diff --git a/src/Target/Php/Transpiler/Tag/TagTranspiler.php b/src/Target/Php/Transpiler/Tag/TagTranspiler.php index dc98da5d..8d16b1e7 100644 --- a/src/Target/Php/Transpiler/Tag/TagTranspiler.php +++ b/src/Target/Php/Transpiler/Tag/TagTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Tag; -use PackageFactory\ComponentEngine\Parser\Ast\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Attribute\AttributeTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TagContent\TagContentTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -37,7 +37,7 @@ public function __construct( public function transpile(TagNode $tagNode): string { - $result = sprintf('<%s', $tagNode->tagName); + $result = sprintf('<%s', $tagNode->name->value->value); $attributeTranspiler = null; foreach ($tagNode->attributes->items as $attribute) { @@ -60,7 +60,7 @@ public function transpile(TagNode $tagNode): string $result .= $tagContentTranspiler->transpile($child); } - $result .= sprintf('', $tagNode->tagName); + $result .= sprintf('', $tagNode->name->value->value); } return $this->shouldAddQuotes diff --git a/src/Target/Php/Transpiler/TagContent/TagContentTranspiler.php b/src/Target/Php/Transpiler/TagContent/TagContentTranspiler.php index 199770de..495c1fe9 100644 --- a/src/Target/Php/Transpiler/TagContent/TagContentTranspiler.php +++ b/src/Target/Php/Transpiler/TagContent/TagContentTranspiler.php @@ -22,10 +22,9 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\TagContent; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\TagContentNode; -use PackageFactory\ComponentEngine\Parser\Ast\TagNode; -use PackageFactory\ComponentEngine\Parser\Ast\TextNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Tag\TagTranspiler; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Text\TextTranspiler; @@ -63,14 +62,14 @@ private function transpileExpression(ExpressionNode $expressionNode): string return $result; } - public function transpile(TagContentNode $tagContentNode): string + public function transpile(TextNode|ExpressionNode|TagNode $tagContentNode): string { - return match ($tagContentNode->root::class) { - TextNode::class => (new TextTranspiler())->transpile($tagContentNode->root), - ExpressionNode::class => $this->transpileExpression($tagContentNode->root), + return match ($tagContentNode::class) { + TextNode::class => (new TextTranspiler())->transpile($tagContentNode), + ExpressionNode::class => $this->transpileExpression($tagContentNode), TagNode::class => (new TagTranspiler( scope: $this->scope - ))->transpile($tagContentNode->root) + ))->transpile($tagContentNode) }; } } diff --git a/src/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspiler.php b/src/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspiler.php index 567905fc..74c099de 100644 --- a/src/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspiler.php +++ b/src/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspiler.php @@ -22,37 +22,83 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\TemplateLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralExpressionSegmentNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralStringSegmentNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; -use PackageFactory\ComponentEngine\Target\Php\Transpiler\StringLiteral\StringLiteralTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; final class TemplateLiteralTranspiler { + private ?ExpressionTranspiler $expressionTranspiler = null; + public function __construct(private readonly ScopeInterface $scope) { } public function transpile(TemplateLiteralNode $templateLiteralNode): string { - $stringLiteralTranspiler = new StringLiteralTranspiler( - shouldAddQuotes: true - ); - $expressionTranspiler = new ExpressionTranspiler( - scope: $this->scope, - shouldAddQuotesIfNecessary: true - ); $segments = []; - foreach ($templateLiteralNode->segments as $segmentNode) { + foreach ($templateLiteralNode->segments->items as $segmentNode) { $segments[] = match ($segmentNode::class) { - StringLiteralNode::class => $stringLiteralTranspiler->transpile($segmentNode), - ExpressionNode::class => $expressionTranspiler->transpile($segmentNode) + TemplateLiteralStringSegmentNode::class => $this->transpileStringSegment($segmentNode), + TemplateLiteralExpressionSegmentNode::class => $this->transpileExpressionSegment($segmentNode) }; } return join(' . ', $segments); } + + private function transpileStringSegment(TemplateLiteralStringSegmentNode $segmentNode): string + { + $result = $segmentNode->value; + $shouldAddTrailingQuote = true; + $shouldAddLeadingQuote = true; + + if (strpos($result, "\n") !== false) { + $lines = explode("\n", $result); + $result = array_shift($lines); + $additionalLineBreaks = ''; + $shouldAddLeadingQuote = $result !== ''; + + foreach ($lines as $line) { + if ($line === '') { + $additionalLineBreaks .= '\n'; + } else { + $result .= $result + ? '\' . "\n' . $additionalLineBreaks . '" . \'' . $line + : '"\n' . $additionalLineBreaks . '" . \'' . $line; + $additionalLineBreaks = ''; + } + } + + if ($additionalLineBreaks) { + $result .= $result + ? '\' . "' . $additionalLineBreaks . '"' + : '"' . $additionalLineBreaks . '"'; + $shouldAddTrailingQuote = false; + } + } + + if ($shouldAddLeadingQuote) { + $result = '\'' . $result; + } + + if ($shouldAddTrailingQuote) { + $result .= '\''; + } + + return $result; + } + + private function transpileExpressionSegment(TemplateLiteralExpressionSegmentNode $segmentNode): string + { + $this->expressionTranspiler ??= new ExpressionTranspiler( + scope: $this->scope, + shouldAddQuotesIfNecessary: true + ); + + return $this->expressionTranspiler->transpile($segmentNode->expression); + } } diff --git a/src/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspiler.php b/src/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspiler.php index bd1a054d..f492e82a 100644 --- a/src/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspiler.php +++ b/src/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\TernaryOperation; -use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -46,8 +46,8 @@ public function transpile(TernaryOperationNode $ternaryOperationNode): string return sprintf( '(%s ? %s : %s)', $conditionTranspiler->transpile($ternaryOperationNode->condition), - $branchTranspiler->transpile($ternaryOperationNode->true), - $branchTranspiler->transpile($ternaryOperationNode->false) + $branchTranspiler->transpile($ternaryOperationNode->trueBranch), + $branchTranspiler->transpile($ternaryOperationNode->falseBranch) ); } } diff --git a/src/Target/Php/Transpiler/Text/TextTranspiler.php b/src/Target/Php/Transpiler/Text/TextTranspiler.php index 2b1d25a6..1d4042c9 100644 --- a/src/Target/Php/Transpiler/Text/TextTranspiler.php +++ b/src/Target/Php/Transpiler/Text/TextTranspiler.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Text; -use PackageFactory\ComponentEngine\Parser\Ast\TextNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; final class TextTranspiler { diff --git a/src/Target/Php/Transpiler/TypeReference/TypeReferenceStrategyInterface.php b/src/Target/Php/Transpiler/TypeReference/TypeReferenceStrategyInterface.php index 45ce8814..d4f94acf 100644 --- a/src/Target/Php/Transpiler/TypeReference/TypeReferenceStrategyInterface.php +++ b/src/Target/Php/Transpiler/TypeReference/TypeReferenceStrategyInterface.php @@ -22,12 +22,12 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; -use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; interface TypeReferenceStrategyInterface { @@ -35,5 +35,5 @@ public function getPhpTypeReferenceForSlotType(SlotType $slotType, TypeReference public function getPhpTypeReferenceForComponentType(ComponentType $componentType, TypeReferenceNode $typeReferenceNode): string; public function getPhpTypeReferenceForEnumType(EnumStaticType $enumType, TypeReferenceNode $typeReferenceNode): string; public function getPhpTypeReferenceForStructType(StructType $structType, TypeReferenceNode $typeReferenceNode): string; - public function getPhpTypeReferenceForCustomType(TypeInterface $customType, TypeReferenceNode $typeReferenceNode): string; + public function getPhpTypeReferenceForCustomType(AtomicTypeInterface $customType, TypeReferenceNode $typeReferenceNode): string; } diff --git a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php index 2992d632..d08c42b1 100644 --- a/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php +++ b/src/Target/Php/Transpiler/TypeReference/TypeReferenceTranspiler.php @@ -22,7 +22,9 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; @@ -31,6 +33,8 @@ use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; +use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class TypeReferenceTranspiler { @@ -42,8 +46,44 @@ public function __construct( public function transpile(TypeReferenceNode $typeReferenceNode): string { - $type = $this->scope->resolveTypeReference($typeReferenceNode); - $phpTypeReference = match ($type::class) { + $type = $this->getTypeFromTypeReferenceNode($typeReferenceNode); + $phpTypeReference = $this->transpileTypeToPHPTypeReference($type, $typeReferenceNode); + + if ($typeReferenceNode->isOptional) { + if (str_contains($phpTypeReference, '|')) { + $phpTypeReference = 'null|' . $phpTypeReference; + } else { + $phpTypeReference = '?' . $phpTypeReference; + } + } + + return $phpTypeReference; + } + + private function getTypeFromTypeReferenceNode(TypeReferenceNode $typeReferenceNode): TypeInterface + { + $types = array_map( + fn (TypeName $typeName) => $this->scope->getType($typeName), + $typeReferenceNode->names->toTypeNames()->items + ); + + return UnionType::of(...$types); + } + + private function transpileTypeToPHPTypeReference(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string + { + if ($type instanceof UnionType) { + return join('|', array_map( + fn (TypeInterface $type) => $this->transpileTypeToPHPTypeReference($type, $typeReferenceNode), + $type->members + )); + } + + if (!($type instanceof AtomicTypeInterface)) { + throw new \Exception('@TODO: Cannot transpile type ' . $type::class); + } + + return match ($type::class) { IntegerType::class => 'int|float', StringType::class => 'string', BooleanType::class => 'bool', @@ -53,12 +93,5 @@ public function transpile(TypeReferenceNode $typeReferenceNode): string StructType::class => $this->strategy->getPhpTypeReferenceForStructType($type, $typeReferenceNode), default => $this->strategy->getPhpTypeReferenceForCustomType($type, $typeReferenceNode) }; - - return $typeReferenceNode->isOptional - ? match ($phpTypeReference) { - 'int|float' => 'null|int|float', - default => '?' . $phpTypeReference - } - : $phpTypeReference; } } diff --git a/src/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspiler.php b/src/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspiler.php index dd6e7595..7f0ca97a 100644 --- a/src/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspiler.php +++ b/src/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspiler.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\UnaryOperation; -use PackageFactory\ComponentEngine\Definition\UnaryOperator; -use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperator; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -48,7 +48,7 @@ public function transpile(UnaryOperationNode $unaryOperationNode): string ); $operator = $this->transpileUnaryOperator($unaryOperationNode->operator); - $argument = $expressionTranspiler->transpile($unaryOperationNode->argument); + $argument = $expressionTranspiler->transpile($unaryOperationNode->operand); return sprintf('(%s%s)', $operator, $argument); } diff --git a/src/Target/Php/Transpiler/Identifier/IdentifierTranspiler.php b/src/Target/Php/Transpiler/ValueReference/ValueReferenceTranspiler.php similarity index 73% rename from src/Target/Php/Transpiler/Identifier/IdentifierTranspiler.php rename to src/Target/Php/Transpiler/ValueReference/ValueReferenceTranspiler.php index e58291d1..7e535ee3 100644 --- a/src/Target/Php/Transpiler/Identifier/IdentifierTranspiler.php +++ b/src/Target/Php/Transpiler/ValueReference/ValueReferenceTranspiler.php @@ -20,26 +20,26 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\Identifier; +namespace PackageFactory\ComponentEngine\Target\Php\Transpiler\ValueReference; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; -final class IdentifierTranspiler +final class ValueReferenceTranspiler { public function __construct(private readonly ScopeInterface $scope) { } - public function transpile(IdentifierNode $identifierNode): string + public function transpile(ValueReferenceNode $identifierNode): string { - $typeOfIdentifiedValue = $this->scope->lookupTypeFor($identifierNode->value); + $typeOfIdentifiedValue = $this->scope->getTypeOf($identifierNode->name); return match (true) { // @TODO: Generate Name via TypeReferenceStrategyInterface Dynamically - $typeOfIdentifiedValue instanceof EnumStaticType => $identifierNode->value, - default => '$this->' . $identifierNode->value + $typeOfIdentifiedValue instanceof EnumStaticType => $identifierNode->name->value, + default => '$this->' . $identifierNode->name->value }; } } diff --git a/src/TypeSystem/AtomicTypeInterface.php b/src/TypeSystem/AtomicTypeInterface.php new file mode 100644 index 00000000..60de5a7d --- /dev/null +++ b/src/TypeSystem/AtomicTypeInterface.php @@ -0,0 +1,30 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; + +interface AtomicTypeInterface extends TypeInterface +{ + public function getName(): TypeName; +} diff --git a/src/TypeSystem/Resolver/Access/AccessTypeResolver.php b/src/TypeSystem/Resolver/Access/AccessTypeResolver.php index 2bceeae2..f804eca9 100644 --- a/src/TypeSystem/Resolver/Access/AccessTypeResolver.php +++ b/src/TypeSystem/Resolver/Access/AccessTypeResolver.php @@ -22,15 +22,14 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\Access; - -use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumInstanceType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -use PackageFactory\ComponentEngine\Definition\AccessType; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessKeyNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; final class AccessTypeResolver { @@ -41,27 +40,23 @@ public function __construct( public function resolveTypeOf(AccessNode $accessNode): TypeInterface { - $expressionResolver = new ExpressionTypeResolver(scope: $this->scope); - $rootType = $expressionResolver->resolveTypeOf($accessNode->root); + $expressionTypeResolver = new ExpressionTypeResolver(scope: $this->scope); + $parentType = $expressionTypeResolver->resolveTypeOf($accessNode->parent); - return match ($rootType::class) { - EnumStaticType::class => $this->createEnumInstanceMemberType($accessNode, $rootType), - StructType::class => throw new \Exception('@TODO: StructType Access is not implemented'), - default => throw new \Exception('@TODO Error: Cannot access on type ' . $rootType::class) + return match ($parentType::class) { + EnumStaticType::class => $this->resolveEnumInstanceMemberType($parentType, $accessNode->key), + StructType::class => $this->resolveStructPropertyType($parentType, $accessNode->key), + default => throw new \Exception('@TODO Error: Cannot access on type ' . $parentType::class) }; } - - private function createEnumInstanceMemberType(AccessNode $accessNode, EnumStaticType $enumType): EnumInstanceType + + private function resolveEnumInstanceMemberType(EnumStaticType $enumType, AccessKeyNode $keyNode): EnumInstanceType { - if (!( - count($accessNode->chain->items) === 1 - && $accessNode->chain->items[0]->accessType === AccessType::MANDATORY - )) { - throw new \Error('@TODO Error: Enum access malformed, only one level member access is allowed.'); - } + return $enumType->getMemberType($keyNode->value->toEnumMemberName()); + } - $enumMemberName = $accessNode->chain->items[0]->accessor->value; - - return $enumType->getMemberType($enumMemberName); + private function resolveStructPropertyType(StructType $structType, AccessKeyNode $keyNode): TypeInterface + { + return $structType->getTypeOfProperty($keyNode->value)->toType($this->scope); } } diff --git a/src/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolver.php b/src/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolver.php index d6580a6c..b81798cd 100644 --- a/src/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolver.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\BinaryOperation; -use PackageFactory\ComponentEngine\Definition\BinaryOperator; -use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperator; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; @@ -48,7 +48,7 @@ public function resolveTypeOf(BinaryOperationNode $binaryOperationNode): TypeInt BinaryOperator::GREATER_THAN, BinaryOperator::GREATER_THAN_OR_EQUAL, BinaryOperator::LESS_THAN, - BinaryOperator::LESS_THAN_OR_EQUAL => BooleanType::get() + BinaryOperator::LESS_THAN_OR_EQUAL => BooleanType::singleton() }; } @@ -58,9 +58,9 @@ private function resolveTypeOfBooleanOperation(BinaryOperationNode $binaryOperat scope: $this->scope ); - return UnionType::of( - $expressionTypeResolver->resolveTypeOf($binaryOperationNode->left), - $expressionTypeResolver->resolveTypeOf($binaryOperationNode->right) + return UnionType::merge( + $expressionTypeResolver->resolveTypeOf($binaryOperationNode->leftOperand), + $expressionTypeResolver->resolveTypeOf($binaryOperationNode->rightOperand) ); } } diff --git a/src/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolver.php b/src/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolver.php index 3d4fa313..7cfe1b65 100644 --- a/src/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolver.php +++ b/src/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolver.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\BooleanLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -30,6 +30,6 @@ final class BooleanLiteralTypeResolver { public function resolveTypeOf(BooleanLiteralNode $booleanLiteralNode): TypeInterface { - return BooleanType::get(); + return BooleanType::singleton(); } } diff --git a/src/TypeSystem/Resolver/Expression/ExpressionTypeResolver.php b/src/TypeSystem/Resolver/Expression/ExpressionTypeResolver.php index 872b9da1..2a273d63 100644 --- a/src/TypeSystem/Resolver/Expression/ExpressionTypeResolver.php +++ b/src/TypeSystem/Resolver/Expression/ExpressionTypeResolver.php @@ -22,22 +22,22 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression; -use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; -use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; -use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; -use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\IntegerLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\TagNode; -use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Access\AccessTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Resolver\BinaryOperation\BinaryOperationTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Resolver\BooleanLiteral\BooleanLiteralTypeResolver; -use PackageFactory\ComponentEngine\TypeSystem\Resolver\Identifier\IdentifierTypeResolver; +use PackageFactory\ComponentEngine\TypeSystem\Resolver\ValueReference\ValueReferenceTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Match\MatchTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Resolver\NullLiteral\NullLiteralTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Resolver\IntegerLiteral\IntegerLiteralTypeResolver; @@ -64,7 +64,7 @@ public function resolveTypeOf(ExpressionNode $expressionNode): TypeInterface ))->resolveTypeOf($rootNode), BooleanLiteralNode::class => (new BooleanLiteralTypeResolver()) ->resolveTypeOf($rootNode), - IdentifierNode::class => (new IdentifierTypeResolver( + ValueReferenceNode::class => (new ValueReferenceTypeResolver( scope: $this->scope ))->resolveTypeOf($rootNode), MatchNode::class => (new MatchTypeResolver( diff --git a/src/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolver.php b/src/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolver.php index 93582f6c..21951e09 100644 --- a/src/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolver.php +++ b/src/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolver.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\IntegerLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -30,6 +30,6 @@ final class IntegerLiteralTypeResolver { public function resolveTypeOf(IntegerLiteralNode $IntegerLiteralNode): TypeInterface { - return IntegerType::get(); + return IntegerType::singleton(); } } diff --git a/src/TypeSystem/Resolver/Match/MatchTypeResolver.php b/src/TypeSystem/Resolver/Match/MatchTypeResolver.php index 9301e179..6e7fd609 100644 --- a/src/TypeSystem/Resolver/Match/MatchTypeResolver.php +++ b/src/TypeSystem/Resolver/Match/MatchTypeResolver.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\Match; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; @@ -66,23 +66,13 @@ private function resolveTypeOfBooleanMatch(MatchNode $matchNode): TypeInterface throw new \Exception('@TODO: Not implemented: Incomplete Match'); } else { $types = []; - - $defaultArmPresent = false; foreach ($matchNode->arms->items as $matchArmNode) { - if ($defaultArmPresent) { - throw new \Exception('@TODO: Multiple illegal default arms'); - } - if ($matchArmNode->left === null) { - $defaultArmPresent = true; - } - $types[] = $expressionTypeResolver->resolveTypeOf( - $matchArmNode->right - ); + $types[] = $expressionTypeResolver->resolveTypeOf($matchArmNode->right); } // @TODO: Ensure that match is complete - return UnionType::of(...$types); + return UnionType::merge(...$types); } } @@ -97,9 +87,6 @@ private function resolveTypeOfEnumMatch(MatchNode $matchNode, EnumInstanceType $ $referencedEnumMembers = []; foreach ($matchNode->arms->items as $matchArmNode) { - if ($defaultArmPresent) { - throw new \Exception('@TODO Error: Multiple illegal default arms'); - } if ($matchArmNode->left === null) { $defaultArmPresent = true; } else { @@ -114,14 +101,14 @@ private function resolveTypeOfEnumMatch(MatchNode $matchNode, EnumInstanceType $ } if (!$enumMemberType->enumStaticType->is($subjectEnumType->enumStaticType)) { - throw new \Error('@TODO Error: incompatible enum match: got ' . $enumMemberType->enumStaticType->enumName . ' expected ' . $subjectEnumType->enumStaticType->enumName); + throw new \Error('@TODO Error: incompatible enum match: got ' . $enumMemberType->enumStaticType->getName()->value . ' expected ' . $subjectEnumType->enumStaticType->getName()->value); } - if (isset($referencedEnumMembers[$enumMemberType->getMemberName()])) { - throw new \Error('@TODO Error: Enum path ' . $enumMemberType->getMemberName() . ' was already defined once in this match and cannot be used twice'); + if (isset($referencedEnumMembers[$enumMemberType->getMemberName()->value])) { + throw new \Error('@TODO Error: Enum path ' . $enumMemberType->getMemberName()->value . ' was already defined once in this match and cannot be used twice'); } - $referencedEnumMembers[$enumMemberType->getMemberName()] = true; + $referencedEnumMembers[$enumMemberType->getMemberName()->value] = true; } } @@ -138,7 +125,7 @@ private function resolveTypeOfEnumMatch(MatchNode $matchNode, EnumInstanceType $ } } - return UnionType::of(...$types); + return UnionType::merge(...$types); } public function resolveTypeOf(MatchNode $matchNode): TypeInterface @@ -151,7 +138,7 @@ public function resolveTypeOf(MatchNode $matchNode): TypeInterface ); return match (true) { - BooleanType::get()->is($typeOfSubject) => $this->resolveTypeOfBooleanMatch($matchNode), + BooleanType::singleton()->is($typeOfSubject) => $this->resolveTypeOfBooleanMatch($matchNode), $typeOfSubject instanceof EnumInstanceType => $this->resolveTypeOfEnumMatch($matchNode, $typeOfSubject), default => throw new \Exception('@TODO: Not handled ' . $typeOfSubject::class) }; diff --git a/src/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolver.php b/src/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolver.php index 63675a64..03acdb99 100644 --- a/src/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolver.php +++ b/src/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolver.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\NullLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -30,6 +30,6 @@ final class NullLiteralTypeResolver { public function resolveTypeOf(NullLiteralNode $nullLiteralNode): TypeInterface { - return NullType::get(); + return NullType::singleton(); } } diff --git a/src/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolver.php b/src/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolver.php index 7bdee085..6cf9a83a 100644 --- a/src/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolver.php +++ b/src/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolver.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\StringLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -30,6 +30,6 @@ final class StringLiteralTypeResolver { public function resolveTypeOf(StringLiteralNode $stringLiteralNode): TypeInterface { - return StringType::get(); + return StringType::singleton(); } } diff --git a/src/TypeSystem/Resolver/Tag/TagTypeResolver.php b/src/TypeSystem/Resolver/Tag/TagTypeResolver.php index 79bacd23..d8ec6f98 100644 --- a/src/TypeSystem/Resolver/Tag/TagTypeResolver.php +++ b/src/TypeSystem/Resolver/Tag/TagTypeResolver.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\Tag; -use PackageFactory\ComponentEngine\Parser\Ast\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -30,6 +30,6 @@ final class TagTypeResolver { public function resolveTypeOf(TagNode $tagNode): TypeInterface { - return StringType::get(); + return StringType::singleton(); } } diff --git a/src/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolver.php b/src/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolver.php index 1cde5b61..7ba43acf 100644 --- a/src/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolver.php +++ b/src/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolver.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\TemplateLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -30,6 +30,6 @@ final class TemplateLiteralTypeResolver { public function resolveTypeOf(TemplateLiteralNode $templateLiteralNode): TypeInterface { - return StringType::get(); + return StringType::singleton(); } } diff --git a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php index c3e6d8ff..1e249c35 100644 --- a/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php +++ b/src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\TernaryOperation; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; @@ -45,13 +45,13 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI if ($conditionNode instanceof BooleanLiteralNode) { return $conditionNode->value - ? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true) - : $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false); + ? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->trueBranch) + : $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->falseBranch); } - return UnionType::of( - $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true), - $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false) + return UnionType::merge( + $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->trueBranch), + $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->falseBranch) ); } } diff --git a/src/TypeSystem/Resolver/Identifier/IdentifierTypeResolver.php b/src/TypeSystem/Resolver/ValueReference/ValueReferenceTypeResolver.php similarity index 75% rename from src/TypeSystem/Resolver/Identifier/IdentifierTypeResolver.php rename to src/TypeSystem/Resolver/ValueReference/ValueReferenceTypeResolver.php index c0b8d811..ae9c4594 100644 --- a/src/TypeSystem/Resolver/Identifier/IdentifierTypeResolver.php +++ b/src/TypeSystem/Resolver/ValueReference/ValueReferenceTypeResolver.php @@ -20,25 +20,25 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\Identifier; +namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\ValueReference; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class IdentifierTypeResolver +final class ValueReferenceTypeResolver { public function __construct(private readonly ScopeInterface $scope) { } - public function resolveTypeOf(IdentifierNode $identifierNode): TypeInterface + public function resolveTypeOf(ValueReferenceNode $identifierNode): TypeInterface { - $name = $identifierNode->value; - $foundType = $this->scope->lookupTypeFor($name); + $name = $identifierNode->name; + $foundType = $this->scope->getTypeOf($name); if ($foundType === null) { - throw new \Exception('@TODO: Unknown identifier ' . $name); + throw new \Exception('@TODO: Unknown identifier ' . $name->value); } else { return $foundType; } diff --git a/src/TypeSystem/Scope/ComponentScope/ComponentScope.php b/src/TypeSystem/Scope/ComponentScope/ComponentScope.php index 853912d0..dab24788 100644 --- a/src/TypeSystem/Scope/ComponentScope/ComponentScope.php +++ b/src/TypeSystem/Scope/ComponentScope/ComponentScope.php @@ -22,10 +22,13 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Scope\ComponentScope; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class ComponentScope implements ScopeInterface @@ -36,23 +39,28 @@ public function __construct( ) { } - public function lookupTypeFor(string $name): ?TypeInterface + public function getType(TypeName $typeName): AtomicTypeInterface { - $propertyDeclarationNode = $this->componentDeclarationNode->propertyDeclarations->getPropertyDeclarationNodeOfName($name); - if ($propertyDeclarationNode) { - $typeReferenceNode = $propertyDeclarationNode->type; - $type = $this->resolveTypeReference($typeReferenceNode); - if ($type instanceof EnumStaticType) { - $type = $type->toEnumInstanceType(); - } - return $type; - } - - return $this->parentScope->lookupTypeFor($name); + return $this->parentScope->getType($typeName); } - public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface + public function getTypeOf(VariableName $variableName): ?TypeInterface { - return $this->parentScope->resolveTypeReference($typeReferenceNode); + foreach ($this->componentDeclarationNode->props->items as $propertyDeclarationNode) { + if ($propertyDeclarationNode->name->value->value === $variableName->value) { + $types = []; + foreach ($propertyDeclarationNode->type->names->toTypeNames()->items as $typeName) { + $type = $this->getType($typeName); + if ($type instanceof EnumStaticType) { + $type = $type->toEnumInstanceType(); + } + $types[] = $type; + } + + return UnionType::of(...$types); + } + } + + return $this->parentScope->getTypeOf($variableName); } } diff --git a/src/TypeSystem/Scope/GlobalScope/GlobalScope.php b/src/TypeSystem/Scope/GlobalScope/GlobalScope.php index f9253a55..6025584b 100644 --- a/src/TypeSystem/Scope/GlobalScope/GlobalScope.php +++ b/src/TypeSystem/Scope/GlobalScope/GlobalScope.php @@ -22,7 +22,10 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Scope\GlobalScope; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; @@ -32,30 +35,21 @@ final class GlobalScope implements ScopeInterface { - private static null|self $instance = null; + use Singleton; - private function __construct() + public function getType(TypeName $typeName): AtomicTypeInterface { + return match ($typeName) { + StringType::singleton()->getName() => StringType::singleton(), + IntegerType::singleton()->getName() => IntegerType::singleton(), + BooleanType::singleton()->getName() => BooleanType::singleton(), + SlotType::singleton()->getName() => SlotType::singleton(), + default => throw new \Exception('@TODO: Unknown Type ' . $typeName->value) + }; } - public static function get(): self - { - return self::$instance ??= new self(); - } - - public function lookupTypeFor(string $name): ?TypeInterface + public function getTypeOf(VariableName $variableName): ?TypeInterface { return null; } - - public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface - { - return match ($typeReferenceNode->name) { - 'string' => StringType::get(), - 'number' => IntegerType::get(), - 'boolean' => BooleanType::get(), - 'slot' => SlotType::get(), - default => throw new \Exception('@TODO: Unknown Type ' . $typeReferenceNode->name) - }; - } } diff --git a/src/TypeSystem/Scope/ModuleScope/ModuleScope.php b/src/TypeSystem/Scope/ModuleScope/ModuleScope.php index 55e386c9..e0d62f51 100644 --- a/src/TypeSystem/Scope/ModuleScope/ModuleScope.php +++ b/src/TypeSystem/Scope/ModuleScope/ModuleScope.php @@ -22,9 +22,11 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Scope\ModuleScope; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; use PackageFactory\ComponentEngine\Module\LoaderInterface; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -37,20 +39,33 @@ public function __construct( ) { } - public function lookupTypeFor(string $name): ?TypeInterface + public function getType(TypeName $typeName): AtomicTypeInterface { - if ($importNode = $this->moduleNode->imports->get($name)) { - return $this->loader->resolveTypeOfImport($importNode); + foreach ($this->moduleNode->imports->items as $importNode) { + foreach ($importNode->names->items as $importedNameNode) { + if ($importedNameNode->value->value === $typeName->value) { + $module = $this->loader->loadModule($importNode->path->value); + + return $module->getTypeOf($typeName->toVariableName()); + } + } } - return $this->parentScope->lookupTypeFor($name); + + return $this->parentScope->getType($typeName); } - public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface + public function getTypeOf(VariableName $variableName): ?TypeInterface { - if ($importNode = $this->moduleNode->imports->get($typeReferenceNode->name)) { - return $this->loader->resolveTypeOfImport($importNode); + foreach ($this->moduleNode->imports->items as $importNode) { + foreach ($importNode->names->items as $importedNameNode) { + if ($importedNameNode->value->value === $variableName->value) { + $module = $this->loader->loadModule($importNode->path->value); + + return $module->getTypeOf($variableName); + } + } } - return $this->parentScope->resolveTypeReference($typeReferenceNode); + return $this->parentScope->getTypeOf($variableName); } } diff --git a/src/TypeSystem/ScopeInterface.php b/src/TypeSystem/ScopeInterface.php index 029d3093..a53304f9 100644 --- a/src/TypeSystem/ScopeInterface.php +++ b/src/TypeSystem/ScopeInterface.php @@ -22,10 +22,11 @@ namespace PackageFactory\ComponentEngine\TypeSystem; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; interface ScopeInterface { - public function lookupTypeFor(string $name): ?TypeInterface; - public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface; + public function getType(TypeName $typeName): AtomicTypeInterface; + public function getTypeOf(VariableName $variableName): ?TypeInterface; } diff --git a/src/TypeSystem/Type/BooleanType/BooleanType.php b/src/TypeSystem/Type/BooleanType/BooleanType.php index 8bd7072a..f0f8af55 100644 --- a/src/TypeSystem/Type/BooleanType/BooleanType.php +++ b/src/TypeSystem/Type/BooleanType/BooleanType.php @@ -22,19 +22,18 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class BooleanType implements TypeInterface +final class BooleanType implements AtomicTypeInterface { - private static null|self $instance = null; + use Singleton; - private function __construct() + public function getName(): TypeName { - } // @codeCoverageIgnore - - public static function get(): self - { - return self::$instance ??= new self(); + return TypeName::from('boolean'); } public function is(TypeInterface $other): bool diff --git a/src/TypeSystem/Type/ComponentType/ComponentType.php b/src/TypeSystem/Type/ComponentType/ComponentType.php index ba853313..cc609fd9 100644 --- a/src/TypeSystem/Type/ComponentType/ComponentType.php +++ b/src/TypeSystem/Type/ComponentType/ComponentType.php @@ -22,20 +22,26 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Domain\ComponentName\ComponentName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class ComponentType implements TypeInterface +final class ComponentType implements AtomicTypeInterface { - private function __construct(public readonly string $componentName) + private function __construct(private readonly ComponentName $name) { } public static function fromComponentDeclarationNode(ComponentDeclarationNode $componentDeclarationNode): self { - return new self( - componentName: $componentDeclarationNode->componentName - ); + return new self($componentDeclarationNode->name->value); + } + + public function getName(): TypeName + { + return $this->name->toTypeName(); } public function is(TypeInterface $other): bool diff --git a/src/TypeSystem/Type/EnumType/EnumInstanceType.php b/src/TypeSystem/Type/EnumType/EnumInstanceType.php index 69729d4a..9949c347 100644 --- a/src/TypeSystem/Type/EnumType/EnumInstanceType.php +++ b/src/TypeSystem/Type/EnumType/EnumInstanceType.php @@ -22,16 +22,19 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\EnumType; +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class EnumInstanceType implements TypeInterface +final class EnumInstanceType implements AtomicTypeInterface { private function __construct( public readonly EnumStaticType $enumStaticType, - private readonly ?string $memberName + private readonly ?EnumMemberName $name ) { - if ($memberName !== null && !$enumStaticType->hasMember($memberName)) { - throw new \Exception('@TODO cannot access member ' . $memberName . ' of enum ' . $enumStaticType->enumName); + if ($name !== null && !$enumStaticType->hasMember($name)) { + throw new \Exception('@TODO cannot access member ' . $name->value . ' of enum ' . $enumStaticType->getName()->value); } } @@ -39,26 +42,31 @@ public static function createUnspecifiedEnumInstanceType(EnumStaticType $enumSta { return new self( enumStaticType: $enumStaticType, - memberName: null + name: null ); } - public static function fromStaticEnumTypeAndMemberName(EnumStaticType $enumStaticType, string $enumMemberName): self + public static function fromStaticEnumTypeAndMemberName(EnumStaticType $enumStaticType, EnumMemberName $enumMemberName): self { return new self( enumStaticType: $enumStaticType, - memberName: $enumMemberName + name: $enumMemberName ); } public function isUnspecified(): bool { - return $this->memberName === null; + return $this->name === null; } - public function getMemberName(): string + public function getMemberName(): EnumMemberName { - return $this->memberName ?? throw new \Exception('@TODO Error cannot access memberName of unspecified instance'); + return $this->name ?? throw new \Exception('@TODO Error cannot access name of unspecified instance'); + } + + public function getName(): TypeName + { + return $this->enumStaticType->getName(); } public function is(TypeInterface $other): bool @@ -68,7 +76,7 @@ public function is(TypeInterface $other): bool } if ($other instanceof EnumInstanceType) { return $other->enumStaticType->is($other->enumStaticType) - && $other->memberName === $this->memberName; + && $other->name === $this->name; } return false; } diff --git a/src/TypeSystem/Type/EnumType/EnumStaticType.php b/src/TypeSystem/Type/EnumType/EnumStaticType.php index df730104..7b4bf7cc 100644 --- a/src/TypeSystem/Type/EnumType/EnumStaticType.php +++ b/src/TypeSystem/Type/EnumType/EnumStaticType.php @@ -22,19 +22,23 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\EnumType; +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PackageFactory\ComponentEngine\Domain\EnumName\EnumName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class EnumStaticType implements TypeInterface +final class EnumStaticType implements AtomicTypeInterface { /** - * @param string $enumName + * @param EnumName $name * @param ModuleId $moduleId * @param array $memberNameHashMap */ public function __construct( - public readonly string $enumName, + private readonly EnumName $name, private readonly ModuleId $moduleId, private readonly array $memberNameHashMap, ) { @@ -43,12 +47,12 @@ public function __construct( public static function fromModuleIdAndDeclaration(ModuleId $moduleId, EnumDeclarationNode $enumDeclarationNode): self { $memberNameHashMap = []; - foreach ($enumDeclarationNode->memberDeclarations->items as $memberDeclarationNode) { - $memberNameHashMap[$memberDeclarationNode->name] = true; + foreach ($enumDeclarationNode->members->items as $memberDeclarationNode) { + $memberNameHashMap[$memberDeclarationNode->name->value->value] = true; } return new self( - enumName: $enumDeclarationNode->enumName, + name: $enumDeclarationNode->name->value, moduleId: $moduleId, memberNameHashMap: $memberNameHashMap ); @@ -62,12 +66,12 @@ public function getMemberNames(): array return array_keys($this->memberNameHashMap); } - public function hasMember(string $memberName): bool + public function hasMember(EnumMemberName $memberName): bool { - return array_key_exists($memberName, $this->memberNameHashMap); + return array_key_exists($memberName->value, $this->memberNameHashMap); } - public function getMemberType(string $memberName): EnumInstanceType + public function getMemberType(EnumMemberName $memberName): EnumInstanceType { return EnumInstanceType::fromStaticEnumTypeAndMemberName( $this, @@ -75,6 +79,11 @@ public function getMemberType(string $memberName): EnumInstanceType ); } + public function getName(): TypeName + { + return $this->name->toTypeName(); + } + public function is(TypeInterface $other): bool { if ($other === $this) { @@ -82,7 +91,7 @@ public function is(TypeInterface $other): bool } if ($other instanceof EnumStaticType) { return $this->moduleId === $other->moduleId - && $this->enumName === $other->enumName; + && $this->name->value === $other->name->value; } return false; } diff --git a/src/TypeSystem/Type/IntegerType/IntegerType.php b/src/TypeSystem/Type/IntegerType/IntegerType.php index d7570833..cf293caf 100644 --- a/src/TypeSystem/Type/IntegerType/IntegerType.php +++ b/src/TypeSystem/Type/IntegerType/IntegerType.php @@ -22,19 +22,18 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class IntegerType implements TypeInterface +final class IntegerType implements AtomicTypeInterface { - private static null|self $instance = null; + use Singleton; - private function __construct() + public function getName(): TypeName { - } // @codeCoverageIgnore - - public static function get(): self - { - return self::$instance ??= new self(); + return TypeName::from('number'); } public function is(TypeInterface $other): bool diff --git a/src/TypeSystem/Type/NullType/NullType.php b/src/TypeSystem/Type/NullType/NullType.php index 068b4b89..848be2bc 100644 --- a/src/TypeSystem/Type/NullType/NullType.php +++ b/src/TypeSystem/Type/NullType/NullType.php @@ -22,19 +22,18 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\NullType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class NullType implements TypeInterface +final class NullType implements AtomicTypeInterface { - private static null|self $instance = null; + use Singleton; - private function __construct() + public function getName(): TypeName { - } - - public static function get(): self - { - return self::$instance ??= new self(); + return TypeName::from('null'); } public function is(TypeInterface $other): bool diff --git a/src/TypeSystem/Type/SlotType/SlotType.php b/src/TypeSystem/Type/SlotType/SlotType.php index 99c48037..db2b98b1 100644 --- a/src/TypeSystem/Type/SlotType/SlotType.php +++ b/src/TypeSystem/Type/SlotType/SlotType.php @@ -22,19 +22,18 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\SlotType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class SlotType implements TypeInterface +final class SlotType implements AtomicTypeInterface { - private static null|self $instance = null; + use Singleton; - private function __construct() + public function getName(): TypeName { - } - - public static function get(): self - { - return self::$instance ??= new self(); + return TypeName::from('slot'); } public function is(TypeInterface $other): bool diff --git a/src/TypeSystem/Type/StringType/StringType.php b/src/TypeSystem/Type/StringType/StringType.php index a69932fb..869b1e31 100644 --- a/src/TypeSystem/Type/StringType/StringType.php +++ b/src/TypeSystem/Type/StringType/StringType.php @@ -22,19 +22,18 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\StringType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Framework\PHP\Singleton\Singleton; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; -final class StringType implements TypeInterface +final class StringType implements AtomicTypeInterface { - private static null|self $instance = null; + use Singleton; - private function __construct() + public function getName(): TypeName { - } // @codeCoverageIgnore - - public static function get(): self - { - return self::$instance ??= new self(); + return TypeName::from('string'); } public function is(TypeInterface $other): bool diff --git a/src/TypeSystem/Type/StructType/Properties.php b/src/TypeSystem/Type/StructType/Properties.php new file mode 100644 index 00000000..6e58d181 --- /dev/null +++ b/src/TypeSystem/Type/StructType/Properties.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem\Type\StructType; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; + +final class Properties +{ + /** + * @var array + */ + private readonly array $items; + + public function __construct(Property ...$items) + { + $itemsAsHashMap = []; + foreach ($items as $item) { + $itemsAsHashMap[$item->name->value] = $item; + } + + $this->items = $itemsAsHashMap; + } + + public function get(PropertyName $propertyName): ?Property + { + return $this->items[$propertyName->value] ?? null; + } +} diff --git a/src/TypeSystem/Type/StructType/Property.php b/src/TypeSystem/Type/StructType/Property.php new file mode 100644 index 00000000..4fb1a91a --- /dev/null +++ b/src/TypeSystem/Type/StructType/Property.php @@ -0,0 +1,35 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem\Type\StructType; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\TypeSystem\TypeReference; + +final class Property +{ + public function __construct( + public readonly PropertyName $name, + public readonly TypeReference $type + ) { + } +} diff --git a/src/TypeSystem/Type/StructType/StructType.php b/src/TypeSystem/Type/StructType/StructType.php index a2481d2b..93416067 100644 --- a/src/TypeSystem/Type/StructType/StructType.php +++ b/src/TypeSystem/Type/StructType/StructType.php @@ -22,24 +22,37 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\StructType; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; +use PackageFactory\ComponentEngine\TypeSystem\TypeReference; -final class StructType implements TypeInterface +final class StructType implements AtomicTypeInterface { - private function __construct(public readonly string $structName) - { + public function __construct( + private readonly StructName $name, + private readonly Properties $properties + ) { } - public static function fromStructDeclarationNode(StructDeclarationNode $structDeclarationNode): self + public function getName(): TypeName { - return new self( - structName: $structDeclarationNode->structName - ); + return $this->name->toTypeName(); } public function is(TypeInterface $other): bool { return $other === $this; } + + public function getTypeOfProperty(PropertyName $propertyName): TypeReference + { + if ($property = $this->properties->get($propertyName)) { + return $property->type; + } + + throw new \Exception('@TODO: Unknown struct property: ' . $propertyName->value); + } } diff --git a/src/TypeSystem/Type/UnionType/UnionType.php b/src/TypeSystem/Type/UnionType/UnionType.php index c48ed99d..7132000f 100644 --- a/src/TypeSystem/Type/UnionType/UnionType.php +++ b/src/TypeSystem/Type/UnionType/UnionType.php @@ -22,21 +22,22 @@ namespace PackageFactory\ComponentEngine\TypeSystem\Type\UnionType; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class UnionType implements TypeInterface { /** - * @var TypeInterface[] + * @var AtomicTypeInterface[] */ - private array $members; + public readonly array $members; - private function __construct(TypeInterface ...$members) + private function __construct(AtomicTypeInterface ...$members) { $this->members = $members; } - public static function of(TypeInterface ...$members): TypeInterface + public static function of(AtomicTypeInterface ...$members): TypeInterface { $uniqueMembers = []; foreach ($members as $member) { @@ -56,6 +57,23 @@ public static function of(TypeInterface ...$members): TypeInterface return new self(...$members); } + public static function merge(TypeInterface ...$items): TypeInterface + { + $members = []; + + foreach ($items as $item) { + if ($item instanceof UnionType) { + $members = array_merge($members, $item->members); + } else if ($item instanceof AtomicTypeInterface) { + $members[] = $item; + } else { + throw new \Exception('@TODO: Unable to merge type ' . $item::class); + } + } + + return self::of(...$members); + } + public function is(TypeInterface $other): bool { if ($other instanceof UnionType) { diff --git a/src/TypeSystem/TypeReference.php b/src/TypeSystem/TypeReference.php new file mode 100644 index 00000000..a05bddf6 --- /dev/null +++ b/src/TypeSystem/TypeReference.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\TypeSystem; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; +use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; +use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; + +final class TypeReference +{ + public function __construct( + public readonly TypeNames $names, + public readonly bool $isOptional, + public readonly bool $isArray + ) { + } + + public function toType(ScopeInterface $scope): TypeInterface + { + $types = []; + + if ($this->isOptional) { + $types[] = NullType::singleton(); + } + + foreach ($this->names->items as $name) { + $types[] = $scope->getType($name); + } + + + return UnionType::of(...$types); + } +} diff --git a/test/Integration/Examples/Comment/Comment.afx b/test/Integration/Examples/Comment/Comment.afx deleted file mode 100644 index 6f738014..00000000 --- a/test/Integration/Examples/Comment/Comment.afx +++ /dev/null @@ -1,3 +0,0 @@ -# This is a comment. -# The only way to have comments is to prefix them with '#'. -# A comment alwas ends at an END_OF_LINE token. diff --git a/test/Integration/Examples/Comment/Comment.ast.alt.json b/test/Integration/Examples/Comment/Comment.ast.alt.json deleted file mode 100644 index 1cfb3f65..00000000 --- a/test/Integration/Examples/Comment/Comment.ast.alt.json +++ /dev/null @@ -1 +0,0 @@ -[1,[2,[],[]]] diff --git a/test/Integration/Examples/Comment/Comment.ast.json b/test/Integration/Examples/Comment/Comment.ast.json deleted file mode 100644 index 99bbf0b4..00000000 --- a/test/Integration/Examples/Comment/Comment.ast.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": {} - } -} diff --git a/test/Integration/Examples/Comment/Comment.tokens.json b/test/Integration/Examples/Comment/Comment.tokens.json deleted file mode 100644 index 4badfb1a..00000000 --- a/test/Integration/Examples/Comment/Comment.tokens.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "type": "COMMENT", - "value": "# This is a comment." - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "COMMENT", - "value": "# The only way to have comments is to prefix them with '#'." - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "COMMENT", - "value": "# A comment alwas ends at an END_OF_LINE token." - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/Component/Component.ast.alt.json b/test/Integration/Examples/Component/Component.ast.alt.json deleted file mode 100644 index 7bbbeb61..00000000 --- a/test/Integration/Examples/Component/Component.ast.alt.json +++ /dev/null @@ -1 +0,0 @@ -[1,[2,[],[[3,["Image",[["src",[4,"string",0]],["alt",[4,"string",0]],["title",[4,"string",0]]],["Tag","img",[["src",[5,"src",0]],["alt",[5,"alt",0]],["title",[5,"title",0]]],[]]]]]]] diff --git a/test/Integration/Examples/Component/Component.ast.json b/test/Integration/Examples/Component/Component.ast.json deleted file mode 100644 index 3998cab8..00000000 --- a/test/Integration/Examples/Component/Component.ast.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "Image", - "propertyDeclarations": [ - { - "name": "src", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "alt", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "title", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - } - ], - "returnExpression": { - "type": "TagNode", - "payload": { - "tagName": "img", - "attributes": [ - { - "name": "src", - "value": { - "type": "Identifier", - "payload": "src" - } - }, - { - "name": "alt", - "value": { - "type": "Identifier", - "payload": "alt" - } - }, - { - "name": "title", - "value": { - "type": "Identifier", - "payload": "title" - } - } - ], - "children": [], - "isSelfClosing": true - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/Component/Component.tokens.json b/test/Integration/Examples/Component/Component.tokens.json deleted file mode 100644 index 5f6e33ab..00000000 --- a/test/Integration/Examples/Component/Component.tokens.json +++ /dev/null @@ -1,222 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Image" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "src" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "alt" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "title" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "img" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "src" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "src" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "alt" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "alt" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "title" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "title" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_SELF_CLOSE", - "value": "/>" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] \ No newline at end of file diff --git a/test/Integration/Examples/ComponentEmpty/ComponentEmpty.tokens.json b/test/Integration/Examples/ComponentEmpty/ComponentEmpty.tokens.json deleted file mode 100644 index 977cc0a3..00000000 --- a/test/Integration/Examples/ComponentEmpty/ComponentEmpty.tokens.json +++ /dev/null @@ -1,58 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Empty" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING_QUOTED", - "value": "" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/ComponentWithKeywords/ComponentWithKeywords.ast.json b/test/Integration/Examples/ComponentWithKeywords/ComponentWithKeywords.ast.json deleted file mode 100644 index 42e92b84..00000000 --- a/test/Integration/Examples/ComponentWithKeywords/ComponentWithKeywords.ast.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "ComponentWithKeywords", - "propertyDeclarations": [], - "returnExpression": { - "type": "TagNode", - "payload": { - "tagName": "div", - "attributes": [], - "children": [ - { - "type": "TextNode", - "payload": { - "value": "Keywords like import or export or component are allowed in here." - } - } - ], - "isSelfClosing": false - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/ComponentWithKeywords/ComponentWithKeywords.tokens.json b/test/Integration/Examples/ComponentWithKeywords/ComponentWithKeywords.tokens.json deleted file mode 100644 index d94f0049..00000000 --- a/test/Integration/Examples/ComponentWithKeywords/ComponentWithKeywords.tokens.json +++ /dev/null @@ -1,186 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ComponentWithKeywords" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "div" - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Keywords" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "like" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "import" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "or" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "or" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "component" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "are" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "allowed" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "in" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "here." - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/ComponentWithNesting/ComponentWithNesting.ast.json b/test/Integration/Examples/ComponentWithNesting/ComponentWithNesting.ast.json deleted file mode 100644 index 5b27badd..00000000 --- a/test/Integration/Examples/ComponentWithNesting/ComponentWithNesting.ast.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "TextWithImage", - "propertyDeclarations": [ - { - "name": "src", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "alt", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "title", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "text", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - } - ], - "returnExpression": { - "type": "TagNode", - "payload": { - "tagName": "div", - "attributes": [ - { - "name": "class", - "value": { - "type": "StringLiteralNode", - "payload": "text-with-image" - } - } - ], - "children": [ - { - "type": "TagNode", - "payload": { - "tagName": "img", - "attributes": [ - { - "name": "class", - "value": { - "type": "StringLiteralNode", - "payload": "image" - } - }, - { - "name": "src", - "value": { - "type": "Identifier", - "payload": "src" - } - }, - { - "name": "alt", - "value": { - "type": "Identifier", - "payload": "alt" - } - }, - { - "name": "title", - "value": { - "type": "Identifier", - "payload": "title" - } - } - ], - "children": [], - "isSelfClosing": true - } - }, - { - "type": "TagNode", - "payload": { - "tagName": "p", - "attributes": [ - { - "name": "class", - "value": { - "type": "StringLiteralNode", - "payload": "text" - } - } - ], - "children": [ - { - "type": "Identifier", - "payload": "text" - } - ], - "isSelfClosing": false - } - } - ], - "isSelfClosing": false - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/ComponentWithNesting/ComponentWithNesting.tokens.json b/test/Integration/Examples/ComponentWithNesting/ComponentWithNesting.tokens.json deleted file mode 100644 index ce21e02f..00000000 --- a/test/Integration/Examples/ComponentWithNesting/ComponentWithNesting.tokens.json +++ /dev/null @@ -1,394 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "TextWithImage" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "src" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "alt" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "title" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "text" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "div" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "class" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "STRING_QUOTED", - "value": "text-with-image" - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "img" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "class" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "STRING_QUOTED", - "value": "image" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "src" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "src" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "alt" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "alt" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "title" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "title" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_SELF_CLOSE", - "value": "/>" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "p" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "class" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "STRING_QUOTED", - "value": "text" - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "text" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/Enum/Enum.ast.json b/test/Integration/Examples/Enum/Enum.ast.json deleted file mode 100644 index e41fea27..00000000 --- a/test/Integration/Examples/Enum/Enum.ast.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "EnumDeclarationNode", - "payload": { - "enumName": "DayOfWeek", - "memberDeclarations": { - "MONDAY": { - "name": "MONDAY", - "value": null - }, - "TUESDAY": { - "name": "TUESDAY", - "value": null - }, - "WEDNESDAY": { - "name": "WEDNESDAY", - "value": null - }, - "THURSDAY": { - "name": "THURSDAY", - "value": null - }, - "FRIDAY": { - "name": "FRIDAY", - "value": null - }, - "SATURDAY": { - "name": "SATURDAY", - "value": null - }, - "SUNDAY": { - "name": "SUNDAY", - "value": null - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/Enum/Enum.tokens.json b/test/Integration/Examples/Enum/Enum.tokens.json deleted file mode 100644 index c08c9f24..00000000 --- a/test/Integration/Examples/Enum/Enum.tokens.json +++ /dev/null @@ -1,126 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_ENUM", - "value": "enum" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "DayOfWeek" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "MONDAY" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "TUESDAY" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "WEDNESDAY" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "THURSDAY" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "FRIDAY" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "SATURDAY" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "SUNDAY" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/EnumWithNumberValue/EnumWithNumberValue.ast.json b/test/Integration/Examples/EnumWithNumberValue/EnumWithNumberValue.ast.json deleted file mode 100644 index 8633cd3d..00000000 --- a/test/Integration/Examples/EnumWithNumberValue/EnumWithNumberValue.ast.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "EnumDeclarationNode", - "payload": { - "enumName": "TrafficLight", - "memberDeclarations": { - "RED": { - "name": "RED", - "value": { - "type": "IntegerLiteralNode", - "payload": { - "value": "1", - "format": "DECIMAL" - } - } - }, - "YELLOW": { - "name": "YELLOW", - "value": { - "type": "IntegerLiteralNode", - "payload": { - "value": "2", - "format": "DECIMAL" - } - } - }, - "GREEN": { - "name": "GREEN", - "value": { - "type": "IntegerLiteralNode", - "payload": { - "value": "3", - "format": "DECIMAL" - } - } - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/EnumWithNumberValue/EnumWithNumberValue.tokens.json b/test/Integration/Examples/EnumWithNumberValue/EnumWithNumberValue.tokens.json deleted file mode 100644 index 808397a6..00000000 --- a/test/Integration/Examples/EnumWithNumberValue/EnumWithNumberValue.tokens.json +++ /dev/null @@ -1,114 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_ENUM", - "value": "enum" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "TrafficLight" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "RED" - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "NUMBER_DECIMAL", - "value": "1" - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "YELLOW" - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "NUMBER_DECIMAL", - "value": "2" - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "GREEN" - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "NUMBER_DECIMAL", - "value": "3" - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/EnumWithStringValue/EnumWithStringValue.ast.json b/test/Integration/Examples/EnumWithStringValue/EnumWithStringValue.ast.json deleted file mode 100644 index 70fcfabd..00000000 --- a/test/Integration/Examples/EnumWithStringValue/EnumWithStringValue.ast.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "EnumDeclarationNode", - "payload": { - "enumName": "ImageLoadingMethod", - "memberDeclarations": { - "EAGER": { - "name": "EAGER", - "value": { - "type": "StringLiteralNode", - "payload": "eager" - } - }, - "LAZY": { - "name": "LAZY", - "value": { - "type": "StringLiteralNode", - "payload": "lazy" - } - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/EnumWithStringValue/EnumWithStringValue.tokens.json b/test/Integration/Examples/EnumWithStringValue/EnumWithStringValue.tokens.json deleted file mode 100644 index c5462f1f..00000000 --- a/test/Integration/Examples/EnumWithStringValue/EnumWithStringValue.tokens.json +++ /dev/null @@ -1,90 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_ENUM", - "value": "enum" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ImageLoadingMethod" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "EAGER" - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "STRING_QUOTED", - "value": "eager" - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "LAZY" - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "STRING_QUOTED", - "value": "lazy" - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/Expression/Expression.ast.json b/test/Integration/Examples/Expression/Expression.ast.json deleted file mode 100644 index a2055f1d..00000000 --- a/test/Integration/Examples/Expression/Expression.ast.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "Expression", - "propertyDeclarations": [ - { - "name": "a", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "number", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "b", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "number", - "isArray": false, - "isOptional": false - } - } - } - ], - "returnExpression": { - "type": "TernaryOperationNode", - "payload": { - "condition": { - "type": "BinaryOperationNode", - "payload": { - "operator": "LESS_THAN_OR_EQUAL", - "operands": [ - { - "type": "Identifier", - "payload": "a" - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "120", - "format": "DECIMAL" - } - } - ] - } - }, - "true": { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "Identifier", - "payload": "b" - }, - { - "type": "Identifier", - "payload": "a" - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "17", - "format": "DECIMAL" - } - } - ] - } - }, - "false": { - "type": "BinaryOperationNode", - "payload": { - "operator": "AND", - "operands": [ - { - "type": "Identifier", - "payload": "b" - }, - { - "type": "Identifier", - "payload": "a" - } - ] - } - } - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/Expression/Expression.tokens.json b/test/Integration/Examples/Expression/Expression.tokens.json deleted file mode 100644 index 0ff447a1..00000000 --- a/test/Integration/Examples/Expression/Expression.tokens.json +++ /dev/null @@ -1,218 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Expression" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "a" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "number" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "b" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "number" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "a" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COMPARATOR_LESS_THAN_OR_EQUAL", - "value": "<=" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "120" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "QUESTIONMARK", - "value": "?" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "b" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "a" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "17" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "b" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_AND", - "value": "&&" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "a" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/ImportExport/ImportExport.ast.json b/test/Integration/Examples/ImportExport/ImportExport.ast.json deleted file mode 100644 index 26289c46..00000000 --- a/test/Integration/Examples/ImportExport/ImportExport.ast.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": [ - { - "path": "../Struct/Struct.afx", - "name": { - "type": "Identifier", - "payload": "Link" - } - }, - { - "path": "./Button.afx", - "name": { - "type": "Identifier", - "payload": "Button" - } - } - ], - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "Card", - "propertyDeclarations": [ - { - "name": "title", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "link", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "Link", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "button", - "type": { - "type": "TypeReferenceNode", - "payload": { "name": "Button", "isArray": false, "isOptional": false } - } - } - ], - "returnExpression": { - "type": "TagNode", - "payload": { - "tagName": "div", - "attributes": [ - { - "name": "class", - "value": { "type": "StringLiteralNode", "payload": "card" } - } - ], - "children": [ - { - "type": "TagNode", - "payload": { - "tagName": "a", - "attributes": [ - { - "name": "href", - "value": { - "type": "AccessNode", - "payload": { - "root": { "type": "Identifier", "payload": "link" }, - "chain": [ - { - "accessType": "MANDATORY", - "accessor": { - "type": "Identifier", - "payload": "href" - } - } - ] - } - } - }, - { - "name": "target", - "value": { - "type": "AccessNode", - "payload": { - "root": { "type": "Identifier", "payload": "link" }, - "chain": [ - { - "accessType": "MANDATORY", - "accessor": { - "type": "Identifier", - "payload": "target" - } - } - ] - } - } - } - ], - "children": [{ "type": "Identifier", "payload": "title" }], - "isSelfClosing": false - } - }, - { "type": "Identifier", "payload": "button" } - ], - "isSelfClosing": false - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/ImportExport/ImportExport.tokens.json b/test/Integration/Examples/ImportExport/ImportExport.tokens.json deleted file mode 100644 index 303dd861..00000000 --- a/test/Integration/Examples/ImportExport/ImportExport.tokens.json +++ /dev/null @@ -1,109 +0,0 @@ -[ - { "type": "KEYWORD_FROM", "value": "from" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING_QUOTED", "value": "../Struct/Struct.afx" }, - { "type": "SPACE", "value": " " }, - { "type": "KEYWORD_IMPORT", "value": "import" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "Link" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "KEYWORD_FROM", "value": "from" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING_QUOTED", "value": "./Button.afx" }, - { "type": "SPACE", "value": " " }, - { "type": "KEYWORD_IMPORT", "value": "import" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "Button" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "KEYWORD_EXPORT", "value": "export" }, - { "type": "SPACE", "value": " " }, - { "type": "KEYWORD_COMPONENT", "value": "component" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "Card" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "title" }, - { "type": "COLON", "value": ":" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "string" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "link" }, - { "type": "COLON", "value": ":" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "Link" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "button" }, - { "type": "COLON", "value": ":" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "Button" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "KEYWORD_RETURN", "value": "return" }, - { "type": "SPACE", "value": " " }, - { "type": "TAG_START_OPENING", "value": "<" }, - { "type": "STRING", "value": "div" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "class" }, - { "type": "EQUALS", "value": "=" }, - { "type": "STRING_QUOTED", "value": "card" }, - { "type": "TAG_END", "value": ">" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "TAG_START_OPENING", "value": "<" }, - { "type": "STRING", "value": "a" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "href" }, - { "type": "EQUALS", "value": "=" }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "STRING", "value": "link" }, - { "type": "PERIOD", "value": "." }, - { "type": "STRING", "value": "href" }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "target" }, - { "type": "EQUALS", "value": "=" }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "STRING", "value": "link" }, - { "type": "PERIOD", "value": "." }, - { "type": "STRING", "value": "target" }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "TAG_END", "value": ">" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "STRING", "value": "title" }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "TAG_START_CLOSING", "value": "" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "STRING", "value": "button" }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "SPACE", "value": " " }, - { "type": "TAG_START_CLOSING", "value": "" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "END_OF_LINE", "value": "\n" } -] diff --git a/test/Integration/Examples/Match/Match.ast.json b/test/Integration/Examples/Match/Match.ast.json deleted file mode 100644 index bd3a1cb2..00000000 --- a/test/Integration/Examples/Match/Match.ast.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": [ - { - "path": "./ButtonType.afx", - "name": { - "type": "Identifier", - "payload": "ButtonType" - } - } - ], - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "Button", - "propertyDeclarations": [ - { - "name": "type", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "ButtonType", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "content", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "slot", - "isArray": false, - "isOptional": false - } - } - } - ], - "returnExpression": { - "type": "MatchNode", - "payload": { - "subject": { - "type": "Identifier", - "payload": "type" - }, - "arms": [ - { - "type": "MatchArmNode", - "payload": { - "left": [ - { - "type": "AccessNode", - "payload": { - "root": { - "type": "Identifier", - "payload": "ButtonType" - }, - "chain": [ - { - "accessType": "MANDATORY", - "accessor": { - "type": "Identifier", - "payload": "LINK" - } - } - ] - } - } - ], - "right": { - "type": "TagNode", - "payload": { - "tagName": "a", - "attributes": [ - { - "name": "class", - "value": { - "type": "StringLiteralNode", - "payload": "btn" - } - }, - { - "name": "href", - "value": { - "type": "StringLiteralNode", - "payload": "#" - } - } - ], - "children": [ - { - "type": "Identifier", - "payload": "content" - } - ], - "isSelfClosing": false - } - } - } - }, - { - "type": "MatchArmNode", - "payload": { - "left": [ - { - "type": "AccessNode", - "payload": { - "root": { - "type": "Identifier", - "payload": "ButtonType" - }, - "chain": [ - { - "accessType": "MANDATORY", - "accessor": { - "type": "Identifier", - "payload": "BUTTON" - } - } - ] - } - }, - { - "type": "AccessNode", - "payload": { - "root": { - "type": "Identifier", - "payload": "ButtonType" - }, - "chain": [ - { - "accessType": "MANDATORY", - "accessor": { - "type": "Identifier", - "payload": "SUBMIT" - } - } - ] - } - } - ], - "right": { - "type": "TagNode", - "payload": { - "tagName": "button", - "attributes": [ - { - "name": "class", - "value": { - "type": "StringLiteralNode", - "payload": "btn" - } - }, - { - "name": "type", - "value": { - "type": "MatchNode", - "payload": { - "subject": { - "type": "Identifier", - "payload": "type" - }, - "arms": [ - { - "type": "MatchArmNode", - "payload": { - "left": [ - { - "type": "AccessNode", - "payload": { - "root": { - "type": "Identifier", - "payload": "ButtonType" - }, - "chain": [ - { - "accessType": "MANDATORY", - "accessor": { - "type": "Identifier", - "payload": "SUBMIT" - } - } - ] - } - } - ], - "right": { - "type": "StringLiteralNode", - "payload": "submit" - } - } - }, - { - "type": "MatchArmNode", - "payload": { - "left": null, - "right": { - "type": "StringLiteralNode", - "payload": "button" - } - } - } - ] - } - } - } - ], - "children": [ - { - "type": "Identifier", - "payload": "content" - } - ], - "isSelfClosing": false - } - } - } - }, - { - "type": "MatchArmNode", - "payload": { - "left": [ - { - "type": "AccessNode", - "payload": { - "root": { - "type": "Identifier", - "payload": "ButtonType" - }, - "chain": [ - { - "accessType": "MANDATORY", - "accessor": { - "type": "Identifier", - "payload": "NONE" - } - } - ] - } - } - ], - "right": { - "type": "TagNode", - "payload": { - "tagName": "div", - "attributes": [ - { - "name": "class", - "value": { - "type": "StringLiteralNode", - "payload": "btn" - } - } - ], - "children": [ - { - "type": "Identifier", - "payload": "content" - } - ], - "isSelfClosing": false - } - } - } - } - ] - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/Match/Match.tokens.json b/test/Integration/Examples/Match/Match.tokens.json deleted file mode 100644 index 49d15d46..00000000 --- a/test/Integration/Examples/Match/Match.tokens.json +++ /dev/null @@ -1,683 +0,0 @@ -[ - { "type": "KEYWORD_FROM", "value": "from" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING_QUOTED", "value": "./ButtonType.afx" }, - { "type": "SPACE", "value": " " }, - { "type": "KEYWORD_IMPORT", "value": "import" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_OPEN", "value": "{" }, - { "type": "SPACE", "value": " " }, - { "type": "STRING", "value": "ButtonType" }, - { "type": "SPACE", "value": " " }, - { "type": "BRACKET_CURLY_CLOSE", "value": "}" }, - { "type": "END_OF_LINE", "value": "\n" }, - { "type": "END_OF_LINE", "value": "\n" }, - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Button" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "type" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ButtonType" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "content" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "slot" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_MATCH", - "value": "match" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "STRING", - "value": "type" - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ButtonType" - }, - { - "type": "PERIOD", - "value": "." - }, - { - "type": "STRING", - "value": "LINK" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "ARROW_SINGLE", - "value": "->" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "a" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "class" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "STRING_QUOTED", - "value": "btn" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "href" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "STRING_QUOTED", - "value": "#" - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "content" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ButtonType" - }, - { - "type": "PERIOD", - "value": "." - }, - { - "type": "STRING", - "value": "BUTTON" - }, - { - "type": "COMMA", - "value": "," - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ButtonType" - }, - { - "type": "PERIOD", - "value": "." - }, - { - "type": "STRING", - "value": "SUBMIT" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "ARROW_SINGLE", - "value": "->" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "button" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "class" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "STRING_QUOTED", - "value": "btn" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "type" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "KEYWORD_MATCH", - "value": "match" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "STRING", - "value": "type" - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ButtonType" - }, - { - "type": "PERIOD", - "value": "." - }, - { - "type": "STRING", - "value": "SUBMIT" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "ARROW_SINGLE", - "value": "->" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING_QUOTED", - "value": "submit" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_DEFAULT", - "value": "default" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "ARROW_SINGLE", - "value": "->" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING_QUOTED", - "value": "button" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "content" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "ButtonType" - }, - { - "type": "PERIOD", - "value": "." - }, - { - "type": "STRING", - "value": "NONE" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "ARROW_SINGLE", - "value": "->" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_OPEN", - "value": "(" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "div" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "class" - }, - { - "type": "EQUALS", - "value": "=" - }, - { - "type": "STRING_QUOTED", - "value": "btn" - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "content" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_ROUND_CLOSE", - "value": ")" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/Numbers/Numbers.ast.json b/test/Integration/Examples/Numbers/Numbers.ast.json deleted file mode 100644 index 6ba7112c..00000000 --- a/test/Integration/Examples/Numbers/Numbers.ast.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": [], - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "Numbers", - "propertyDeclarations": [], - "returnExpression": { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "BinaryOperationNode", - "payload": { - "operator": "OR", - "operands": [ - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0", - "format": "DECIMAL" - } - }, - - { - "type": "IntegerLiteralNode", - "payload": { - "value": "1234567890", - "format": "DECIMAL" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "42", - "format": "DECIMAL" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0b10000000000000000000000000000000", - "format": "BINARY" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0b01111111100000000000000000000000", - "format": "BINARY" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0B00000000011111111111111111111111", - "format": "BINARY" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0o755", - "format": "OCTAL" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0o644", - "format": "OCTAL" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0xFFFFFFFFFFFFFFFFF", - "format": "HEXADECIMAL" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0x123456789ABCDEF", - "format": "HEXADECIMAL" - } - } - ] - } - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "0xA", - "format": "HEXADECIMAL" - } - } - ] - } - } - } - } - ] - } -} diff --git a/test/Integration/Examples/Numbers/Numbers.tokens.json b/test/Integration/Examples/Numbers/Numbers.tokens.json deleted file mode 100644 index c36ec52e..00000000 --- a/test/Integration/Examples/Numbers/Numbers.tokens.json +++ /dev/null @@ -1,326 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Numbers" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COMMENT", - "value": "# Decimal" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "0" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "1234567890" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "42" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COMMENT", - "value": "# Binary" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_BINARY", - "value": "0b10000000000000000000000000000000" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_BINARY", - "value": "0b01111111100000000000000000000000" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_BINARY", - "value": "0B00000000011111111111111111111111" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COMMENT", - "value": "# Octal" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_OCTAL", - "value": "0o755" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_OCTAL", - "value": "0o644" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COMMENT", - "value": "# Hexadecimal" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_HEXADECIMAL", - "value": "0xFFFFFFFFFFFFFFFFF" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_HEXADECIMAL", - "value": "0x123456789ABCDEF" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "OPERATOR_BOOLEAN_OR", - "value": "||" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_HEXADECIMAL", - "value": "0xA" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/Struct/Struct.ast.json b/test/Integration/Examples/Struct/Struct.ast.json deleted file mode 100644 index dfe87bdc..00000000 --- a/test/Integration/Examples/Struct/Struct.ast.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "Struct", - "payload": { - "structName": "Link", - "propertyDeclarations": [ - { - "name": "href", - "type": { - "type": "TypeReferenceNode", - "payload": { "name": "string", "isArray": false, "isOptional": false } - } - }, - { - "name": "target", - "type": { - "type": "TypeReferenceNode", - "payload": { "name": "string", "isArray": false, "isOptional": false } - } - } - ] - } - } - ] - } -} diff --git a/test/Integration/Examples/Struct/Struct.tokens.json b/test/Integration/Examples/Struct/Struct.tokens.json deleted file mode 100644 index 8fabd0f5..00000000 --- a/test/Integration/Examples/Struct/Struct.tokens.json +++ /dev/null @@ -1,90 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_STRUCT", - "value": "struct" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Link" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "href" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "target" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/StructWithOptionals/StructWithOptionals.ast.json b/test/Integration/Examples/StructWithOptionals/StructWithOptionals.ast.json deleted file mode 100644 index bee5e1c2..00000000 --- a/test/Integration/Examples/StructWithOptionals/StructWithOptionals.ast.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "Struct", - "payload": { - "structName": "Image", - "propertyDeclarations": [ - { - "name": "src", - "type": { - "type": "TypeReferenceNode", - "payload": { "name": "string", "isArray": false, "isOptional": false } - } - }, - { - "name": "alt", - "type": { - "type": "TypeReferenceNode", - "payload": { "name": "string", "isArray": false, "isOptional": false } - } - }, - { - "name": "title", - "type": { - "type": "TypeReferenceNode", - "payload": { "name": "string", "isArray": false, "isOptional": true } - } - } - ] - } - } - ] - } -} diff --git a/test/Integration/Examples/StructWithOptionals/StructWithOptionals.tokens.json b/test/Integration/Examples/StructWithOptionals/StructWithOptionals.tokens.json deleted file mode 100644 index 33d7292d..00000000 --- a/test/Integration/Examples/StructWithOptionals/StructWithOptionals.tokens.json +++ /dev/null @@ -1,118 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_STRUCT", - "value": "struct" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "Image" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "src" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "alt" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "title" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "QUESTIONMARK", - "value": "?" - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/Examples/TemplateLiteral/TemplateLiteral.ast.json b/test/Integration/Examples/TemplateLiteral/TemplateLiteral.ast.json deleted file mode 100644 index b477685b..00000000 --- a/test/Integration/Examples/TemplateLiteral/TemplateLiteral.ast.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "type": "ModuleNode", - "payload": { - "imports": {}, - "exports": [ - { - "type": "ComponentDeclarationNode", - "payload": { - "componentName": "TemplateLiteral", - "propertyDeclarations": [ - { - "name": "expression", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "string", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "isActive", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "boolean", - "isArray": false, - "isOptional": false - } - } - }, - { - "name": "someNumber", - "type": { - "type": "TypeReferenceNode", - "payload": { - "name": "number", - "isArray": false, - "isOptional": false - } - } - } - ], - "returnExpression": { - "type": "TemplateLiteralNode", - "payload": [ - { - "type": "StringLiteralNode", - "payload": "A template literal may contain " - }, - { - "type": "Identifier", - "payload": "expression" - }, - { - "type": "StringLiteralNode", - "payload": "s.\n\n It can span multiple lines.\n\n Interpolated Expressions can be arbitrarily complex:\n " - }, - { - "type": "TernaryOperationNode", - "payload": { - "condition": { - "type": "Identifier", - "payload": "isActive" - }, - "true": { - "type": "IntegerLiteralNode", - "payload": { "value": "27", "format": "DECIMAL" } - }, - "false": { - "type": "IntegerLiteralNode", - "payload": { "value": "17", "format": "DECIMAL" } - } - } - }, - { - "type": "StringLiteralNode", - "payload": "\n\n They can also contain other template literals:\n " - }, - { - "type": "TernaryOperationNode", - "payload": { - "condition": { - "type": "Identifier", - "payload": "isActive" - }, - "true": { - "type": "TemplateLiteralNode", - "payload": [ - { "type": "StringLiteralNode", "payload": "Is 27? " }, - { - "type": "TernaryOperationNode", - "payload": { - "condition": { - "type": "BinaryOperationNode", - "payload": { - "operator": "EQUAL", - "operands": [ - { - "type": "Identifier", - "payload": "someNumber" - }, - { - "type": "IntegerLiteralNode", - "payload": { - "value": "27", - "format": "DECIMAL" - } - } - ] - } - }, - "true": { - "type": "StringLiteralNode", - "payload": "yes" - }, - "false": { - "type": "StringLiteralNode", - "payload": "no" - } - } - } - ] - }, - "false": { - "type": "TemplateLiteralNode", - "payload": [ - { "type": "StringLiteralNode", "payload": "Number is " }, - { - "type": "IntegerLiteralNode", - "payload": { "value": "27", "format": "DECIMAL" } - } - ] - } - } - }, - { - "type": "StringLiteralNode", - "payload": "\n\n Even markup:\n " - }, - { - "type": "TagNode", - "payload": { - "tagName": "header", - "attributes": [], - "children": [ - { - "type": "TagNode", - "payload": { - "tagName": "h1", - "attributes": [], - "children": [ - { - "type": "TextNode", - "payload": { "value": "Number is " } - }, - { - "type": "Identifier", - "payload": "someNumber" - } - ], - "isSelfClosing": false - } - } - ], - "isSelfClosing": false - } - }, - { "type": "StringLiteralNode", "payload": "\n " } - ] - } - } - } - ] - } -} diff --git a/test/Integration/Examples/TemplateLiteral/TemplateLiteral.tokens.json b/test/Integration/Examples/TemplateLiteral/TemplateLiteral.tokens.json deleted file mode 100644 index a2d2c635..00000000 --- a/test/Integration/Examples/TemplateLiteral/TemplateLiteral.tokens.json +++ /dev/null @@ -1,490 +0,0 @@ -[ - { - "type": "KEYWORD_EXPORT", - "value": "export" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_COMPONENT", - "value": "component" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "TemplateLiteral" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "expression" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "string" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "isActive" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "boolean" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "someNumber" - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "number" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "KEYWORD_RETURN", - "value": "return" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TEMPLATE_LITERAL_START", - "value": "`" - }, - { - "type": "STRING_QUOTED", - "value": "A template literal may contain " - }, - { - "type": "DOLLAR", - "value": "$" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "expression" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "STRING_QUOTED", - "value": "s.\n\n It can span multiple lines.\n\n Interpolated Expressions can be arbitrarily complex:\n " - }, - { - "type": "DOLLAR", - "value": "$" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "isActive" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "QUESTIONMARK", - "value": "?" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "27" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "17" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "STRING_QUOTED", - "value": "\n\n They can also contain other template literals:\n " - }, - { - "type": "DOLLAR", - "value": "$" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "isActive" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "QUESTIONMARK", - "value": "?" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TEMPLATE_LITERAL_START", - "value": "`" - }, - { - "type": "STRING_QUOTED", - "value": "Is 27? " - }, - { - "type": "DOLLAR", - "value": "$" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "someNumber" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COMPARATOR_EQUAL", - "value": "===" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "NUMBER_DECIMAL", - "value": "27" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "QUESTIONMARK", - "value": "?" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING_QUOTED", - "value": "yes" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING_QUOTED", - "value": "no" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "TEMPLATE_LITERAL_END", - "value": "`" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "COLON", - "value": ":" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TEMPLATE_LITERAL_START", - "value": "`" - }, - { - "type": "STRING_QUOTED", - "value": "Number is " - }, - { - "type": "DOLLAR", - "value": "$" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "NUMBER_DECIMAL", - "value": "27" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "TEMPLATE_LITERAL_END", - "value": "`" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "STRING_QUOTED", - "value": "\n\n Even markup:\n " - }, - { - "type": "DOLLAR", - "value": "$" - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "header" - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_OPENING", - "value": "<" - }, - { - "type": "STRING", - "value": "h1" - }, - { - "type": "TAG_END", - "value": ">" - }, - { - "type": "STRING", - "value": "Number" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "STRING", - "value": "is" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_OPEN", - "value": "{" - }, - { - "type": "STRING", - "value": "someNumber" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "TAG_START_CLOSING", - "value": "" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "SPACE", - "value": " " - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "STRING_QUOTED", - "value": "\n " - }, - { - "type": "TEMPLATE_LITERAL_END", - "value": "`" - }, - { - "type": "END_OF_LINE", - "value": "\n" - }, - { - "type": "BRACKET_CURLY_CLOSE", - "value": "}" - }, - { - "type": "END_OF_LINE", - "value": "\n" - } -] diff --git a/test/Integration/ParserIntegrationTest.php b/test/Integration/ParserIntegrationTest.php deleted file mode 100644 index 71c10271..00000000 --- a/test/Integration/ParserIntegrationTest.php +++ /dev/null @@ -1,77 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Test\Integration; - -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PHPUnit\Framework\TestCase; - -final class ParserIntegrationTest extends TestCase -{ - /** - * @return array - */ - public static function astExamples(): array - { - return [ - 'Comment' => ["Comment"], - 'Component' => ["Component"], - 'ComponentWithKeywords' => ["ComponentWithKeywords"], - 'ComponentWithNesting' => ["ComponentWithNesting"], - 'Enum' => ["Enum"], - 'EnumWithStringValue' => ["EnumWithStringValue"], - 'EnumWithNumberValue' => ["EnumWithNumberValue"], - 'Expression' => ["Expression"], - 'ImportExport' => ["ImportExport"], - 'Match' => ["Match"], - 'Numbers' => ["Numbers"], - 'Struct' => ["Struct"], - 'StructWithOptionals' => ["StructWithOptionals"], - 'TemplateLiteral' => ["TemplateLiteral"], - ]; - } - - /** - * @dataProvider astExamples - * @test - * @small - * @param string $example - * @return void - */ - public function testParser(string $example): void - { - $source = Source::fromFile(__DIR__ . '/Examples/' . $example . '/' . $example . '.afx'); - $tokenizer = Tokenizer::fromSource($source); - $astAsJsonString = file_get_contents(__DIR__ . '/Examples/' . $example . '/' . $example . '.ast.json'); - assert($astAsJsonString !== false); - - $expected = json_decode($astAsJsonString, true); - - $module = ModuleNode::fromTokens($tokenizer->getIterator()); - $moduleAsJson = json_encode($module); - assert($moduleAsJson !== false); - - $this->assertEquals($expected, json_decode($moduleAsJson, true)); - } -} diff --git a/test/Integration/PhpTranspilerIntegrationTest.php b/test/Integration/PhpTranspilerIntegrationTest.php index fe0760bb..12346c4e 100644 --- a/test/Integration/PhpTranspilerIntegrationTest.php +++ b/test/Integration/PhpTranspilerIntegrationTest.php @@ -22,8 +22,9 @@ namespace PackageFactory\ComponentEngine\Test\Integration; +use PackageFactory\ComponentEngine\Language\Parser\Module\ModuleParser; use PackageFactory\ComponentEngine\Module\Loader\ModuleFile\ModuleFileLoader; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; +use PackageFactory\ComponentEngine\Parser\Source\Path; use PackageFactory\ComponentEngine\Parser\Source\Source; use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Module\ModuleTranspiler; @@ -64,15 +65,20 @@ public static function transpilerExamples(): array */ public function testTranspiler(string $example): void { - $source = Source::fromFile(__DIR__ . '/Examples/' . $example . '/' . $example . '.afx'); + $sourcePath = Path::fromString(__DIR__ . '/Examples/' . $example . '/' . $example . '.afx'); + $source = Source::fromFile($sourcePath->value); $tokenizer = Tokenizer::fromSource($source); - $module = ModuleNode::fromTokens($tokenizer->getIterator()); + $tokens = $tokenizer->getIterator(); + + $module = ModuleParser::singleton()->parse($tokens); $expected = file_get_contents(__DIR__ . '/Examples/' . $example . '/' . $example . '.php'); $transpiler = new ModuleTranspiler( - loader: new ModuleFileLoader(), - globalScope: GlobalScope::get(), + loader: new ModuleFileLoader( + sourcePath: $sourcePath + ), + globalScope: GlobalScope::singleton(), strategy: new ModuleTestStrategy() ); diff --git a/test/Integration/TokenizerIntegrationTest.php b/test/Integration/TokenizerIntegrationTest.php deleted file mode 100644 index 9de5a5f3..00000000 --- a/test/Integration/TokenizerIntegrationTest.php +++ /dev/null @@ -1,82 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Test\Integration; - -use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; -use PackageFactory\ComponentEngine\Parser\Source\Source; -use PHPUnit\Framework\TestCase; - -final class TokenizerIntegrationTest extends TestCase -{ - /** - * @return array - */ - public static function tokenizationExamples(): array - { - return [ - 'Comment' => ["Comment"], - 'Component' => ["Component"], - 'ComponentEmpty' => ["ComponentEmpty"], - 'ComponentWithKeywords' => ["ComponentWithKeywords"], - 'ComponentWithNesting' => ["ComponentWithNesting"], - 'Enum' => ["Enum"], - 'EnumWithStringValue' => ["EnumWithStringValue"], - 'EnumWithNumberValue' => ["EnumWithNumberValue"], - 'Expression' => ["Expression"], - 'ImportExport' => ["ImportExport"], - 'Match' => ["Match"], - 'Numbers' => ["Numbers"], - 'Struct' => ["Struct"], - 'StructWithOptionals' => ["StructWithOptionals"], - 'TemplateLiteral' => ["TemplateLiteral"], - ]; - } - - /** - * @dataProvider tokenizationExamples - * @test - * @small - * @param string $example - * @return void - */ - public function testTokenizer(string $example): void - { - $source = Source::fromFile(__DIR__ . '/Examples/' . $example . '/' . $example . '.afx'); - $tokenStreamAsJsonString = file_get_contents(__DIR__ . '/Examples/' . $example . '/' . $example . '.tokens.json'); - assert($tokenStreamAsJsonString !== false); - - $tokenizer = Tokenizer::fromSource($source); - $expected = json_decode($tokenStreamAsJsonString); - - $index = 0; - foreach ($tokenizer as $token) { - if (!isset($expected[$index])) { - $tokenType = $token->type; - $this->fail("Unfinished expectation at $index [$tokenType->name]($token->value)"); - } - $this->assertEquals($expected[$index]->type, $token->type->name, "Type mismatch at index $index ($token->value)"); - $this->assertEquals($expected[$index]->value, $token->value, "Value mismatch at index $index"); - $index++; - } - } -} diff --git a/test/Unit/Domain/AttributeName/AttributeNameTest.php b/test/Unit/Domain/AttributeName/AttributeNameTest.php new file mode 100644 index 00000000..d7f11554 --- /dev/null +++ b/test/Unit/Domain/AttributeName/AttributeNameTest.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\AttributeName; + +use PackageFactory\ComponentEngine\Domain\AttributeName\AttributeName; +use PHPUnit\Framework\TestCase; + +final class AttributeNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(AttributeName::from('foo'), AttributeName::from('foo')); + $this->assertSame(AttributeName::from('bar'), AttributeName::from('bar')); + $this->assertSame(AttributeName::from('data-baz'), AttributeName::from('data-baz')); + + $this->assertNotSame(AttributeName::from('foo'), AttributeName::from('bar')); + } +} diff --git a/test/Unit/Domain/ComponentName/ComponentNameTest.php b/test/Unit/Domain/ComponentName/ComponentNameTest.php new file mode 100644 index 00000000..30eb1fd3 --- /dev/null +++ b/test/Unit/Domain/ComponentName/ComponentNameTest.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\ComponentName; + +use PackageFactory\ComponentEngine\Domain\ComponentName\ComponentName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PHPUnit\Framework\TestCase; + +final class ComponentNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(ComponentName::from('Foo'), ComponentName::from('Foo')); + $this->assertSame(ComponentName::from('Bar'), ComponentName::from('Bar')); + $this->assertSame(ComponentName::from('FooBar'), ComponentName::from('FooBar')); + } + + /** + * @test + */ + public function convertsToTypeName(): void + { + $this->assertEquals( + TypeName::from('Foo'), + ComponentName::from('Foo')->toTypeName() + ); + } +} diff --git a/test/Unit/Domain/EnumMemberName/EnumMemberNameTest.php b/test/Unit/Domain/EnumMemberName/EnumMemberNameTest.php new file mode 100644 index 00000000..515aacb6 --- /dev/null +++ b/test/Unit/Domain/EnumMemberName/EnumMemberNameTest.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\EnumMemberName; + +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PHPUnit\Framework\TestCase; + +final class EnumMemberNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(EnumMemberName::from('foo'), EnumMemberName::from('foo')); + $this->assertSame(EnumMemberName::from('bar'), EnumMemberName::from('bar')); + $this->assertSame(EnumMemberName::from('foobar'), EnumMemberName::from('foobar')); + + $this->assertNotSame(EnumMemberName::from('foo'), EnumMemberName::from('bar')); + } +} diff --git a/test/Unit/Domain/EnumName/EnumNameTest.php b/test/Unit/Domain/EnumName/EnumNameTest.php new file mode 100644 index 00000000..d4ef70ca --- /dev/null +++ b/test/Unit/Domain/EnumName/EnumNameTest.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\EnumName; + +use PackageFactory\ComponentEngine\Domain\EnumName\EnumName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PHPUnit\Framework\TestCase; + +final class EnumNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(EnumName::from('Foo'), EnumName::from('Foo')); + $this->assertSame(EnumName::from('Bar'), EnumName::from('Bar')); + $this->assertSame(EnumName::from('FooBar'), EnumName::from('FooBar')); + + $this->assertNotSame(EnumName::from('Foo'), EnumName::from('Bar')); + } + + /** + * @test + */ + public function convertsToTypeName(): void + { + $this->assertEquals( + TypeName::from('Foo'), + EnumName::from('Foo')->toTypeName() + ); + } +} diff --git a/test/Unit/Domain/PropertyName/PropertyNameTest.php b/test/Unit/Domain/PropertyName/PropertyNameTest.php new file mode 100644 index 00000000..cb8040e6 --- /dev/null +++ b/test/Unit/Domain/PropertyName/PropertyNameTest.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\PropertyName; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PHPUnit\Framework\TestCase; + +final class PropertyNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(PropertyName::from('foo'), PropertyName::from('foo')); + $this->assertSame(PropertyName::from('bar'), PropertyName::from('bar')); + $this->assertSame(PropertyName::from('foobar'), PropertyName::from('foobar')); + + $this->assertNotSame(PropertyName::from('foo'), PropertyName::from('bar')); + } +} diff --git a/test/Unit/Domain/StructName/StructNameTest.php b/test/Unit/Domain/StructName/StructNameTest.php new file mode 100644 index 00000000..8c24b791 --- /dev/null +++ b/test/Unit/Domain/StructName/StructNameTest.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\StructName; + +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PHPUnit\Framework\TestCase; + +final class StructNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(StructName::from('Foo'), StructName::from('Foo')); + $this->assertSame(StructName::from('Bar'), StructName::from('Bar')); + $this->assertSame(StructName::from('FooBar'), StructName::from('FooBar')); + + $this->assertNotSame(StructName::from('Foo'), StructName::from('Bar')); + } + + /** + * @test + */ + public function convertsToTypeName(): void + { + $this->assertEquals( + TypeName::from('Foo'), + StructName::from('Foo')->toTypeName() + ); + } +} diff --git a/test/Unit/Domain/TagName/TagNameTest.php b/test/Unit/Domain/TagName/TagNameTest.php new file mode 100644 index 00000000..6da2faa6 --- /dev/null +++ b/test/Unit/Domain/TagName/TagNameTest.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\TagName; + +use PackageFactory\ComponentEngine\Domain\TagName\TagName; +use PHPUnit\Framework\TestCase; + +final class TagNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(TagName::from('div'), TagName::from('div')); + $this->assertSame(TagName::from('a'), TagName::from('a')); + $this->assertSame(TagName::from('vendor-component'), TagName::from('vendor-component')); + + $this->assertNotSame(TagName::from('pre'), TagName::from('table')); + } +} diff --git a/test/Unit/Domain/TypeName/TypeNameTest.php b/test/Unit/Domain/TypeName/TypeNameTest.php new file mode 100644 index 00000000..9ee597bc --- /dev/null +++ b/test/Unit/Domain/TypeName/TypeNameTest.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\TypeName; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PHPUnit\Framework\TestCase; + +final class TypeNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(TypeName::from('Foo'), TypeName::from('Foo')); + $this->assertSame(TypeName::from('Bar'), TypeName::from('Bar')); + $this->assertSame(TypeName::from('FooBar'), TypeName::from('FooBar')); + + $this->assertNotSame(TypeName::from('Foo'), TypeName::from('Bar')); + } + + /** + * @test + */ + public function convertsToVariableName(): void + { + $this->assertEquals( + VariableName::from('Foo'), + TypeName::from('Foo')->toVariableName() + ); + } +} diff --git a/test/Unit/Domain/TypeName/TypeNamesTest.php b/test/Unit/Domain/TypeName/TypeNamesTest.php new file mode 100644 index 00000000..3647d2be --- /dev/null +++ b/test/Unit/Domain/TypeName/TypeNamesTest.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\TypeName; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; +use PHPUnit\Framework\TestCase; + +final class TypeNamesTest extends TestCase +{ + /** + * @test + */ + public function providesDebugString(): void + { + $typeNames = new TypeNames( + TypeName::from('Foo'), + TypeName::from('Bar'), + TypeName::from('Baz') + ); + + $this->assertEquals( + 'Foo|Bar|Baz', + $typeNames->toDebugString() + ); + } +} diff --git a/test/Unit/Domain/VariableName/VariableNameTest.php b/test/Unit/Domain/VariableName/VariableNameTest.php new file mode 100644 index 00000000..1868b158 --- /dev/null +++ b/test/Unit/Domain/VariableName/VariableNameTest.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Domain\VariableName; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PHPUnit\Framework\TestCase; + +final class VariableNameTest extends TestCase +{ + /** + * @test + */ + public function isFlyweight(): void + { + $this->assertSame(VariableName::from('foo'), VariableName::from('foo')); + $this->assertSame(VariableName::from('bar'), VariableName::from('bar')); + $this->assertSame(VariableName::from('foobar'), VariableName::from('foobar')); + + $this->assertNotSame(VariableName::from('foo'), VariableName::from('bar')); + } +} diff --git a/test/Unit/Language/AST/Node/Import/ImportedNameNodesTest.php b/test/Unit/Language/AST/Node/Import/ImportedNameNodesTest.php new file mode 100644 index 00000000..84906c3d --- /dev/null +++ b/test/Unit/Language/AST/Node/Import/ImportedNameNodesTest.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\AST\Node\Import; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\InvalidImportedNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNodes; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PHPUnit\Framework\TestCase; + +final class ImportedNameNodesTest extends TestCase +{ + protected function createImportedNameNode(string $typeName): ImportedNameNode + { + return new ImportedNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: VariableName::from($typeName) + ); + } + + /** + * @test + */ + public function mustNotBeEmpty(): void + { + $this->expectExceptionObject( + InvalidImportedNameNodes::becauseTheyWereEmpty() + ); + + new ImportedNameNodes(); + } + + /** + * @test + */ + public function mustNotContainDuplicates(): void + { + $duplicate = new ImportedNameNode( + rangeInSource: Range::from( + new Position(1, 1), + new Position(1, 1) + ), + value: VariableName::from('Foo') + ); + + $this->expectExceptionObject( + InvalidImportedNameNodes::becauseTheyContainDuplicates( + duplicateImportedNameNode: $duplicate + ) + ); + + new ImportedNameNodes( + $this->createImportedNameNode('Foo'), + $this->createImportedNameNode('Bar'), + $duplicate, + $this->createImportedNameNode('Baz'), + ); + } +} diff --git a/test/Unit/Language/AST/Node/Match/MatchArmNodeTest.php b/test/Unit/Language/AST/Node/Match/MatchArmNodeTest.php new file mode 100644 index 00000000..fca24aba --- /dev/null +++ b/test/Unit/Language/AST/Node/Match/MatchArmNodeTest.php @@ -0,0 +1,92 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\AST\Node\Match; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PHPUnit\Framework\TestCase; + +final class MatchArmNodeTest extends TestCase +{ + /** + * @test + */ + public function matchArmWithEmptyLeftIsDefault(): void + { + $zeroRange = Range::from(new Position(0, 0), new Position(0, 0)); + $matchArmNode = new MatchArmNode( + rangeInSource: $zeroRange, + left: null, + right: new ExpressionNode( + rangeInSource: $zeroRange, + root: new ValueReferenceNode( + rangeInSource: $zeroRange, + name: VariableName::from('foo') + ) + ) + ); + + $this->assertTrue($matchArmNode->isDefault()); + } + + /** + * @test + */ + public function matchArmWithNonEmptyLeftIsNotDefault(): void + { + $zeroRange = Range::from(new Position(0, 0), new Position(0, 0)); + $matchArmNode = new MatchArmNode( + rangeInSource: $zeroRange, + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $zeroRange, + root: new ValueReferenceNode( + rangeInSource: $zeroRange, + name: VariableName::from('foo') + ) + ), + new ExpressionNode( + rangeInSource: $zeroRange, + root: new ValueReferenceNode( + rangeInSource: $zeroRange, + name: VariableName::from('bar') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $zeroRange, + root: new ValueReferenceNode( + rangeInSource: $zeroRange, + name: VariableName::from('baz') + ) + ) + ); + + $this->assertFalse($matchArmNode->isDefault()); + } +} diff --git a/test/Unit/Language/AST/Node/Match/MatchArmNodesTest.php b/test/Unit/Language/AST/Node/Match/MatchArmNodesTest.php new file mode 100644 index 00000000..b943db10 --- /dev/null +++ b/test/Unit/Language/AST/Node/Match/MatchArmNodesTest.php @@ -0,0 +1,119 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\AST\Node\Match; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\InvalidMatchArmNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PHPUnit\Framework\TestCase; + +final class MatchArmNodesTest extends TestCase +{ + /** + * @param string[] $left + * @param string $right + * @return MatchArmNode + */ + protected function createMatchArmNode(array $left, string $right): MatchArmNode + { + $zeroRange = Range::from(new Position(0, 0), new Position(0, 0)); + + return new MatchArmNode( + rangeInSource: $zeroRange, + left: new ExpressionNodes( + ...array_map( + static fn(string $name) => new ExpressionNode( + rangeInSource: $zeroRange, + root: new ValueReferenceNode( + rangeInSource: $zeroRange, + name: VariableName::from($name) + ) + ), + $left + ) + ), + right: new ExpressionNode( + rangeInSource: $zeroRange, + root: new ValueReferenceNode( + rangeInSource: $zeroRange, + name: VariableName::from($right) + ) + ) + ); + + } + protected function createDefaultMatchArmNode(string $right): MatchArmNode + { + $zeroRange = Range::from(new Position(0, 0), new Position(0, 0)); + + return new MatchArmNode( + rangeInSource: $zeroRange, + left: null, + right: new ExpressionNode( + rangeInSource: $zeroRange, + root: new ValueReferenceNode( + rangeInSource: $zeroRange, + name: VariableName::from($right) + ) + ) + ); + } + + /** + * @test + */ + public function mustNotBeEmpty(): void + { + $this->expectExceptionObject( + InvalidMatchArmNodes::becauseTheyWereEmpty() + ); + + new MatchArmNodes(); + } + + /** + * @test + */ + public function mustNotContainMultipleDefaultArms(): void + { + $secondDefaultMatchArmNode = $this->createDefaultMatchArmNode('bar'); + + $this->expectExceptionObject( + InvalidMatchArmNodes::becauseTheyContainMoreThanOneDefaultMatchArmNode( + secondDefaultMatchArmNode: $secondDefaultMatchArmNode + ) + ); + + new MatchArmNodes( + $this->createMatchArmNode(['a', 'b'], 'c'), + $this->createDefaultMatchArmNode('foo'), + $secondDefaultMatchArmNode + ); + } +} diff --git a/test/Unit/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNodesTest.php b/test/Unit/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNodesTest.php new file mode 100644 index 00000000..384104ad --- /dev/null +++ b/test/Unit/Language/AST/Node/PropertyDeclaration/PropertyDeclarationNodesTest.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\AST\Node\PropertyDeclaration; + +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; +use PHPUnit\Framework\TestCase; + +final class PropertyDeclarationNodesTest extends TestCase +{ + /** + * @test + */ + public function canTellIfItsEmpty(): void + { + $propertyDeclarationNodes = new PropertyDeclarationNodes(); + + $this->assertTrue($propertyDeclarationNodes->isEmpty()); + + $propertyDeclarationNodes = new PropertyDeclarationNodes( + ASTNodeFixtures::PropertyDeclaration('foo: bar') + ); + + $this->assertFalse($propertyDeclarationNodes->isEmpty()); + } +} diff --git a/test/Unit/Language/AST/Node/TypeReference/TypeNameNodesTest.php b/test/Unit/Language/AST/Node/TypeReference/TypeNameNodesTest.php new file mode 100644 index 00000000..6395fc33 --- /dev/null +++ b/test/Unit/Language/AST/Node/TypeReference/TypeNameNodesTest.php @@ -0,0 +1,157 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\AST\Node\TypeReference; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PHPUnit\Framework\TestCase; + +final class TypeNameNodesTest extends TestCase +{ + protected function createTypeNameNode(string $typeName): TypeNameNode + { + return new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from($typeName) + ); + } + + /** + * @test + */ + public function mustNotBeEmpty(): void + { + $this->expectExceptionObject( + InvalidTypeNameNodes::becauseTheyWereEmpty() + ); + + new TypeNameNodes(); + } + + /** + * @test + */ + public function mustNotContainDuplicates(): void + { + $duplicate = new TypeNameNode( + rangeInSource: Range::from( + new Position(1, 1), + new Position(1, 1) + ), + value: TypeName::from('Foo') + ); + + $this->expectExceptionObject( + InvalidTypeNameNodes::becauseTheyContainDuplicates( + duplicateTypeNameNode: $duplicate + ) + ); + + new TypeNameNodes( + $this->createTypeNameNode('Foo'), + $this->createTypeNameNode('Bar'), + $duplicate, + $this->createTypeNameNode('Baz'), + ); + } + + /** + * @test + */ + public function providesItsOwnSize(): void + { + $typeNameNodes = new TypeNameNodes( + $this->createTypeNameNode('Foo') + ); + + $this->assertEquals(1, $typeNameNodes->getSize()); + + $typeNameNodes = new TypeNameNodes( + $this->createTypeNameNode('Foo'), + $this->createTypeNameNode('Bar') + ); + + $this->assertEquals(2, $typeNameNodes->getSize()); + + $typeNameNodes = new TypeNameNodes( + $this->createTypeNameNode('Foo'), + $this->createTypeNameNode('Bar'), + $this->createTypeNameNode('Baz') + ); + + $this->assertEquals(3, $typeNameNodes->getSize()); + } + + /** + * @test + */ + public function providesItsLastItemIfTheresOnlyOne(): void + { + $foo = $this->createTypeNameNode('Foo'); + $typeNameNodes = new TypeNameNodes($foo); + + $this->assertSame($foo, $typeNameNodes->getLast()); + } + + /** + * @test + */ + public function providesItsLastItemIfTheresMultiple(): void + { + $foo = $this->createTypeNameNode('Foo'); + $bar = $this->createTypeNameNode('Bar'); + $baz = $this->createTypeNameNode('Baz'); + $typeNameNodes = new TypeNameNodes($foo, $bar, $baz); + + $this->assertSame($baz, $typeNameNodes->getLast()); + } + + /** + * @test + */ + public function convertsToTypeNames(): void + { + $typeNameNodes = new TypeNameNodes( + $this->createTypeNameNode('Foo'), + $this->createTypeNameNode('Bar'), + $this->createTypeNameNode('Baz') + ); + + $this->assertEquals( + new TypeNames( + TypeName::from('Foo'), + TypeName::from('Bar'), + TypeName::from('Baz') + ), + $typeNameNodes->toTypeNames() + ); + } +} diff --git a/test/Unit/Language/AST/Node/TypeReference/TypeReferenceNodeTest.php b/test/Unit/Language/AST/Node/TypeReference/TypeReferenceNodeTest.php new file mode 100644 index 00000000..ef70055b --- /dev/null +++ b/test/Unit/Language/AST/Node/TypeReference/TypeReferenceNodeTest.php @@ -0,0 +1,304 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\AST\Node\TypeReference; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PHPUnit\Framework\TestCase; + +final class TypeReferenceNodeTest extends TestCase +{ + /** + * @test + */ + public function validSimpleTypeReferenceIsValid(): void + { + $typeReferenceNode = new TypeReferenceNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Foo') + ) + ), + isArray: false, + isOptional: false + ); + + $this->assertEquals(1, $typeReferenceNode->names->getSize()); + $this->assertEquals('Foo', $typeReferenceNode->names->items[0]->value->value); + $this->assertFalse($typeReferenceNode->isArray); + $this->assertFalse($typeReferenceNode->isOptional); + } + + /** + * @test + */ + public function validArrayTypeReferenceIsValid(): void + { + $typeReferenceNode = new TypeReferenceNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Foo') + ) + ), + isArray: true, + isOptional: false + ); + + $this->assertEquals(1, $typeReferenceNode->names->getSize()); + $this->assertEquals('Foo', $typeReferenceNode->names->items[0]->value->value); + $this->assertTrue($typeReferenceNode->isArray); + $this->assertFalse($typeReferenceNode->isOptional); + } + + /** + * @test + */ + public function validOptionalTypeReferenceIsValid(): void + { + $typeReferenceNode = new TypeReferenceNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Foo') + ) + ), + isArray: false, + isOptional: true + ); + + $this->assertEquals(1, $typeReferenceNode->names->getSize()); + $this->assertEquals('Foo', $typeReferenceNode->names->items[0]->value->value); + $this->assertFalse($typeReferenceNode->isArray); + $this->assertTrue($typeReferenceNode->isOptional); + } + + /** + * @test + */ + public function validUnionTypeReferenceIsValid(): void + { + $typeReferenceNode = new TypeReferenceNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Foo') + ), + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Bar') + ), + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Baz') + ) + ), + isArray: false, + isOptional: false + ); + + $this->assertEquals(3, $typeReferenceNode->names->getSize()); + $this->assertEquals('Foo', $typeReferenceNode->names->items[0]->value->value); + $this->assertEquals('Bar', $typeReferenceNode->names->items[1]->value->value); + $this->assertEquals('Baz', $typeReferenceNode->names->items[2]->value->value); + $this->assertFalse($typeReferenceNode->isArray); + $this->assertFalse($typeReferenceNode->isOptional); + } + + /** + * @test + */ + public function mustNotBeArrayAndOptionalSimultaneously(): void + { + $name = TypeName::from('Foo'); + + $this->expectExceptionObject( + InvalidTypeReferenceNode::becauseItWasOptionalAndArrayAtTheSameTime( + affectedTypeNames: new TypeNames($name), + affectedRangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ) + ) + ); + + new TypeReferenceNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: $name + ) + ), + isArray: true, + isOptional: true + ); + } + + /** + * @test + */ + public function mustNotBeUnionAndArraySimultaneously(): void + { + $typeNameNodes = new TypeNameNodes( + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Foo') + ), + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Bar') + ), + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Baz') + ) + ); + + $this->expectExceptionObject( + InvalidTypeReferenceNode::becauseItWasUnionAndArrayAtTheSameTime( + affectedTypeNames: $typeNameNodes->toTypeNames(), + affectedRangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ) + ) + ); + + new TypeReferenceNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + names: $typeNameNodes, + isArray: true, + isOptional: false + ); + } + + /** + * @test + */ + public function mustNotBeUnionAndOptionalSimultaneously(): void + { + $typeNameNodes = new TypeNameNodes( + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Foo') + ), + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Bar') + ), + new TypeNameNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + value: TypeName::from('Baz') + ) + ); + + $this->expectExceptionObject( + InvalidTypeReferenceNode::becauseItWasUnionAndOptionalAtTheSameTime( + affectedTypeNames: $typeNameNodes->toTypeNames(), + affectedRangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ) + ) + ); + + new TypeReferenceNode( + rangeInSource: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + names: $typeNameNodes, + isArray: false, + isOptional: true + ); + } +} diff --git a/test/Unit/Language/ASTNodeFixtures.php b/test/Unit/Language/ASTNodeFixtures.php new file mode 100644 index 00000000..93652b30 --- /dev/null +++ b/test/Unit/Language/ASTNodeFixtures.php @@ -0,0 +1,243 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language; + +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\BooleanLiteral\BooleanLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\ComponentDeclaration\ComponentDeclarationParser; +use PackageFactory\ComponentEngine\Language\Parser\EnumDeclaration\EnumDeclarationParser; +use PackageFactory\ComponentEngine\Language\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Language\Parser\IntegerLiteral\IntegerLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\Match\MatchParser; +use PackageFactory\ComponentEngine\Language\Parser\Module\ModuleParser; +use PackageFactory\ComponentEngine\Language\Parser\NullLiteral\NullLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\PropertyDeclaration\PropertyDeclarationParser; +use PackageFactory\ComponentEngine\Language\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\StructDeclaration\StructDeclarationParser; +use PackageFactory\ComponentEngine\Language\Parser\Tag\TagParser; +use PackageFactory\ComponentEngine\Language\Parser\TemplateLiteral\TemplateLiteralParser; +use PackageFactory\ComponentEngine\Language\Parser\Text\TextParser; +use PackageFactory\ComponentEngine\Language\Parser\TypeReference\TypeReferenceParser; +use PackageFactory\ComponentEngine\Language\Parser\ValueReference\ValueReferenceParser; +use PackageFactory\ComponentEngine\Test\Unit\Parser\Tokenizer\Fixtures as TokenizerFixtures; + +final class ASTNodeFixtures +{ + public static function Access(string $sourceAsString): AccessNode + { + $expressionNode = self::Expression($sourceAsString); + $accessNode = $expressionNode->root; + assert($accessNode instanceof AccessNode); + + return $accessNode; + } + + public static function Attribute(string $sourceAsString): ?AttributeNode + { + $tagNode = self::Tag(sprintf('', $sourceAsString)); + + return array_values($tagNode->attributes->items)[0] ?? null; + } + + public static function BinaryOperation(string $sourceAsString): BinaryOperationNode + { + $expressionNode = self::Expression($sourceAsString); + $binaryOperationNode = $expressionNode->root; + assert($binaryOperationNode instanceof BinaryOperationNode); + + return $binaryOperationNode; + } + + public static function BooleanLiteral(string $sourceAsString): BooleanLiteralNode + { + $booleanLiteralParser = BooleanLiteralParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $booleanLiteralParser->parse($tokens); + } + + public static function ComponentDeclaration(string $sourceAsString): ComponentDeclarationNode + { + $componentDeclarationParser = ComponentDeclarationParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $componentDeclarationParser->parse($tokens); + } + + public static function EnumDeclaration(string $sourceAsString): EnumDeclarationNode + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $enumDeclarationParser->parse($tokens); + } + + public static function Expression(string $sourceAsString): ExpressionNode + { + $epxressionParser = new ExpressionParser(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $epxressionParser->parse($tokens); + } + + public static function IntegerLiteral(string $sourceAsString): IntegerLiteralNode + { + $integerLiteralParser = IntegerLiteralParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $integerLiteralParser->parse($tokens); + } + + public static function Match(string $sourceAsString): MatchNode + { + $matchParser = MatchParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $matchParser->parse($tokens); + } + + public static function Module(string $sourceAsString): ModuleNode + { + $moduleParser = ModuleParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $moduleParser->parse($tokens); + } + + public static function NullLiteral(string $sourceAsString): NullLiteralNode + { + $nullLiteralParser = NullLiteralParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $nullLiteralParser->parse($tokens); + } + + public static function PropertyDeclaration(string $sourceAsString): PropertyDeclarationNode + { + $propertyDeclarationParser = PropertyDeclarationParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $propertyDeclarationParser->parse($tokens); + } + + public static function StringLiteral(string $sourceAsString): StringLiteralNode + { + $stringLiteralParser = StringLiteralParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $stringLiteralParser->parse($tokens); + } + + public static function StructDeclaration(string $sourceAsString): StructDeclarationNode + { + $structDeclarationParser = StructDeclarationParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $structDeclarationParser->parse($tokens); + } + + public static function Tag(string $sourceAsString): TagNode + { + $tagParser = TagParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $tagParser->parse($tokens); + } + + public static function TagContent(string $sourceAsString): null|TextNode|ExpressionNode|TagNode + { + $tagNode = self::Tag(sprintf('
%s
', $sourceAsString)); + + return $tagNode->children->items[0] ?? null; + } + + public static function TemplateLiteral(string $sourceAsString): TemplateLiteralNode + { + $templateLiteralParser = TemplateLiteralParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $templateLiteralParser->parse($tokens); + } + + public static function TernaryOperation(string $sourceAsString): TernaryOperationNode + { + $expressionNode = self::Expression($sourceAsString); + $ternaryOperationNode = $expressionNode->root; + assert($ternaryOperationNode instanceof TernaryOperationNode); + + return $ternaryOperationNode; + } + + public static function Text(string $sourceAsString): ?TextNode + { + $textParser = TextParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $textParser->parse($tokens); + } + + public static function TypeReference(string $sourceAsString): TypeReferenceNode + { + $typeReferenceParser = TypeReferenceParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $typeReferenceParser->parse($tokens); + } + + public static function ValueReference(string $sourceAsString): ValueReferenceNode + { + $valueReferenceParser = ValueReferenceParser::singleton(); + $tokens = TokenizerFixtures::tokens($sourceAsString); + + return $valueReferenceParser->parse($tokens); + } + + public static function UnaryOperation(string $sourceAsString): UnaryOperationNode + { + $expressionNode = self::Expression($sourceAsString); + $ternaryOperationNode = $expressionNode->root; + assert($ternaryOperationNode instanceof UnaryOperationNode); + + return $ternaryOperationNode; + } +} diff --git a/test/Unit/Language/Parser/BooleanLiteral/BooleanLiteralParserTest.php b/test/Unit/Language/Parser/BooleanLiteral/BooleanLiteralParserTest.php new file mode 100644 index 00000000..447c56b3 --- /dev/null +++ b/test/Unit/Language/Parser/BooleanLiteral/BooleanLiteralParserTest.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\BooleanLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\BooleanLiteral\BooleanLiteralParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class BooleanLiteralParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesTrue(): void + { + $booleanLiteralParser = BooleanLiteralParser::singleton(); + $tokens = $this->createTokenIterator('true'); + + $expectedBooleanLiteralNode = new BooleanLiteralNode( + rangeInSource: $this->range([0, 0], [0, 3]), + value: true + ); + + $this->assertEquals( + $expectedBooleanLiteralNode, + $booleanLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesFalse(): void + { + $booleanLiteralParser = BooleanLiteralParser::singleton(); + $tokens = $this->createTokenIterator('false'); + + $expectedBooleanLiteralNode = new BooleanLiteralNode( + rangeInSource: $this->range([0, 0], [0, 4]), + value: false + ); + + $this->assertEquals( + $expectedBooleanLiteralNode, + $booleanLiteralParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/ComponentDeclaration/ComponentDeclarationParserTest.php b/test/Unit/Language/Parser/ComponentDeclaration/ComponentDeclarationParserTest.php new file mode 100644 index 00000000..a724ce97 --- /dev/null +++ b/test/Unit/Language/Parser/ComponentDeclaration/ComponentDeclarationParserTest.php @@ -0,0 +1,305 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ComponentDeclaration; + +use PackageFactory\ComponentEngine\Domain\AttributeName\AttributeName; +use PackageFactory\ComponentEngine\Domain\ComponentName\ComponentName; +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\TagName\TagName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\ChildNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\ComponentDeclaration\ComponentDeclarationParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class ComponentDeclarationParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesComponentDeclarationWithNoProps(): void + { + $componentDeclarationParser = ComponentDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('component Foo { return "bar" }'); + + $expectedComponentDeclarationNode = new ComponentDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 29]), + name: new ComponentNameNode( + rangeInSource: $this->range([0, 10], [0, 12]), + value: ComponentName::from('Foo') + ), + props: new PropertyDeclarationNodes(), + return: new ExpressionNode( + rangeInSource: $this->range([0, 24], [0, 26]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 24], [0, 26]), + value: 'bar' + ) + ) + ); + + $this->assertEquals( + $expectedComponentDeclarationNode, + $componentDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesComponentDeclarationWithOneProp(): void + { + $componentDeclarationParser = ComponentDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('component Foo { bar: string return bar }'); + + $expectedComponentDeclarationNode = new ComponentDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 39]), + name: new ComponentNameNode( + rangeInSource: $this->range([0, 10], [0, 12]), + value: ComponentName::from('Foo') + ), + props: new PropertyDeclarationNodes( + new PropertyDeclarationNode( + rangeInSource: $this->range([0, 16], [0, 26]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 16], [0, 18]), + value: PropertyName::from('bar') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 21], [0, 26]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 21], [0, 26]), + value: TypeName::from('string') + ) + ), + isArray: false, + isOptional: false + ) + ) + ), + return: new ExpressionNode( + rangeInSource: $this->range([0, 35], [0, 37]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 35], [0, 37]), + name: VariableName::from('bar') + ) + ) + ); + + $this->assertEquals( + $expectedComponentDeclarationNode, + $componentDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesComponentDeclarationWithMultiplePropsAndComplexReturnStatement(): void + { + $componentDeclarationParser = ComponentDeclarationParser::singleton(); + $componentAsString = <<{children}
+ } + AFX; + $tokens = $this->createTokenIterator($componentAsString); + + $expectedComponentDeclarationNode = new ComponentDeclarationNode( + rangeInSource: $this->range([0, 0], [7, 0]), + name: new ComponentNameNode( + rangeInSource: $this->range([0, 10], [0, 13]), + value: ComponentName::from('Link') + ), + props: new PropertyDeclarationNodes( + new PropertyDeclarationNode( + rangeInSource: $this->range([1, 4], [1, 19]), + name: new PropertyNameNode( + rangeInSource: $this->range([1, 4], [1, 7]), + value: PropertyName::from('href') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([1, 10], [1, 19]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([1, 10], [1, 15]), + value: TypeName::from('string') + ), + new TypeNameNode( + rangeInSource: $this->range([1, 17], [1, 19]), + value: TypeName::from('Uri') + ) + ), + isArray: false, + isOptional: false + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([2, 4], [2, 18]), + name: new PropertyNameNode( + rangeInSource: $this->range([2, 4], [2, 9]), + value: PropertyName::from('target') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([2, 12], [2, 18]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([2, 13], [2, 18]), + value: TypeName::from('string') + ) + ), + isArray: false, + isOptional: true + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([3, 4], [3, 16]), + name: new PropertyNameNode( + rangeInSource: $this->range([3, 4], [3, 6]), + value: PropertyName::from('rel') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([3, 9], [3, 16]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([3, 9], [3, 14]), + value: TypeName::from('string') + ) + ), + isArray: true, + isOptional: false + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([4, 4], [4, 17]), + name: new PropertyNameNode( + rangeInSource: $this->range([4, 4], [4, 11]), + value: PropertyName::from('children') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([4, 14], [4, 17]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([4, 14], [4, 17]), + value: TypeName::from('slot') + ) + ), + isArray: false, + isOptional: false + ) + ), + ), + return: new ExpressionNode( + rangeInSource: $this->range([6, 11], [6, 65]), + root: new TagNode( + rangeInSource: $this->range([6, 11], [6, 65]), + name: new TagNameNode( + rangeInSource: $this->range([6, 12], [6, 12]), + value: TagName::from('a') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([6, 14], [6, 23]), + name: new AttributeNameNode( + rangeInSource: $this->range([6, 14], [6, 17]), + value: AttributeName::from('href') + ), + value: new ExpressionNode( + rangeInSource: $this->range([6, 20], [6, 23]), + root: new ValueReferenceNode( + rangeInSource: $this->range([6, 20], [6, 23]), + name: VariableName::from('href') + ) + ) + ), + new AttributeNode( + rangeInSource: $this->range([6, 26], [6, 39]), + name: new AttributeNameNode( + rangeInSource: $this->range([6, 26], [6, 31]), + value: AttributeName::from('target') + ), + value: new ExpressionNode( + rangeInSource: $this->range([6, 34], [6, 39]), + root: new ValueReferenceNode( + rangeInSource: $this->range([6, 34], [6, 39]), + name: VariableName::from('target') + ) + ) + ), + new AttributeNode( + rangeInSource: $this->range([6, 42], [6, 49]), + name: new AttributeNameNode( + rangeInSource: $this->range([6, 42], [6, 44]), + value: AttributeName::from('rel') + ), + value: new ExpressionNode( + rangeInSource: $this->range([6, 47], [6, 49]), + root: new ValueReferenceNode( + rangeInSource: $this->range([6, 47], [6, 49]), + name: VariableName::from('rel') + ) + ) + ) + ), + children: new ChildNodes( + new ExpressionNode( + rangeInSource: $this->range([6, 53], [6, 60]), + root: new ValueReferenceNode( + rangeInSource: $this->range([6, 53], [6, 60]), + name: VariableName::from('children') + ) + ) + ), + isSelfClosing: false + ) + ) + ); + + $this->assertEquals( + $expectedComponentDeclarationNode, + $componentDeclarationParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/EnumDeclaration/EnumDeclarationParserTest.php b/test/Unit/Language/Parser/EnumDeclaration/EnumDeclarationParserTest.php new file mode 100644 index 00000000..7eedc1d1 --- /dev/null +++ b/test/Unit/Language/Parser/EnumDeclaration/EnumDeclarationParserTest.php @@ -0,0 +1,667 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\EnumDeclaration; + +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PackageFactory\ComponentEngine\Domain\EnumName\EnumName; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberValueNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerFormat; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\EnumDeclaration\EnumDeclarationParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class EnumDeclarationParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesEnumDeclarationWithOneValuelessMember(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('enum Foo { BAR }'); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 15]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 11], [0, 13]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: EnumMemberName::from('BAR') + ), + value: null + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithThreeValuelessMembers(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('enum Foo { BAR BAZ QUX }'); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 23]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 11], [0, 13]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: EnumMemberName::from('BAR') + ), + value: null + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 15], [0, 17]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 15], [0, 17]), + value: EnumMemberName::from('BAZ') + ), + value: null + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 19], [0, 21]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 19], [0, 21]), + value: EnumMemberName::from('QUX') + ), + value: null + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithOneStringValueMember(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('enum Foo { BAR("BAR") }'); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 22]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 11], [0, 20]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: EnumMemberName::from('BAR') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([0, 14], [0, 20]), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 16], [0, 18]), + value: 'BAR' + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithSevenStringValueMembers(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $enumAsString = <<createTokenIterator($enumAsString); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [8, 0]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 11]), + value: EnumName::from('Weekday') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([1, 4], [1, 16]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([1, 4], [1, 9]), + value: EnumMemberName::from('MONDAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([1, 10], [1, 16]), + value: new StringLiteralNode( + rangeInSource: $this->range([1, 12], [1, 14]), + value: 'mon' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([2, 4], [2, 17]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([2, 4], [2, 10]), + value: EnumMemberName::from('TUESDAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([2, 11], [2, 17]), + value: new StringLiteralNode( + rangeInSource: $this->range([2, 13], [2, 15]), + value: 'tue' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([3, 4], [3, 19]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([3, 4], [3, 12]), + value: EnumMemberName::from('WEDNESDAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([3, 13], [3, 19]), + value: new StringLiteralNode( + rangeInSource: $this->range([3, 15], [3, 17]), + value: 'wed' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([4, 4], [4, 18]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([4, 4], [4, 11]), + value: EnumMemberName::from('THURSDAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([4, 12], [4, 18]), + value: new StringLiteralNode( + rangeInSource: $this->range([4, 14], [4, 16]), + value: 'thu' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([5, 4], [5, 16]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([5, 4], [5, 9]), + value: EnumMemberName::from('FRIDAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([5, 10], [5, 16]), + value: new StringLiteralNode( + rangeInSource: $this->range([5, 12], [5, 14]), + value: 'fri' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([6, 4], [6, 18]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([6, 4], [6, 11]), + value: EnumMemberName::from('SATURDAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([6, 12], [6, 18]), + value: new StringLiteralNode( + rangeInSource: $this->range([6, 14], [6, 16]), + value: 'sat' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([7, 4], [7, 16]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([7, 4], [7, 9]), + value: EnumMemberName::from('SUNDAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([7, 10], [7, 16]), + value: new StringLiteralNode( + rangeInSource: $this->range([7, 12], [7, 14]), + value: 'sun' + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithOneBinaryIntegerValueMember(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('enum Foo { BAR(0b101) }'); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 22]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 11], [0, 20]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: EnumMemberName::from('BAR') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([0, 14], [0, 20]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([0, 15], [0, 19]), + format: IntegerFormat::BINARY, + value: '0b101' + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithOneOctalIntegerValueMember(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('enum Foo { BAR(0o644) }'); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 22]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 11], [0, 20]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: EnumMemberName::from('BAR') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([0, 14], [0, 20]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([0, 15], [0, 19]), + format: IntegerFormat::OCTAL, + value: '0o644' + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithOneDecimalIntegerValueMember(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('enum Foo { BAR(42) }'); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 19]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 11], [0, 17]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: EnumMemberName::from('BAR') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([0, 14], [0, 17]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([0, 15], [0, 16]), + format: IntegerFormat::DECIMAL, + value: '42' + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithOneHexadecimalIntegerValueMember(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('enum Foo { BAR(0xABC) }'); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 22]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 11], [0, 20]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: EnumMemberName::from('BAR') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([0, 14], [0, 20]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([0, 15], [0, 19]), + format: IntegerFormat::HEXADECIMAL, + value: '0xABC' + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumDeclarationWithwelveIntegerValueMembers(): void + { + $enumDeclarationParser = EnumDeclarationParser::singleton(); + $enumAsString = <<createTokenIterator($enumAsString); + + $expectedEnumDeclarationNode = new EnumDeclarationNode( + rangeInSource: $this->range([0, 0], [13, 0]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 5], [0, 9]), + value: EnumName::from('Month') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([1, 4], [1, 13]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([1, 4], [1, 10]), + value: EnumMemberName::from('JANUARY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([1, 11], [1, 13]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([1, 12], [1, 12]), + format: IntegerFormat::DECIMAL, + value: '1' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([2, 4], [2, 14]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([2, 4], [2, 11]), + value: EnumMemberName::from('FEBRUARY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([2, 12], [2, 14]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([2, 13], [2, 13]), + format: IntegerFormat::DECIMAL, + value: '2' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([3, 4], [3, 11]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([3, 4], [3, 8]), + value: EnumMemberName::from('MARCH') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([3, 9], [3, 11]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([3, 10], [3, 10]), + format: IntegerFormat::DECIMAL, + value: '3' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([4, 4], [4, 11]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([4, 4], [4, 8]), + value: EnumMemberName::from('APRIL') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([4, 9], [4, 11]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([4, 10], [4, 10]), + format: IntegerFormat::DECIMAL, + value: '4' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([5, 4], [5, 9]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([5, 4], [5, 6]), + value: EnumMemberName::from('MAY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([5, 7], [5, 9]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([5, 8], [5, 8]), + format: IntegerFormat::DECIMAL, + value: '5' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([6, 4], [6, 10]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([6, 4], [6, 7]), + value: EnumMemberName::from('JUNE') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([6, 8], [6, 10]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([6, 9], [6, 9]), + format: IntegerFormat::DECIMAL, + value: '6' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([7, 4], [7, 10]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([7, 4], [7, 7]), + value: EnumMemberName::from('JULY') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([7, 8], [7, 10]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([7, 9], [7, 9]), + format: IntegerFormat::DECIMAL, + value: '7' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([8, 4], [8, 12]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([8, 4], [8, 9]), + value: EnumMemberName::from('AUGUST') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([8, 10], [8, 12]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([8, 11], [8, 11]), + format: IntegerFormat::DECIMAL, + value: '8' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([9, 4], [9, 15]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([9, 4], [9, 12]), + value: EnumMemberName::from('SEPTEMBER') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([9, 13], [9, 15]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([9, 14], [9, 14]), + format: IntegerFormat::DECIMAL, + value: '9' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([10, 4], [10, 14]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([10, 4], [10, 10]), + value: EnumMemberName::from('OCTOBER') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([10, 11], [10, 14]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([10, 12], [10, 13]), + format: IntegerFormat::DECIMAL, + value: '10' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([11, 4], [11, 15]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([11, 4], [11, 11]), + value: EnumMemberName::from('NOVEMBER') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([11, 12], [11, 15]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([11, 13], [11, 14]), + format: IntegerFormat::DECIMAL, + value: '11' + ) + ) + ), + new EnumMemberDeclarationNode( + rangeInSource: $this->range([12, 4], [12, 15]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([12, 4], [12, 11]), + value: EnumMemberName::from('DECEMBER') + ), + value: new EnumMemberValueNode( + rangeInSource: $this->range([12, 12], [12, 15]), + value: new IntegerLiteralNode( + rangeInSource: $this->range([12, 13], [12, 14]), + format: IntegerFormat::DECIMAL, + value: '12' + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedEnumDeclarationNode, + $enumDeclarationParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/Export/ExportParserTest.php b/test/Unit/Language/Parser/Export/ExportParserTest.php new file mode 100644 index 00000000..37a45f9b --- /dev/null +++ b/test/Unit/Language/Parser/Export/ExportParserTest.php @@ -0,0 +1,206 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\Export; + +use PackageFactory\ComponentEngine\Domain\ComponentName\ComponentName; +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PackageFactory\ComponentEngine\Domain\EnumName\EnumName; +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumMemberNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Export\ExportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\Export\ExportCouldNotBeParsed; +use PackageFactory\ComponentEngine\Language\Parser\Export\ExportParser; +use PackageFactory\ComponentEngine\Parser\Source\Path; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class ExportParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesComponentExport(): void + { + $exportParser = ExportParser::singleton(); + $tokens = $this->createTokenIterator( + 'export component Foo { return bar }' + ); + + $expectedExportNode = new ExportNode( + rangeInSource: $this->range([0, 0], [0, 34]), + declaration: new ComponentDeclarationNode( + rangeInSource: $this->range([0, 7], [0, 34]), + name: new ComponentNameNode( + rangeInSource: $this->range([0, 17], [0, 19]), + value: ComponentName::from('Foo') + ), + props: new PropertyDeclarationNodes(), + return: new ExpressionNode( + rangeInSource: $this->range([0, 30], [0, 32]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 30], [0, 32]), + name: VariableName::from('bar') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExportNode, + $exportParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesEnumExport(): void + { + $exportParser = ExportParser::singleton(); + $tokens = $this->createTokenIterator( + 'export enum Foo { BAR }' + ); + + $expectedExportNode = new ExportNode( + rangeInSource: $this->range([0, 0], [0, 22]), + declaration: new EnumDeclarationNode( + rangeInSource: $this->range([0, 7], [0, 22]), + name: new EnumNameNode( + rangeInSource: $this->range([0, 12], [0, 14]), + value: EnumName::from('Foo') + ), + members: new EnumMemberDeclarationNodes( + new EnumMemberDeclarationNode( + rangeInSource: $this->range([0, 18], [0, 20]), + name: new EnumMemberNameNode( + rangeInSource: $this->range([0, 18], [0, 20]), + value: EnumMemberName::from('BAR') + ), + value: null + ) + ) + ) + ); + + $this->assertEquals( + $expectedExportNode, + $exportParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesStructExport(): void + { + $exportParser = ExportParser::singleton(); + $tokens = $this->createTokenIterator( + 'export struct Foo { bar: baz }' + ); + + $expectedExportNode = new ExportNode( + rangeInSource: $this->range([0, 0], [0, 29]), + declaration: new StructDeclarationNode( + rangeInSource: $this->range([0, 7], [0, 29]), + name: new StructNameNode( + rangeInSource: $this->range([0, 14], [0, 16]), + value: StructName::from('Foo') + ), + properties: new PropertyDeclarationNodes( + new PropertyDeclarationNode( + rangeInSource: $this->range([0, 20], [0, 27]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 20], [0, 22]), + value: PropertyName::from('bar') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 25], [0, 27]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 25], [0, 27]), + value: TypeName::from('baz') + ) + ), + isArray: false, + isOptional: false + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedExportNode, + $exportParser->parse($tokens) + ); + } + + /** + * @test + */ + public function throwsIfExportIsNoDeclaration(): void + { + $this->assertThrowsParserException( + function () { + $exportParser = ExportParser::singleton(); + $tokens = $this->createTokenIterator('export null'); + + $exportParser->parse($tokens); + }, + ExportCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::KEYWORD_COMPONENT, + TokenType::KEYWORD_ENUM, + TokenType::KEYWORD_STRUCT + ), + actualToken: new Token( + type: TokenType::KEYWORD_NULL, + value: 'null', + boundaries: $this->range([0, 7], [0, 10]), + sourcePath: Path::createMemory() + ) + ) + ); + } +} diff --git a/test/Unit/Language/Parser/Expression/ExpressionParserTest.php b/test/Unit/Language/Parser/Expression/ExpressionParserTest.php new file mode 100644 index 00000000..a7bfb490 --- /dev/null +++ b/test/Unit/Language/Parser/Expression/ExpressionParserTest.php @@ -0,0 +1,1858 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\Expression; + +use ArrayIterator; +use PackageFactory\ComponentEngine\Domain\AttributeName\AttributeName; +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\TagName\TagName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessKeyNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Access\AccessType; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperator; +use PackageFactory\ComponentEngine\Language\AST\Node\BooleanLiteral\BooleanLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerFormat; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\ChildNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralExpressionSegmentNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralSegments; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralStringSegmentNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\UnaryOperation\UnaryOperator; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\Expression\ExpressionParser; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class ExpressionParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesMandatoryAccessWithOneLevel(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a.b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 2]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 2]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 2], [0, 2]), + value: PropertyName::from('b') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMandatoryAccessWithMultipleLevels(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a.b.c.d.e'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 8]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 8]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 6]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 6]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 4]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 2]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 2]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 2], [0, 2]), + value: PropertyName::from('b') + ) + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 4], [0, 4]), + value: PropertyName::from('c') + ) + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 6], [0, 6]), + value: PropertyName::from('d') + ) + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 8], [0, 8]), + value: PropertyName::from('e') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesOptionalAccessWithOneLevel(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a?.b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 3]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 3]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 3], [0, 3]), + value: PropertyName::from('b') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesOptionalAccessWithMultipleLevels(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a?.b?.c?.d?.e'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 12]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 12]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 9]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 9]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 6]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 6]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 3]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 3]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 3], [0, 3]), + value: PropertyName::from('b') + ) + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 6], [0, 6]), + value: PropertyName::from('c') + ) + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 9], [0, 9]), + value: PropertyName::from('d') + ) + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 12], [0, 12]), + value: PropertyName::from('e') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMixedAccessChainStartingWithMandatoryAccess(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a.b?.c'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 5]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 5]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 2]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 2]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 2], [0, 2]), + value: PropertyName::from('b') + ) + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 5], [0, 5]), + value: PropertyName::from('c') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMixedAccessChainStartingWithOptionalAccess(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a?.b.c'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 5]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 5]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 3]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 3]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 3], [0, 3]), + value: PropertyName::from('b') + ) + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 5], [0, 5]), + value: PropertyName::from('c') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMandatoryAccessWithBracketedEpxressionAsParent(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('(a ? b : c).d'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 12]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 12]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 10]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 1]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 1], [0, 1]), + name: VariableName::from('a') + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 5], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 5], [0, 5]), + name: VariableName::from('b') + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 9]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 9], [0, 9]), + name: VariableName::from('c') + ) + ) + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 12], [0, 12]), + value: PropertyName::from('d') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesOptionalAccessWithBracketedEpxressionAsParent(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('(a ? b : c)?.d'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 13]), + root: new AccessNode( + rangeInSource: $this->range([0, 0], [0, 13]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 10]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 1]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 1], [0, 1]), + name: VariableName::from('a') + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 5], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 5], [0, 5]), + name: VariableName::from('b') + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 9]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 9], [0, 9]), + name: VariableName::from('c') + ) + ) + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 13], [0, 13]), + value: PropertyName::from('d') + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationAnd(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a && b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 5]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 5]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::AND, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 5], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 5], [0, 5]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationOr(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a || b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 5]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 5]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::OR, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 5], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 5], [0, 5]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationEquals(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a === b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 6]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 6]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::EQUAL, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 6]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 6], [0, 6]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationNotEquals(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a !== b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 6]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 6]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::NOT_EQUAL, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 6]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 6], [0, 6]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationGreaterThan(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a > b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 4]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::GREATER_THAN, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 4], [0, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 4], [0, 4]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationGreaterThanOrEqual(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a >= b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 5]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 5]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::GREATER_THAN_OR_EQUAL, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 5], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 5], [0, 5]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationLessThan(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a < b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 4]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::LESS_THAN, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 4], [0, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 4], [0, 4]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationLessThanOrEqual(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a <= b'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 5]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 5]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::LESS_THAN_OR_EQUAL, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 5], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 5], [0, 5]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationInBrackets(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('(a <= b)'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 7]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 1], [0, 6]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 1]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 1], [0, 1]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::LESS_THAN_OR_EQUAL, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 6]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 6], [0, 6]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryOperationInMultipleBrackets(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('((((a <= b))))'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 13]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 4], [0, 9]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 4], [0, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 4], [0, 4]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::LESS_THAN_OR_EQUAL, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 9]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 9], [0, 9]), + name: VariableName::from('b') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBooleanLiteralTrue(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('true'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 3]), + root: new BooleanLiteralNode( + rangeInSource: $this->range([0, 0], [0, 3]), + value: true + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBooleanLiteralFalse(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('false'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new BooleanLiteralNode( + rangeInSource: $this->range([0, 0], [0, 4]), + value: false + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesBinaryIntegerLiteral(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('0b1001'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 5]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 5]), + format: IntegerFormat::BINARY, + value: '0b1001' + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesOctalIntegerLiteral(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('0o755'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 4]), + format: IntegerFormat::OCTAL, + value: '0o755' + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesDecimalIntegerLiteral(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('42'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 1]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 1]), + format: IntegerFormat::DECIMAL, + value: '42' + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesHexadecimalIntegerLiteral(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('0xABC'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 4]), + format: IntegerFormat::HEXADECIMAL, + value: '0xABC' + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatch(): void + { + $expressionParser = new ExpressionParser(); + $matchAsString = << foo + null -> foo.bar + default -> "N/A" + } + AFX; + $tokens = $this->createTokenIterator($matchAsString); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [5, 0]), + root: new MatchNode( + rangeInSource: $this->range([0, 0], [5, 0]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 17]), + root: new AccessNode( + rangeInSource: $this->range([0, 6], [0, 17]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 12]), + root: new AccessNode( + rangeInSource: $this->range([0, 6], [0, 12]), + parent: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 6], [0, 8]), + name: VariableName::from('foo') + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 10], [0, 12]), + value: PropertyName::from('bar') + ) + ) + ), + type: AccessType::OPTIONAL, + key: new AccessKeyNode( + rangeInSource: $this->range([0, 15], [0, 17]), + value: PropertyName::from('baz') + ) + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([1, 4], [2, 19]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([1, 4], [1, 11]), + root: new AccessNode( + rangeInSource: $this->range([1, 4], [1, 11]), + parent: new ExpressionNode( + rangeInSource: $this->range([1, 4], [1, 6]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 4], [1, 6]), + name: VariableName::from('Qux') + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([1, 8], [1, 11]), + value: PropertyName::from('QUUX') + ) + ) + ), + new ExpressionNode( + rangeInSource: $this->range([2, 4], [2, 12]), + root: new AccessNode( + rangeInSource: $this->range([2, 4], [2, 12]), + parent: new ExpressionNode( + rangeInSource: $this->range([2, 4], [2, 6]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 4], [2, 6]), + name: VariableName::from('Qux') + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([2, 8], [2, 12]), + value: PropertyName::from('CORGE') + ) + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([2, 17], [2, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 17], [2, 19]), + name: VariableName::from('foo') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([3, 4], [3, 18]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([3, 4], [3, 7]), + root: new NullLiteralNode( + rangeInSource: $this->range([3, 4], [3, 7]), + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([3, 12], [3, 18]), + root: new AccessNode( + rangeInSource: $this->range([3, 12], [3, 18]), + parent: new ExpressionNode( + rangeInSource: $this->range([3, 12], [3, 14]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 12], [3, 14]), + name: VariableName::from('foo') + ) + ), + type: AccessType::MANDATORY, + key: new AccessKeyNode( + rangeInSource: $this->range([3, 16], [3, 18]), + value: PropertyName::from('bar') + ) + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([4, 4], [4, 18]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([4, 16], [4, 18]), + root: new StringLiteralNode( + rangeInSource: $this->range([4, 16], [4, 18]), + value: 'N/A' + ) + ) + ), + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesNullLiteral(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('null'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 3]), + root: new NullLiteralNode( + rangeInSource: $this->range([0, 0], [0, 3]) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesStringLiteral(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('"Hello World"'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 11]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 1], [0, 11]), + value: 'Hello World' + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTag(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('Bar!'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 22]), + root: new TagNode( + rangeInSource: $this->range([0, 0], [0, 22]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 3], [0, 12]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 3], [0, 6]), + value: AttributeName::from('href') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 9], [0, 12]), + value: '#foo' + ) + ) + ), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([0, 15], [0, 18]), + value: 'Bar!' + ) + ), + isSelfClosing: false + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTemplateLiteral(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('`Hello ${friend}!`'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 17]), + root: new TemplateLiteralNode( + rangeInSource: $this->range([0, 0], [0, 17]), + segments: new TemplateLiteralSegments( + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 1], [0, 6]), + value: 'Hello ' + ), + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 7], [0, 15]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 14]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 9], [0, 14]), + name: VariableName::from('friend') + ) + ) + ), + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 16], [0, 16]), + value: '!' + ), + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTernaryOperation(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a ? b : c'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 8]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 4], [0, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 4], [0, 4]), + name: VariableName::from('b') + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 8], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 8], [0, 8]), + name: VariableName::from('c') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesNestedBracketedTernaryOperation(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('(a ? b : c) ? (d ? e : f) : (g ? h : i)'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 38]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 10]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 1]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 1], [0, 1]), + name: VariableName::from('a') + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 5], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 5], [0, 5]), + name: VariableName::from('b') + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 9]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 9], [0, 9]), + name: VariableName::from('c') + ) + ) + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 14], [0, 24]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 15], [0, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 15], [0, 15]), + name: VariableName::from('d') + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 19], [0, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 19], [0, 19]), + name: VariableName::from('e') + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 23], [0, 23]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 23], [0, 23]), + name: VariableName::from('f') + ) + ) + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 28], [0, 38]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 29], [0, 29]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 29], [0, 29]), + name: VariableName::from('g') + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 33], [0, 33]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 33], [0, 33]), + name: VariableName::from('h') + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 37], [0, 37]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 37], [0, 37]), + name: VariableName::from('i') + ) + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesNestedUnbracketedTernaryOperation(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('a < b ? "yes" : (foo ? "maybe" : "no")'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 37]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 4]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 0]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::LESS_THAN, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 4], [0, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 4], [0, 4]), + name: VariableName::from('b') + ) + ), + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 11]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 9], [0, 11]), + value: 'yes' + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 16], [0, 37]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 17], [0, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 17], [0, 19]), + name: VariableName::from('foo') + ), + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 24], [0, 28]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 24], [0, 28]), + value: 'maybe' + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 34], [0, 35]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 34], [0, 35]), + value: 'no' + ) + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTernaryOperationWithComplexUnbracketedCondition(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator( + '1 < 2 === a || 5 > b || c === true && false ? "a" : "foo"' + ); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 55]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 42]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 42]), + operator: BinaryOperator::OR, + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 19]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 19]), + operator: BinaryOperator::OR, + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 10]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 10]), + operator: BinaryOperator::EQUAL, + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 4]), + operator: BinaryOperator::LESS_THAN, + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 0]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 0]), + format: IntegerFormat::DECIMAL, + value: '1' + ) + ), + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 4], [0, 4]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 4], [0, 4]), + format: IntegerFormat::DECIMAL, + value: '2' + ) + ), + ) + ), + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 10], [0, 10]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 10], [0, 10]), + name: VariableName::from('a') + ) + ) + ) + ), + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 15], [0, 19]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 15], [0, 19]), + operator: BinaryOperator::GREATER_THAN, + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 15], [0, 15]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 15], [0, 15]), + format: IntegerFormat::DECIMAL, + value: '5' + ) + ), + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 19], [0, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 19], [0, 19]), + name: VariableName::from('b') + ) + ) + ) + ) + ), + ), + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 24], [0, 42]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 24], [0, 42]), + operator: BinaryOperator::AND, + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 24], [0, 33]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 24], [0, 33]), + operator: BinaryOperator::EQUAL, + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 24], [0, 24]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 24], [0, 24]), + name: VariableName::from('c') + ) + ), + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 30], [0, 33]), + root: new BooleanLiteralNode( + rangeInSource: $this->range([0, 30], [0, 33]), + value: true + ) + ) + ) + ), + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 38], [0, 42]), + root: new BooleanLiteralNode( + rangeInSource: $this->range([0, 38], [0, 42]), + value: false + ) + ) + ) + ) + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 47], [0, 47]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 47], [0, 47]), + value: 'a' + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 53], [0, 55]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 53], [0, 55]), + value: 'foo' + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTernaryOperationWithComplexParentheses(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('(((foo)) === ((null))) ? 1 : (((0)))'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 35]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 21]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 1], [0, 20]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 7]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 3], [0, 5]), + name: VariableName::from('foo') + ) + ), + operator: BinaryOperator::EQUAL, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 13], [0, 20]), + root: new NullLiteralNode( + rangeInSource: $this->range([0, 15], [0, 18]) + ) + ), + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 25], [0, 25]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 25], [0, 25]), + format: IntegerFormat::DECIMAL, + value: '1' + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 29], [0, 35]), + root: new IntegerLiteralNode( + rangeInSource: $this->range([0, 32], [0, 32]), + format: IntegerFormat::DECIMAL, + value: '0' + ), + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesUnaryOperation(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('!a'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 1]), + root: new UnaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 1]), + operator: UnaryOperator::NOT, + operand: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 1]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 1], [0, 1]), + name: VariableName::from('a') + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesDoubleUnaryOperation(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('!!a'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 2]), + root: new UnaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 2]), + operator: UnaryOperator::NOT, + operand: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 2]), + root: new UnaryOperationNode( + rangeInSource: $this->range([0, 1], [0, 2]), + operator: UnaryOperator::NOT, + operand: new ExpressionNode( + rangeInSource: $this->range([0, 2], [0, 2]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 2], [0, 2]), + name: VariableName::from('a') + ) + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTripleUnaryOperation(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('!!!a'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 3]), + root: new UnaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 3]), + operator: UnaryOperator::NOT, + operand: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 3]), + root: new UnaryOperationNode( + rangeInSource: $this->range([0, 1], [0, 3]), + operator: UnaryOperator::NOT, + operand: new ExpressionNode( + rangeInSource: $this->range([0, 2], [0, 3]), + root: new UnaryOperationNode( + rangeInSource: $this->range([0, 2], [0, 3]), + operator: UnaryOperator::NOT, + operand: new ExpressionNode( + rangeInSource: $this->range([0, 3], [0, 3]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 3], [0, 3]), + name: VariableName::from('a') + ) + ) + ) + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesUnaryOperationWithBracketedExpressionAsOperand(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('!(a > b)'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 7]), + root: new UnaryOperationNode( + rangeInSource: $this->range([0, 0], [0, 7]), + operator: UnaryOperator::NOT, + operand: new ExpressionNode( + rangeInSource: $this->range([0, 1], [0, 7]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 2], [0, 6]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 2], [0, 2]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 2], [0, 2]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::GREATER_THAN, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 6]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 6], [0, 6]), + name: VariableName::from('b') + ) + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesValueReference(): void + { + $expressionParser = new ExpressionParser(); + $tokens = $this->createTokenIterator('foo'); + + $expectedExpressioNode = new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 2]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 2]), + name: VariableName::from('foo') + ) + ); + + $this->assertEquals( + $expectedExpressioNode, + $expressionParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMultipleParenthesesAroundValureReferenceCorrecly(): void + { + $expressionParser = new ExpressionParser(); + + $tokens = $this->createTokenIterator('(foo)'); + $this->assertEquals( + new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 1], [0, 3]), + name: VariableName::from('foo') + ) + ), + $expressionParser->parse($tokens) + ); + + $tokens = $this->createTokenIterator('((foo))'); + $this->assertEquals( + new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 6]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 2], [0, 4]), + name: VariableName::from('foo') + ) + ), + $expressionParser->parse($tokens) + ); + + $tokens = $this->createTokenIterator('(((foo)))'); + $this->assertEquals( + new ExpressionNode( + rangeInSource: $this->range([0, 0], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 3], [0, 5]), + name: VariableName::from('foo') + ) + ), + $expressionParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/Import/ImportParserTest.php b/test/Unit/Language/Parser/Import/ImportParserTest.php new file mode 100644 index 00000000..bf7779ed --- /dev/null +++ b/test/Unit/Language/Parser/Import/ImportParserTest.php @@ -0,0 +1,151 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\Import; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\InvalidImportedNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\Import\ImportCouldNotBeParsed; +use PackageFactory\ComponentEngine\Language\Parser\Import\ImportParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class ImportParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesImportWithOneName(): void + { + $importParser = ImportParser::singleton(); + $tokens = $this->createTokenIterator( + 'from "/some/where/in/the/filesystem" import { Foo }' + ); + + $expectedImportNode = new ImportNode( + rangeInSource: $this->range([0, 0], [0, 50]), + path: new StringLiteralNode( + rangeInSource: $this->range([0, 6], [0, 34]), + value: '/some/where/in/the/filesystem' + ), + names: new ImportedNameNodes( + new ImportedNameNode( + rangeInSource: $this->range([0, 46], [0, 48]), + value: VariableName::from('Foo') + ) + ) + ); + + $this->assertEquals( + $expectedImportNode, + $importParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesImportWithMultipleNames(): void + { + $importParser = ImportParser::singleton(); + $tokens = $this->createTokenIterator( + 'from "./some/other.component" import { Foo, Bar, Baz }' + ); + + $expectedImportNode = new ImportNode( + rangeInSource: $this->range([0, 0], [0, 53]), + path: new StringLiteralNode( + rangeInSource: $this->range([0, 6], [0, 27]), + value: './some/other.component' + ), + names: new ImportedNameNodes( + new ImportedNameNode( + rangeInSource: $this->range([0, 39], [0, 41]), + value: VariableName::from('Foo') + ), + new ImportedNameNode( + rangeInSource: $this->range([0, 44], [0, 46]), + value: VariableName::from('Bar') + ), + new ImportedNameNode( + rangeInSource: $this->range([0, 49], [0, 51]), + value: VariableName::from('Baz') + ) + ) + ); + + $this->assertEquals( + $expectedImportNode, + $importParser->parse($tokens) + ); + } + + /** + * @test + */ + public function throwsIfEmptyImportOccurs(): void + { + $this->assertThrowsParserException( + function () { + $importParser = ImportParser::singleton(); + $tokens = $this->createTokenIterator( + 'from "/some/where" import {}' + ); + + $importParser->parse($tokens); + }, + ImportCouldNotBeParsed::becauseOfInvalidImportedNameNodes( + cause: InvalidImportedNameNodes::becauseTheyWereEmpty(), + affectedRangeInSource: $this->range([0, 26], [0, 26]) + ) + ); + } + + /** + * @test + */ + public function throwsIfDuplicateImportsOccur(): void + { + $this->assertThrowsParserException( + function () { + $importParser = ImportParser::singleton(); + $tokens = $this->createTokenIterator( + 'from "/some/where" import { Foo, Bar, Baz, Bar, Qux }' + ); + + $importParser->parse($tokens); + }, + ImportCouldNotBeParsed::becauseOfInvalidImportedNameNodes( + cause: InvalidImportedNameNodes::becauseTheyContainDuplicates( + duplicateImportedNameNode: new ImportedNameNode( + rangeInSource: $this->range([0, 43], [0, 45]), + value: VariableName::from('Bar') + ) + ), + affectedRangeInSource: $this->range([0, 43], [0, 45]), + ) + ); + } +} diff --git a/test/Unit/Language/Parser/IntegerLiteral/IntegerLiteralParserTest.php b/test/Unit/Language/Parser/IntegerLiteral/IntegerLiteralParserTest.php new file mode 100644 index 00000000..5831a74a --- /dev/null +++ b/test/Unit/Language/Parser/IntegerLiteral/IntegerLiteralParserTest.php @@ -0,0 +1,161 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\IntegerLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerFormat; +use PackageFactory\ComponentEngine\Language\AST\Node\IntegerLiteral\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\IntegerLiteral\IntegerLiteralCouldNotBeParsed; +use PackageFactory\ComponentEngine\Language\Parser\IntegerLiteral\IntegerLiteralParser; +use PackageFactory\ComponentEngine\Parser\Source\Path; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class IntegerLiteralParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesBinaryInteger(): void + { + $integerLiteralParser = IntegerLiteralParser::singleton(); + $tokens = $this->createTokenIterator('0b1010110101'); + + $expectedIntegerLiteralNode = new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 11]), + format: IntegerFormat::BINARY, + value: '0b1010110101' + ); + + $this->assertEquals( + $expectedIntegerLiteralNode, + $integerLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesOctalInteger(): void + { + $integerLiteralParser = IntegerLiteralParser::singleton(); + $tokens = $this->createTokenIterator('0o755'); + + $expectedIntegerLiteralNode = new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 4]), + format: IntegerFormat::OCTAL, + value: '0o755' + ); + + $this->assertEquals( + $expectedIntegerLiteralNode, + $integerLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesDecimalInteger(): void + { + $integerLiteralParser = IntegerLiteralParser::singleton(); + $tokens = $this->createTokenIterator('1234567890'); + + $expectedIntegerLiteralNode = new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 9]), + format: IntegerFormat::DECIMAL, + value: '1234567890' + ); + + $this->assertEquals( + $expectedIntegerLiteralNode, + $integerLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesHexadecimalInteger(): void + { + $integerLiteralParser = IntegerLiteralParser::singleton(); + $tokens = $this->createTokenIterator('0x123456789ABCDEF'); + + $expectedIntegerLiteralNode = new IntegerLiteralNode( + rangeInSource: $this->range([0, 0], [0, 16]), + format: IntegerFormat::HEXADECIMAL, + value: '0x123456789ABCDEF' + ); + + $this->assertEquals( + $expectedIntegerLiteralNode, + $integerLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function throwsIfTokenStreamEndsUnexpectedly(): void + { + $this->assertThrowsParserException( + function () { + $integerLiteralParser = IntegerLiteralParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $integerLiteralParser->parse($tokens); + }, + IntegerLiteralCouldNotBeParsed::becauseOfUnexpectedEndOfFile() + ); + } + + /** + * @test + */ + public function throwsIfUnexpectedTokenIsEncountered(): void + { + $this->assertThrowsParserException( + function () { + $integerLiteralParser = IntegerLiteralParser::singleton(); + $tokens = $this->createTokenIterator('foo1234'); + + $integerLiteralParser->parse($tokens); + }, + IntegerLiteralCouldNotBeParsed::becauseOfUnexpectedToken( + expectedTokenTypes: TokenTypes::from( + TokenType::NUMBER_BINARY, + TokenType::NUMBER_OCTAL, + TokenType::NUMBER_DECIMAL, + TokenType::NUMBER_HEXADECIMAL + ), + actualToken: new Token( + type: TokenType::STRING, + value: 'foo1234', + boundaries: $this->range([0, 0], [0, 6]), + sourcePath: Path::createMemory() + ) + ) + ); + } +} diff --git a/test/Unit/Language/Parser/Match/MatchParserTest.php b/test/Unit/Language/Parser/Match/MatchParserTest.php new file mode 100644 index 00000000..a3c43936 --- /dev/null +++ b/test/Unit/Language/Parser/Match/MatchParserTest.php @@ -0,0 +1,1167 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\Match; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\InvalidMatchArmNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchArmNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Match\MatchNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\Match\MatchCouldNotBeParsed; +use PackageFactory\ComponentEngine\Language\Parser\Match\MatchParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class MatchParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesMatchWithOneArm(): void + { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator( + 'match (a) { b -> c }' + ); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [0, 19]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 12], [0, 17]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 12]), + name: VariableName::from('b') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 17], [0, 17]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 17], [0, 17]), + name: VariableName::from('c') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatchWithMultipleArms(): void + { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator( + 'match (a) { b -> c d -> e f -> g }' + ); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [0, 33]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 12], [0, 17]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 12]), + name: VariableName::from('b') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 17], [0, 17]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 17], [0, 17]), + name: VariableName::from('c') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([0, 19], [0, 24]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 19], [0, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 19], [0, 19]), + name: VariableName::from('d') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 24], [0, 24]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 24], [0, 24]), + name: VariableName::from('e') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([0, 26], [0, 31]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 26], [0, 26]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 26], [0, 26]), + name: VariableName::from('f') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 31], [0, 31]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 31], [0, 31]), + name: VariableName::from('g') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatchWithOneSummarizedArm(): void + { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator( + 'match (a) { b, c, d -> e }' + ); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [0, 25]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 12], [0, 23]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 12]), + name: VariableName::from('b') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 15], [0, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 15], [0, 15]), + name: VariableName::from('c') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 18], [0, 18]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 18], [0, 18]), + name: VariableName::from('d') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 23], [0, 23]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 23], [0, 23]), + name: VariableName::from('e') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatchWithMultipleSummarizedArms(): void + { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator( + 'match (a) { b, c, d -> e f, g, h -> i j, k, l -> m }' + ); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [0, 51]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 12], [0, 23]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 12]), + name: VariableName::from('b') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 15], [0, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 15], [0, 15]), + name: VariableName::from('c') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 18], [0, 18]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 18], [0, 18]), + name: VariableName::from('d') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 23], [0, 23]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 23], [0, 23]), + name: VariableName::from('e') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([0, 25], [0, 36]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 25], [0, 25]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 25], [0, 25]), + name: VariableName::from('f') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 28], [0, 28]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 28], [0, 28]), + name: VariableName::from('g') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 31], [0, 31]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 31], [0, 31]), + name: VariableName::from('h') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 36], [0, 36]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 36], [0, 36]), + name: VariableName::from('i') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([0, 38], [0, 49]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 38], [0, 38]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 38], [0, 38]), + name: VariableName::from('j') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 41], [0, 41]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 41], [0, 41]), + name: VariableName::from('k') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 44], [0, 44]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 44], [0, 44]), + name: VariableName::from('l') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 49], [0, 49]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 49], [0, 49]), + name: VariableName::from('m') + ) + ) + ), + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatchWithOnlyDefaultArm(): void + { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator( + 'match (a) { default -> b }' + ); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [0, 25]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 12], [0, 23]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([0, 23], [0, 23]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 23], [0, 23]), + name: VariableName::from('b') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatchWithOneArmAndDefaultArm(): void + { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator( + 'match (a) { b -> c default -> d }' + ); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [0, 32]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 12], [0, 17]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 12]), + name: VariableName::from('b') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 17], [0, 17]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 17], [0, 17]), + name: VariableName::from('c') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([0, 19], [0, 30]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([0, 30], [0, 30]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 30], [0, 30]), + name: VariableName::from('d') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatchWithOneSummarizedArmAndDefaultArm(): void + { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator( + 'match (a) { b, c, d -> e default -> f }' + ); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [0, 38]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 12], [0, 23]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 12]), + name: VariableName::from('b') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 15], [0, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 15], [0, 15]), + name: VariableName::from('c') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 18], [0, 18]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 18], [0, 18]), + name: VariableName::from('d') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 23], [0, 23]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 23], [0, 23]), + name: VariableName::from('e') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([0, 25], [0, 36]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([0, 36], [0, 36]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 36], [0, 36]), + name: VariableName::from('f') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesMatchWithMixedArms(): void + { + $matchParser = MatchParser::singleton(); + $matchAsString = << c + d, e, f -> g + default -> h + i, j -> k + l -> m + } + AFX; + $tokens = $this->createTokenIterator($matchAsString); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [6, 0]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([1, 4], [1, 9]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([1, 4], [1, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 4], [1, 4]), + name: VariableName::from('b') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([1, 9], [1, 9]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 9], [1, 9]), + name: VariableName::from('c') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([2, 4], [2, 15]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([2, 4], [2, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 4], [2, 4]), + name: VariableName::from('d') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([2, 7], [2, 7]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 7], [2, 7]), + name: VariableName::from('e') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([2, 10], [2, 10]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 10], [2, 10]), + name: VariableName::from('f') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([2, 15], [2, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 15], [2, 15]), + name: VariableName::from('g') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([3, 4], [3, 15]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([3, 15], [3, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 15], [3, 15]), + name: VariableName::from('h') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([4, 4], [4, 12]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([4, 4], [4, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([4, 4], [4, 4]), + name: VariableName::from('i') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([4, 7], [4, 7]), + root: new ValueReferenceNode( + rangeInSource: $this->range([4, 7], [4, 7]), + name: VariableName::from('j') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([4, 12], [4, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([4, 12], [4, 12]), + name: VariableName::from('k') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([5, 4], [5, 9]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([5, 4], [5, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([5, 4], [5, 4]), + name: VariableName::from('l') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([5, 9], [5, 9]), + root: new ValueReferenceNode( + rangeInSource: $this->range([5, 9], [5, 9]), + name: VariableName::from('m') + ) + ) + ), + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesNestedMatchAsSubject(): void + { + $matchParser = MatchParser::singleton(); + $matchAsString = << d default -> e }) { + d, e -> f + default -> g + } + AFX; + $tokens = $this->createTokenIterator($matchAsString); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [3, 0]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 43]), + root: new MatchNode( + rangeInSource: $this->range([0, 7], [0, 42]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 13], [0, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 14], [0, 14]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([0, 19], [0, 27]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 19], [0, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 19], [0, 19]), + name: VariableName::from('b') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([0, 22], [0, 22]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 22], [0, 22]), + name: VariableName::from('c') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([0, 27], [0, 27]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 27], [0, 27]), + name: VariableName::from('d') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([0, 29], [0, 40]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([0, 40], [0, 40]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 40], [0, 40]), + name: VariableName::from('e') + ) + ) + ), + ) + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([1, 4], [1, 12]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([1, 4], [1, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 4], [1, 4]), + name: VariableName::from('d') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([1, 7], [1, 7]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 7], [1, 7]), + name: VariableName::from('e') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([1, 12], [1, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 12], [1, 12]), + name: VariableName::from('f') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([2, 4], [2, 15]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([2, 15], [2, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 15], [2, 15]), + name: VariableName::from('g') + ) + ) + ), + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesNestedMatchAsArmLeft(): void + { + $matchParser = MatchParser::singleton(); + $matchAsString = << e default -> f } -> g + match (h) { i -> j }, + match (k) { l, m -> n default -> o } -> p + default -> q + } + AFX; + $tokens = $this->createTokenIterator($matchAsString); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [5, 0]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([1, 4], [1, 44]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([1, 4], [1, 39]), + root: new MatchNode( + rangeInSource: $this->range([1, 4], [1, 39]), + subject: new ExpressionNode( + rangeInSource: $this->range([1, 10], [1, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 11], [1, 11]), + name: VariableName::from('b') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([1, 16], [1, 24]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([1, 16], [1, 16]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 16], [1, 16]), + name: VariableName::from('c') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([1, 19], [1, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 19], [1, 19]), + name: VariableName::from('d') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([1, 24], [1, 24]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 24], [1, 24]), + name: VariableName::from('e') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([1, 26], [1, 37]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([1, 37], [1, 37]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 37], [1, 37]), + name: VariableName::from('f') + ) + ) + ), + ) + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([1, 44], [1, 44]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 44], [1, 44]), + name: VariableName::from('g') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([2, 4], [3, 44]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([2, 4], [2, 23]), + root: new MatchNode( + rangeInSource: $this->range([2, 4], [2, 23]), + subject: new ExpressionNode( + rangeInSource: $this->range([2, 10], [2, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 11], [2, 11]), + name: VariableName::from('h') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([2, 16], [2, 21]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([2, 16], [2, 16]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 16], [2, 16]), + name: VariableName::from('i') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([2, 21], [2, 21]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 21], [2, 21]), + name: VariableName::from('j') + ) + ) + ), + ) + ) + ), + new ExpressionNode( + rangeInSource: $this->range([3, 4], [3, 39]), + root: new MatchNode( + rangeInSource: $this->range([3, 4], [3, 39]), + subject: new ExpressionNode( + rangeInSource: $this->range([3, 10], [3, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 11], [3, 11]), + name: VariableName::from('k') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([3, 16], [3, 24]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([3, 16], [3, 16]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 16], [3, 16]), + name: VariableName::from('l') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([3, 19], [3, 19]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 19], [3, 19]), + name: VariableName::from('m') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([3, 24], [3, 24]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 24], [3, 24]), + name: VariableName::from('n') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([3, 26], [3, 37]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([3, 37], [3, 37]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 37], [3, 37]), + name: VariableName::from('o') + ) + ) + ), + ) + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([3, 44], [3, 44]), + root: new ValueReferenceNode( + rangeInSource: $this->range([3, 44], [3, 44]), + name: VariableName::from('p') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([4, 4], [4, 15]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([4, 15], [4, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([4, 15], [4, 15]), + name: VariableName::from('q') + ) + ) + ), + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesNestedMatchAsArmRight(): void + { + $matchParser = MatchParser::singleton(); + $matchAsString = << match (c) { d, e -> f default -> g } + default -> h + } + AFX; + $tokens = $this->createTokenIterator($matchAsString); + + $expectedMatchNode = new MatchNode( + rangeInSource: $this->range([0, 0], [3, 0]), + subject: new ExpressionNode( + rangeInSource: $this->range([0, 6], [0, 8]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 7], [0, 7]), + name: VariableName::from('a') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([1, 4], [1, 44]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([1, 4], [1, 4]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 4], [1, 4]), + name: VariableName::from('b') + ) + ), + ), + right: new ExpressionNode( + rangeInSource: $this->range([1, 9], [1, 44]), + root: new MatchNode( + rangeInSource: $this->range([1, 9], [1, 44]), + subject: new ExpressionNode( + rangeInSource: $this->range([1, 15], [1, 17]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 16], [1, 16]), + name: VariableName::from('c') + ) + ), + arms: new MatchArmNodes( + new MatchArmNode( + rangeInSource: $this->range([1, 21], [1, 29]), + left: new ExpressionNodes( + new ExpressionNode( + rangeInSource: $this->range([1, 21], [1, 21]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 21], [1, 21]), + name: VariableName::from('d') + ) + ), + new ExpressionNode( + rangeInSource: $this->range([1, 24], [1, 24]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 24], [1, 24]), + name: VariableName::from('e') + ) + ) + ), + right: new ExpressionNode( + rangeInSource: $this->range([1, 29], [1, 29]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 29], [1, 29]), + name: VariableName::from('f') + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([1, 31], [1, 42]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([1, 42], [1, 42]), + root: new ValueReferenceNode( + rangeInSource: $this->range([1, 42], [1, 42]), + name: VariableName::from('g') + ) + ) + ), + ) + ) + ) + ), + new MatchArmNode( + rangeInSource: $this->range([2, 4], [2, 15]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([2, 15], [2, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([2, 15], [2, 15]), + name: VariableName::from('h') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedMatchNode, + $matchParser->parse($tokens) + ); + } + + /** + * @test + */ + public function throwsIfMatchArmsAreEmpty(): void + { + $this->assertThrowsParserException( + function () { + $matchParser = MatchParser::singleton(); + $tokens = $this->createTokenIterator('match (a) {}'); + + $matchParser->parse($tokens); + }, + MatchCouldNotBeParsed::becauseOfInvalidMatchArmNodes( + cause: InvalidMatchArmNodes::becauseTheyWereEmpty(), + affectedRangeInSource: $this->range([0, 0], [0, 4]) + ) + ); + } + + /** + * @test + */ + public function throwsIfMultipleDefaultArmsOccur(): void + { + $this->assertThrowsParserException( + function () { + $matchParser = MatchParser::singleton(); + $matchAsString = << d + default -> e + f, g -> h + default -> i + j -> k + } + AFX; + $tokens = $this->createTokenIterator($matchAsString); + + $matchParser->parse($tokens); + }, + MatchCouldNotBeParsed::becauseOfInvalidMatchArmNodes( + cause: InvalidMatchArmNodes::becauseTheyContainMoreThanOneDefaultMatchArmNode( + secondDefaultMatchArmNode: new MatchArmNode( + rangeInSource: $this->range([4, 4], [4, 15]), + left: null, + right: new ExpressionNode( + rangeInSource: $this->range([4, 15], [4, 15]), + root: new ValueReferenceNode( + rangeInSource: $this->range([4, 15], [4, 15]), + name: VariableName::from('i') + ) + ) + ) + ) + ) + ); + } +} diff --git a/test/Unit/Language/Parser/Module/ModuleParserTest.php b/test/Unit/Language/Parser/Module/ModuleParserTest.php new file mode 100644 index 00000000..a54ae774 --- /dev/null +++ b/test/Unit/Language/Parser/Module/ModuleParserTest.php @@ -0,0 +1,314 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\Module; + +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Export\ExportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportedNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Import\ImportNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructNameNode; +use PackageFactory\ComponentEngine\Language\Parser\Module\ModuleCouldNotBeParsed; +use PackageFactory\ComponentEngine\Language\Parser\Module\ModuleParser; +use PackageFactory\ComponentEngine\Parser\Source\Path; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class ModuleParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesModuleWithNoImports(): void + { + $moduleParser = ModuleParser::singleton(); + $moduleAsString = <<createTokenIterator($moduleAsString); + + $expectedModuleNode = new ModuleNode( + rangeInSource: $this->range([0, 0], [0, 19]), + imports: new ImportNodes(), + export: new ExportNode( + rangeInSource: $this->range([0, 0], [0, 19]), + declaration: new StructDeclarationNode( + rangeInSource: $this->range([0, 7], [0, 19]), + name: new StructNameNode( + rangeInSource: $this->range([0, 14], [0, 16]), + value: StructName::from('Foo') + ), + properties: new PropertyDeclarationNodes() + ) + ) + ); + + $this->assertEquals( + $expectedModuleNode, + $moduleParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesModuleWithOneImport(): void + { + $moduleParser = ModuleParser::singleton(); + $moduleAsString = <<createTokenIterator($moduleAsString); + + $expectedModuleNode = new ModuleNode( + rangeInSource: $this->range([0, 0], [2, 19]), + imports: new ImportNodes( + new ImportNode( + rangeInSource: $this->range([0, 0], [0, 37]), + path: new StringLiteralNode( + rangeInSource: $this->range([0, 6], [0, 16]), + value: '/some/where' + ), + names: new ImportedNameNodes( + new ImportedNameNode( + rangeInSource: $this->range([0, 28], [0, 30]), + value: VariableName::from('Foo') + ), + new ImportedNameNode( + rangeInSource: $this->range([0, 33], [0, 35]), + value: VariableName::from('Bar') + ) + ) + ) + ), + export: new ExportNode( + rangeInSource: $this->range([2, 0], [2, 19]), + declaration: new StructDeclarationNode( + rangeInSource: $this->range([2, 7], [2, 19]), + name: new StructNameNode( + rangeInSource: $this->range([2, 14], [2, 16]), + value: StructName::from('Baz') + ), + properties: new PropertyDeclarationNodes() + ) + ) + ); + + $this->assertEquals( + $expectedModuleNode, + $moduleParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesModuleWithMultipleImports(): void + { + $moduleParser = ModuleParser::singleton(); + $moduleAsString = <<createTokenIterator($moduleAsString); + + $expectedModuleNode = new ModuleNode( + rangeInSource: $this->range([0, 0], [4, 21]), + imports: new ImportNodes( + new ImportNode( + rangeInSource: $this->range([0, 0], [0, 37]), + path: new StringLiteralNode( + rangeInSource: $this->range([0, 6], [0, 16]), + value: '/some/where' + ), + names: new ImportedNameNodes( + new ImportedNameNode( + rangeInSource: $this->range([0, 28], [0, 30]), + value: VariableName::from('Foo') + ), + new ImportedNameNode( + rangeInSource: $this->range([0, 33], [0, 35]), + value: VariableName::from('Bar') + ) + ) + ), + new ImportNode( + rangeInSource: $this->range([1, 0], [1, 37]), + path: new StringLiteralNode( + rangeInSource: $this->range([1, 6], [1, 21]), + value: '/some/where/else' + ), + names: new ImportedNameNodes( + new ImportedNameNode( + rangeInSource: $this->range([1, 33], [1, 35]), + value: VariableName::from('Baz') + ), + ) + ), + new ImportNode( + rangeInSource: $this->range([2, 0], [2, 33]), + path: new StringLiteralNode( + rangeInSource: $this->range([2, 6], [2, 11]), + value: './here' + ), + names: new ImportedNameNodes( + new ImportedNameNode( + rangeInSource: $this->range([2, 23], [2, 25]), + value: VariableName::from('Qux') + ), + new ImportedNameNode( + rangeInSource: $this->range([2, 28], [2, 31]), + value: VariableName::from('Quux') + ) + ) + ), + + ), + export: new ExportNode( + rangeInSource: $this->range([4, 0], [4, 21]), + declaration: new StructDeclarationNode( + rangeInSource: $this->range([4, 7], [4, 21]), + name: new StructNameNode( + rangeInSource: $this->range([4, 14], [4, 18]), + value: StructName::from('Corge') + ), + properties: new PropertyDeclarationNodes() + ) + ) + ); + + $this->assertEquals( + $expectedModuleNode, + $moduleParser->parse($tokens) + ); + } + + /** + * @test + */ + public function toleratesCommentsAndSpacesInBetweenStatements(): void + { + $moduleParser = ModuleParser::singleton(); + $moduleAsString = <<createTokenIterator($moduleAsString); + + $expectedModuleNode = new ModuleNode( + rangeInSource: $this->range([0, 0], [11, 19]), + imports: new ImportNodes( + new ImportNode( + rangeInSource: $this->range([5, 0], [5, 37]), + path: new StringLiteralNode( + rangeInSource: $this->range([5, 6], [5, 16]), + value: '/some/where' + ), + names: new ImportedNameNodes( + new ImportedNameNode( + rangeInSource: $this->range([5, 28], [5, 30]), + value: VariableName::from('Foo') + ), + new ImportedNameNode( + rangeInSource: $this->range([5, 33], [5, 35]), + value: VariableName::from('Bar') + ) + ) + ) + ), + export: new ExportNode( + rangeInSource: $this->range([11, 0], [11, 19]), + declaration: new StructDeclarationNode( + rangeInSource: $this->range([11, 7], [11, 19]), + name: new StructNameNode( + rangeInSource: $this->range([11, 14], [11, 16]), + value: StructName::from('Baz') + ), + properties: new PropertyDeclarationNodes() + ) + ) + ); + + $this->assertEquals( + $expectedModuleNode, + $moduleParser->parse($tokens) + ); + } + + /** + * @test + */ + public function throwsIfExceedingTokensOccur(): void + { + $this->assertThrowsParserException( + function () { + $moduleParser = ModuleParser::singleton(); + $moduleAsString = <<createTokenIterator($moduleAsString); + + $moduleParser->parse($tokens); + }, + ModuleCouldNotBeParsed::becauseOfUnexpectedExceedingToken( + exceedingToken: new Token( + type: TokenType::KEYWORD_EXPORT, + value: 'export', + boundaries: $this->range([4, 0], [4, 5]), + sourcePath: Path::createMemory() + ) + ) + ); + } +} diff --git a/test/Unit/Language/Parser/NullLiteral/NullLiteralParserTest.php b/test/Unit/Language/Parser/NullLiteral/NullLiteralParserTest.php new file mode 100644 index 00000000..6cc820e8 --- /dev/null +++ b/test/Unit/Language/Parser/NullLiteral/NullLiteralParserTest.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\NullLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\NullLiteral\NullLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\NullLiteral\NullLiteralParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class NullLiteralParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesNull(): void + { + $nullLiteralParser = NullLiteralParser::singleton(); + $tokens = $this->createTokenIterator('null'); + + $expectedNullLiteralNode = new NullLiteralNode( + rangeInSource: $this->range([0, 0], [0, 3]) + ); + + $this->assertEquals( + $expectedNullLiteralNode, + $nullLiteralParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/ParserTestCase.php b/test/Unit/Language/Parser/ParserTestCase.php new file mode 100644 index 00000000..15120259 --- /dev/null +++ b/test/Unit/Language/Parser/ParserTestCase.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser; + +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; +use PHPUnit\Framework\TestCase; + +abstract class ParserTestCase extends TestCase +{ + /** + * @param string $sourceAsString + * @return \Iterator + */ + protected function createTokenIterator(string $sourceAsString): \Iterator + { + $source = Source::fromString($sourceAsString); + $tokenizer = Tokenizer::fromSource($source); + + return $tokenizer->getIterator(); + } + + /** + * @param array{int,int} $startAsArray + * @param array{int,int} $endAsArray + * @return Range + */ + protected function range(array $startAsArray, array $endAsArray): Range + { + return Range::from( + new Position(...$startAsArray), + new Position(...$endAsArray) + ); + } + + protected function assertThrowsParserException(callable $fn, ParserException $expectedParserException): void + { + $this->expectExceptionObject($expectedParserException); + + try { + $fn(); + } catch (ParserException $e) { + $this->assertEquals($expectedParserException, $e); + throw $e; + } + } +} diff --git a/test/Unit/Language/Parser/PropertyDeclaration/PropertyDeclarationParserTest.php b/test/Unit/Language/Parser/PropertyDeclaration/PropertyDeclarationParserTest.php new file mode 100644 index 00000000..720d6360 --- /dev/null +++ b/test/Unit/Language/Parser/PropertyDeclaration/PropertyDeclarationParserTest.php @@ -0,0 +1,176 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\PropertyDeclaration; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\PropertyDeclaration\PropertyDeclarationParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class PropertyDeclarationParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesPropertyDeclarationWithSimpleType(): void + { + $propertyDeclarationParser = PropertyDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('foo: Bar'); + + $expectedPropertyDeclarationNode = new PropertyDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 7]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 0], [0, 2]), + value: PropertyName::from('foo') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 5], [0, 7]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: TypeName::from('Bar') + ) + ), + isArray: false, + isOptional: false + ) + ); + + $this->assertEquals( + $expectedPropertyDeclarationNode, + $propertyDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesPropertyDeclarationWithOptionalType(): void + { + $propertyDeclarationParser = PropertyDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('foo: ?Bar'); + + $expectedPropertyDeclarationNode = new PropertyDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 8]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 0], [0, 2]), + value: PropertyName::from('foo') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 5], [0, 8]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 6], [0, 8]), + value: TypeName::from('Bar') + ) + ), + isArray: false, + isOptional: true + ) + ); + + $this->assertEquals( + $expectedPropertyDeclarationNode, + $propertyDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesPropertyDeclarationWithArrayType(): void + { + $propertyDeclarationParser = PropertyDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('foo: Bar[]'); + + $expectedPropertyDeclarationNode = new PropertyDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 9]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 0], [0, 2]), + value: PropertyName::from('foo') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 5], [0, 9]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: TypeName::from('Bar') + ) + ), + isArray: true, + isOptional: false + ) + ); + + $this->assertEquals( + $expectedPropertyDeclarationNode, + $propertyDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesPropertyDeclarationWithUnionType(): void + { + $propertyDeclarationParser = PropertyDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('foo: Bar|Baz|Qux'); + + $expectedPropertyDeclarationNode = new PropertyDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 15]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 0], [0, 2]), + value: PropertyName::from('foo') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 5], [0, 15]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: TypeName::from('Bar') + ), + new TypeNameNode( + rangeInSource: $this->range([0, 9], [0, 11]), + value: TypeName::from('Baz') + ), + new TypeNameNode( + rangeInSource: $this->range([0, 13], [0, 15]), + value: TypeName::from('Qux') + ) + ), + isArray: false, + isOptional: false + ) + ); + + $this->assertEquals( + $expectedPropertyDeclarationNode, + $propertyDeclarationParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/StringLiteral/StringLiteralParserTest.php b/test/Unit/Language/Parser/StringLiteral/StringLiteralParserTest.php new file mode 100644 index 00000000..bfee07ec --- /dev/null +++ b/test/Unit/Language/Parser/StringLiteral/StringLiteralParserTest.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\StringLiteral; + +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\Parser\StringLiteral\StringLiteralParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class StringLiteralParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesString(): void + { + $stringLiteralParser = StringLiteralParser::singleton(); + $tokens = $this->createTokenIterator('"Hello World"'); + + $expectedStringLiteralNode = new StringLiteralNode( + rangeInSource: $this->range([0, 1], [0, 11]), + value: 'Hello World' + ); + + $this->assertEquals( + $expectedStringLiteralNode, + $stringLiteralParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/StructDeclaration/StructDeclarationParserTest.php b/test/Unit/Language/Parser/StructDeclaration/StructDeclarationParserTest.php new file mode 100644 index 00000000..87fff6e8 --- /dev/null +++ b/test/Unit/Language/Parser/StructDeclaration/StructDeclarationParserTest.php @@ -0,0 +1,324 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\StructDeclaration; + +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyDeclarationNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\PropertyDeclaration\PropertyNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\StructDeclaration\StructDeclarationParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class StructDeclarationParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesStructDeclarationWithOneProperty(): void + { + $structDeclarationParser = StructDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('struct Foo { bar: Baz }'); + + $expectedStructDeclarationNode = new StructDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 22]), + name: new StructNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: StructName::from('Foo') + ), + properties: new PropertyDeclarationNodes( + new PropertyDeclarationNode( + rangeInSource: $this->range([0, 13], [0, 20]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 13], [0, 15]), + value: PropertyName::from('bar') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 18], [0, 20]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 18], [0, 20]), + value: TypeName::from('Baz') + ) + ), + isArray: false, + isOptional: false + ) + ) + ) + ); + + $this->assertEquals( + $expectedStructDeclarationNode, + $structDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesStructDeclarationWithMultipleProperties(): void + { + $structDeclarationParser = StructDeclarationParser::singleton(); + $tokens = $this->createTokenIterator('struct Foo { bar: Baz qux: Quux corge: Grault }'); + + $expectedStructDeclarationNode = new StructDeclarationNode( + rangeInSource: $this->range([0, 0], [0, 46]), + name: new StructNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: StructName::from('Foo') + ), + properties: new PropertyDeclarationNodes( + new PropertyDeclarationNode( + rangeInSource: $this->range([0, 13], [0, 20]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 13], [0, 15]), + value: PropertyName::from('bar') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 18], [0, 20]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 18], [0, 20]), + value: TypeName::from('Baz') + ) + ), + isArray: false, + isOptional: false + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([0, 22], [0, 30]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 22], [0, 24]), + value: PropertyName::from('qux') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 27], [0, 30]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 27], [0, 30]), + value: TypeName::from('Quux') + ) + ), + isArray: false, + isOptional: false + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([0, 32], [0, 44]), + name: new PropertyNameNode( + rangeInSource: $this->range([0, 32], [0, 36]), + value: PropertyName::from('corge') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([0, 39], [0, 44]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 39], [0, 44]), + value: TypeName::from('Grault') + ) + ), + isArray: false, + isOptional: false + ) + ) + ) + ); + + $this->assertEquals( + $expectedStructDeclarationNode, + $structDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesStructDeclarationWithMultiplePropertiesAndSpaceAndComments(): void + { + $structDeclarationParser = StructDeclarationParser::singleton(); + $structAsString = <<createTokenIterator($structAsString); + + $expectedStructDeclarationNode = new StructDeclarationNode( + rangeInSource: $this->range([0, 0], [8, 0]), + name: new StructNameNode( + rangeInSource: $this->range([0, 7], [0, 10]), + value: StructName::from('Link') + ), + properties: new PropertyDeclarationNodes( + new PropertyDeclarationNode( + rangeInSource: $this->range([3, 4], [3, 15]), + name: new PropertyNameNode( + rangeInSource: $this->range([3, 4], [3, 7]), + value: PropertyName::from('href') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([3, 10], [3, 15]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([3, 10], [3, 15]), + value: TypeName::from('string') + ) + ), + isArray: false, + isOptional: false + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([6, 4], [6, 17]), + name: new PropertyNameNode( + rangeInSource: $this->range([6, 4], [6, 9]), + value: PropertyName::from('target') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([6, 12], [6, 17]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([6, 12], [6, 17]), + value: TypeName::from('string') + ) + ), + isArray: false, + isOptional: false + ) + ) + ) + ); + + $this->assertEquals( + $expectedStructDeclarationNode, + $structDeclarationParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesStructDeclarationWitOptionalArrayAndUnionProperties(): void + { + $structDeclarationParser = StructDeclarationParser::singleton(); + $structAsString = <<createTokenIterator($structAsString); + + $expectedStructDeclarationNode = new StructDeclarationNode( + rangeInSource: $this->range([0, 0], [4, 0]), + name: new StructNameNode( + rangeInSource: $this->range([0, 7], [0, 13]), + value: StructName::from('Picture') + ), + properties: new PropertyDeclarationNodes( + new PropertyDeclarationNode( + rangeInSource: $this->range([1, 4], [1, 16]), + name: new PropertyNameNode( + rangeInSource: $this->range([1, 4], [1, 6]), + value: PropertyName::from('src') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([1, 9], [1, 16]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([1, 9], [1, 14]), + value: TypeName::from('string') + ) + ), + isArray: true, + isOptional: false + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([2, 4], [2, 39]), + name: new PropertyNameNode( + rangeInSource: $this->range([2, 4], [2, 14]), + value: PropertyName::from('description') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([2, 17], [2, 39]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([2, 17], [2, 22]), + value: TypeName::from('string') + ), + new TypeNameNode( + rangeInSource: $this->range([2, 24], [2, 27]), + value: TypeName::from('slot') + ), + new TypeNameNode( + rangeInSource: $this->range([2, 29], [2, 39]), + value: TypeName::from('Description') + ) + ), + isArray: false, + isOptional: false + ) + ), + new PropertyDeclarationNode( + rangeInSource: $this->range([3, 4], [3, 17]), + name: new PropertyNameNode( + rangeInSource: $this->range([3, 4], [3, 8]), + value: PropertyName::from('title') + ), + type: new TypeReferenceNode( + rangeInSource: $this->range([3, 11], [3, 17]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([3, 12], [3, 17]), + value: TypeName::from('string') + ) + ), + isArray: false, + isOptional: true + ) + ), + ) + ); + + $this->assertEquals( + $expectedStructDeclarationNode, + $structDeclarationParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/Tag/TagParserTest.php b/test/Unit/Language/Parser/Tag/TagParserTest.php new file mode 100644 index 00000000..f43eeff1 --- /dev/null +++ b/test/Unit/Language/Parser/Tag/TagParserTest.php @@ -0,0 +1,1261 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\Tag; + +use PackageFactory\ComponentEngine\Domain\AttributeName\AttributeName; +use PackageFactory\ComponentEngine\Domain\TagName\TagName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\AttributeNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\ChildNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Tag\TagNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\Tag\TagParser; +use PackageFactory\ComponentEngine\Language\Parser\Tag\TagCouldNotBeParsed; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class TagParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesSelfClosingTagWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 3]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: true + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesSelfClosingTagWithValuelessAttribute(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 11]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 5]), + value: TagName::from('table') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 7], [0, 9]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: AttributeName::from('foo') + ), + value: null + ) + ), + children: new ChildNodes(), + isSelfClosing: true + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesSelfClosingTagWithMultipleValuelessAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator('
'); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 19]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 5]), + value: TagName::from('table') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 7], [0, 9]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: AttributeName::from('foo') + ), + value: null + ), + new AttributeNode( + rangeInSource: $this->range([0, 11], [0, 13]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: AttributeName::from('bar') + ), + value: null + ), + new AttributeNode( + rangeInSource: $this->range([0, 15], [0, 17]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 15], [0, 17]), + value: AttributeName::from('baz') + ), + value: null + ) + ), + children: new ChildNodes(), + isSelfClosing: true + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesSelfClosingTagWithStringAttribute(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 13]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 3], [0, 10]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 3], [0, 5]), + value: AttributeName::from('foo') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 8], [0, 10]), + value: 'bar' + ) + ) + ), + children: new ChildNodes(), + isSelfClosing: true + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesSelfClosingTagWithMultipleStringAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator('
'); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 38]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 3]), + value: TagName::from('div') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 5], [0, 12]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: AttributeName::from('foo') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 10], [0, 12]), + value: 'bar' + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 15], [0, 22]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 15], [0, 17]), + value: AttributeName::from('baz') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 20], [0, 22]), + value: 'qux' + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 25], [0, 35]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 25], [0, 28]), + value: AttributeName::from('quux') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 31], [0, 35]), + value: 'corge' + ) + ) + ), + children: new ChildNodes(), + isSelfClosing: true + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesSelfClosingTagWithExpressionAttribute(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 13]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 3], [0, 10]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 3], [0, 5]), + value: AttributeName::from('foo') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 8], [0, 10]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 8], [0, 10]), + name: VariableName::from('bar') + ) + ) + ) + ), + children: new ChildNodes(), + isSelfClosing: true + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesSelfClosingTagWithMultipleExpressionAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator('
'); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 38]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 3]), + value: TagName::from('div') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 5], [0, 12]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 5], [0, 7]), + value: AttributeName::from('foo') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 10], [0, 12]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 10], [0, 12]), + name: VariableName::from('bar') + ) + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 15], [0, 22]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 15], [0, 17]), + value: AttributeName::from('baz') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 20], [0, 22]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 20], [0, 22]), + name: VariableName::from('qux') + ) + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 25], [0, 35]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 25], [0, 28]), + value: AttributeName::from('quux') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 31], [0, 35]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 31], [0, 35]), + name: VariableName::from('corge') + ) + ) + ) + ), + children: new ChildNodes(), + isSelfClosing: true + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithEmptyContentAndWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 6]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function throwsIfClosingTagNameDoesNotMatchOpeningTagName(): void + { + $this->assertThrowsParserException( + function () { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $tagParser->parse($tokens); + }, + TagCouldNotBeParsed::becauseOfClosingTagNameMismatch( + expectedTagName: TagName::from('a'), + actualTagName: 'b', + affectedRangeInSource: $this->range([0, 5], [0, 5]) + ) + ); + } + + /** + * @test + */ + public function parsesTagWithEmptyContentAndValuelessAttribute(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 10]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 3], [0, 5]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 3], [0, 5]), + value: AttributeName::from('foo') + ), + value: null + ), + ), + children: new ChildNodes(), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithEmptyContentAndMultipleValuelessAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 18]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 3], [0, 5]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 3], [0, 5]), + value: AttributeName::from('foo') + ), + value: null + ), + new AttributeNode( + rangeInSource: $this->range([0, 7], [0, 9]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: AttributeName::from('bar') + ), + value: null + ), + new AttributeNode( + rangeInSource: $this->range([0, 11], [0, 13]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 11], [0, 13]), + value: AttributeName::from('baz') + ), + value: null + ), + ), + children: new ChildNodes(), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithEmptyContentAndStringAttribute(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 24]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 5]), + value: TagName::from('audio') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 7], [0, 14]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: AttributeName::from('foo') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 12], [0, 14]), + value: 'bar' + ) + ), + ), + children: new ChildNodes(), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithEmptyContentAndMultipleStringAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 47]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 5]), + value: TagName::from('video') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 7], [0, 14]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: AttributeName::from('foo') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 12], [0, 14]), + value: 'bar' + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 17], [0, 24]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 17], [0, 19]), + value: AttributeName::from('baz') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 22], [0, 24]), + value: 'qux' + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 27], [0, 37]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 27], [0, 30]), + value: AttributeName::from('quux') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 33], [0, 37]), + value: 'corge' + ) + ), + ), + children: new ChildNodes(), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithEmptyContentAndExpressionAttribute(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 24]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 5]), + value: TagName::from('audio') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 7], [0, 14]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: AttributeName::from('foo') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 14]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 14]), + name: VariableName::from('bar') + ) + ) + ), + ), + children: new ChildNodes(), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithEmptyContentAndMultipleExpressionAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 47]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 5]), + value: TagName::from('video') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 7], [0, 14]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 7], [0, 9]), + value: AttributeName::from('foo') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 12], [0, 14]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 12], [0, 14]), + name: VariableName::from('bar') + ) + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 17], [0, 24]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 17], [0, 19]), + value: AttributeName::from('baz') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 22], [0, 24]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 22], [0, 24]), + name: VariableName::from('qux') + ) + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 27], [0, 37]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 27], [0, 30]), + value: AttributeName::from('quux') + ), + value: new ExpressionNode( + rangeInSource: $this->range([0, 33], [0, 37]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 33], [0, 37]), + name: VariableName::from('corge') + ) + ) + ), + ), + children: new ChildNodes(), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithTextContentAndWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator('Lorem ipsum...'); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 20]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([0, 3], [0, 16]), + value: 'Lorem ipsum...' + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithExpressionContentAndWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator('{someExpression}'); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 22]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new ExpressionNode( + rangeInSource: $this->range([0, 4], [0, 17]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 4], [0, 17]), + name: VariableName::from('someExpression') + ) + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithNestedSelfClosingTagContentAndWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 10]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TagNode( + rangeInSource: $this->range([0, 3], [0, 6]), + name: new TagNameNode( + rangeInSource: $this->range([0, 4], [0, 4]), + value: TagName::from('b') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: true + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithNestedTagAndWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 13]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TagNode( + rangeInSource: $this->range([0, 3], [0, 9]), + name: new TagNameNode( + rangeInSource: $this->range([0, 4], [0, 4]), + value: TagName::from('b') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: false + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithNestedTagsOnMultipleLevelsAndWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 24]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TagNode( + rangeInSource: $this->range([0, 3], [0, 20]), + name: new TagNameNode( + rangeInSource: $this->range([0, 4], [0, 4]), + value: TagName::from('b') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TagNode( + rangeInSource: $this->range([0, 6], [0, 16]), + name: new TagNameNode( + rangeInSource: $this->range([0, 7], [0, 7]), + value: TagName::from('c') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TagNode( + rangeInSource: $this->range([0, 9], [0, 12]), + name: new TagNameNode( + rangeInSource: $this->range([0, 10], [0, 10]), + value: TagName::from('d') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: true + ) + ), + isSelfClosing: false + ) + ), + isSelfClosing: false + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithNestedTagInBetweenSpacesAndWithoutAttributes(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(' '); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 19]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TagNode( + rangeInSource: $this->range([0, 6], [0, 12]), + name: new TagNameNode( + rangeInSource: $this->range([0, 7], [0, 7]), + value: TagName::from('b') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: false + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithNestedTagInBetweenTextContentPreservingSpaceAroundTheNestedTag(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator('Something important happened.'); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 42]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([0, 3], [0, 12]), + value: 'Something ' + ), + new TagNode( + rangeInSource: $this->range([0, 13], [0, 28]), + name: new TagNameNode( + rangeInSource: $this->range([0, 14], [0, 14]), + value: TagName::from('b') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([0, 16], [0, 24]), + value: 'important' + ) + ), + isSelfClosing: false + ), + new TextNode( + rangeInSource: $this->range([0, 29], [0, 38]), + value: ' happened.' + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithExpressionInBetweenTextContentPreservingSpaceAroundTheExpression(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator('Something {variable} happened.'); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 36]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([0, 3], [0, 12]), + value: 'Something ' + ), + new ExpressionNode( + rangeInSource: $this->range([0, 14], [0, 21]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 14], [0, 21]), + name: VariableName::from('variable') + ) + ), + new TextNode( + rangeInSource: $this->range([0, 23], [0, 32]), + value: ' happened.' + ) + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithMultipleNestedTagsAsImmediateChildren(): void + { + $tagParser = TagParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [0, 24]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 1]), + value: TagName::from('a') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TagNode( + rangeInSource: $this->range([0, 3], [0, 9]), + name: new TagNameNode( + rangeInSource: $this->range([0, 4], [0, 4]), + value: TagName::from('b') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: false + ), + new TagNode( + rangeInSource: $this->range([0, 10], [0, 13]), + name: new TagNameNode( + rangeInSource: $this->range([0, 11], [0, 11]), + value: TagName::from('c') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: true + ), + new TagNode( + rangeInSource: $this->range([0, 14], [0, 20]), + name: new TagNameNode( + rangeInSource: $this->range([0, 15], [0, 15]), + value: TagName::from('d') + ), + attributes: new AttributeNodes(), + children: new ChildNodes(), + isSelfClosing: false + ), + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTagWithMultipleNestedTagsOnMultipleLevelsAllHavingAttributesAndContentsThemselves(): void + { + $tagParser = TagParser::singleton(); + $tagAsString = <<
+ AFX; + $tokens = $this->createTokenIterator($tagAsString); + + $expectedTagNode = new TagNode( + rangeInSource: $this->range([0, 0], [8, 5]), + name: new TagNameNode( + rangeInSource: $this->range([0, 1], [0, 3]), + value: TagName::from('div') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([0, 5], [0, 15]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 5], [0, 9]), + value: AttributeName::from('class') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([0, 12], [0, 15]), + value: 'test' + ) + ), + new AttributeNode( + rangeInSource: $this->range([0, 18], [0, 23]), + name: new AttributeNameNode( + rangeInSource: $this->range([0, 18], [0, 23]), + value: AttributeName::from('hidden') + ), + value: null + ), + ), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([0, 25], [2, 3]), + value: 'Some opening text' + ), + new TagNode( + rangeInSource: $this->range([2, 4], [2, 20]), + name: new TagNameNode( + rangeInSource: $this->range([2, 5], [2, 6]), + value: TagName::from('h1') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([2, 8], [2, 15]), + value: 'Headline' + ), + ), + isSelfClosing: false + ), + new TagNode( + rangeInSource: $this->range([3, 4], [3, 59]), + name: new TagNameNode( + rangeInSource: $this->range([3, 5], [3, 5]), + value: TagName::from('a') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([3, 7], [3, 23]), + name: new AttributeNameNode( + rangeInSource: $this->range([3, 7], [3, 10]), + value: AttributeName::from('href') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([3, 13], [3, 23]), + value: 'about:blank' + ) + ), + new AttributeNode( + rangeInSource: $this->range([3, 26], [3, 39]), + name: new AttributeNameNode( + rangeInSource: $this->range([3, 26], [3, 31]), + value: AttributeName::from('target') + ), + value: new StringLiteralNode( + rangeInSource: $this->range([3, 34], [3, 39]), + value: '_blank' + ) + ), + ), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([3, 42], [3, 55]), + value: 'This is a link' + ), + ), + isSelfClosing: false + ), + new TagNode( + rangeInSource: $this->range([4, 4], [6, 7]), + name: new TagNameNode( + rangeInSource: $this->range([4, 5], [4, 5]), + value: TagName::from('p') + ), + attributes: new AttributeNodes( + new AttributeNode( + rangeInSource: $this->range([4, 7], [4, 16]), + name: new AttributeNameNode( + rangeInSource: $this->range([4, 7], [4, 11]), + value: AttributeName::from('class') + ), + value: new ExpressionNode( + rangeInSource: $this->range([4, 14], [4, 16]), + root: new ValueReferenceNode( + rangeInSource: $this->range([4, 14], [4, 16]), + name: VariableName::from('rte') + ) + ) + ), + ), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([4, 19], [5, 17]), + value: 'This is a ' + ), + new ExpressionNode( + rangeInSource: $this->range([5, 19], [5, 27]), + root: new ValueReferenceNode( + rangeInSource: $this->range([5, 19], [5, 27]), + name: VariableName::from('paragraph') + ) + ), + new TextNode( + rangeInSource: $this->range([5, 29], [5, 34]), + value: ' with ' + ), + new TagNode( + rangeInSource: $this->range([5, 35], [5, 53]), + name: new TagNameNode( + rangeInSource: $this->range([5, 36], [5, 37]), + value: TagName::from('em') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([5, 39], [5, 48]), + value: 'emphasized' + ), + ), + isSelfClosing: false + ), + new TextNode( + rangeInSource: $this->range([5, 54], [5, 58]), + value: ' and ' + ), + new TagNode( + rangeInSource: $this->range([5, 59], [5, 83]), + name: new TagNameNode( + rangeInSource: $this->range([5, 60], [5, 65]), + value: TagName::from('strong') + ), + attributes: new AttributeNodes(), + children: new ChildNodes( + new TextNode( + rangeInSource: $this->range([5, 67], [5, 74]), + value: 'boldened' + ), + ), + isSelfClosing: false + ), + new TextNode( + rangeInSource: $this->range([5, 84], [6, 3]), + value: ' text.' + ) + ), + isSelfClosing: false + ), + new TextNode( + rangeInSource: $this->range([6, 8], [7, 21]), + value: 'Some closing text' + ), + ), + isSelfClosing: false + ); + + $this->assertEquals( + $expectedTagNode, + $tagParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Language/Parser/TemplateLiteral/TemplateLiteralParserTest.php b/test/Unit/Language/Parser/TemplateLiteral/TemplateLiteralParserTest.php new file mode 100644 index 00000000..d5b2c65b --- /dev/null +++ b/test/Unit/Language/Parser/TemplateLiteral/TemplateLiteralParserTest.php @@ -0,0 +1,347 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\TemplateLiteral; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\BinaryOperation\BinaryOperator; +use PackageFactory\ComponentEngine\Language\AST\Node\Expression\ExpressionNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StringLiteral\StringLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralExpressionSegmentNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralSegments; +use PackageFactory\ComponentEngine\Language\AST\Node\TemplateLiteral\TemplateLiteralStringSegmentNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TernaryOperation\TernaryOperationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\TemplateLiteral\TemplateLiteralParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class TemplateLiteralParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesTemplateLiteralWithoutEmbeddedExpressions(): void + { + $templateLiteralParser = TemplateLiteralParser::singleton(); + $tokens = $this->createTokenIterator('`Hello World`'); + + $expectedTemplateLiteralNode = new TemplateLiteralNode( + rangeInSource: $this->range([0, 0], [0, 12]), + segments: new TemplateLiteralSegments( + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 1], [0, 11]), + value: 'Hello World' + ) + ) + ); + + $this->assertEquals( + $expectedTemplateLiteralNode, + $templateLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTemplateLiteralWithOnlyEmbeddedExpression(): void + { + $templateLiteralParser = TemplateLiteralParser::singleton(); + $tokens = $this->createTokenIterator('`${foo}`'); + + $expectedTemplateLiteralNode = new TemplateLiteralNode( + rangeInSource: $this->range([0, 0], [0, 7]), + segments: new TemplateLiteralSegments( + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 1], [0, 6]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 3], [0, 5]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 3], [0, 5]), + name: VariableName::from('foo') + ) + ) + ) + ) + ); + + $this->assertEquals( + $expectedTemplateLiteralNode, + $templateLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTemplateLiteralWithLeadingAndTrailingStringSegments(): void + { + $templateLiteralParser = TemplateLiteralParser::singleton(); + $tokens = $this->createTokenIterator('`Hello ${friend}!`'); + + $expectedTemplateLiteralNode = new TemplateLiteralNode( + rangeInSource: $this->range([0, 0], [0, 17]), + segments: new TemplateLiteralSegments( + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 1], [0, 6]), + value: 'Hello ' + ), + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 7], [0, 15]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 14]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 9], [0, 14]), + name: VariableName::from('friend') + ) + ) + ), + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 16], [0, 16]), + value: '!' + ), + ) + ); + + $this->assertEquals( + $expectedTemplateLiteralNode, + $templateLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTemplateLiteralWithLeadingAndTrailingExpressionSegments(): void + { + $templateLiteralParser = TemplateLiteralParser::singleton(); + $tokens = $this->createTokenIterator('`${greeting} to you, ${friend}`'); + + $expectedTemplateLiteralNode = new TemplateLiteralNode( + rangeInSource: $this->range([0, 0], [0, 30]), + segments: new TemplateLiteralSegments( + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 1], [0, 11]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 3], [0, 10]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 3], [0, 10]), + name: VariableName::from('greeting') + ) + ) + ), + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 12], [0, 20]), + value: ' to you, ' + ), + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 21], [0, 29]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 23], [0, 28]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 23], [0, 28]), + name: VariableName::from('friend') + ) + ) + ), + ) + ); + + $this->assertEquals( + $expectedTemplateLiteralNode, + $templateLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTemplateLiteralWithComplexExpression(): void + { + $templateLiteralParser = TemplateLiteralParser::singleton(); + $tokens = $this->createTokenIterator( + '`The result is: ${a < b ? "yes" : (foo ? "maybe" : "no")}`' + ); + + $expectedTemplateLiteralNode = new TemplateLiteralNode( + rangeInSource: $this->range([0, 0], [0, 57]), + segments: new TemplateLiteralSegments( + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 1], [0, 15]), + value: 'The result is: ' + ), + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 16], [0, 56]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 18], [0, 55]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 18], [0, 22]), + root: new BinaryOperationNode( + rangeInSource: $this->range([0, 18], [0, 22]), + leftOperand: new ExpressionNode( + rangeInSource: $this->range([0, 18], [0, 18]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 18], [0, 18]), + name: VariableName::from('a') + ) + ), + operator: BinaryOperator::LESS_THAN, + rightOperand: new ExpressionNode( + rangeInSource: $this->range([0, 22], [0, 22]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 22], [0, 22]), + name: VariableName::from('b') + ) + ), + ) + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 27], [0, 29]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 27], [0, 29]), + value: 'yes' + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 34], [0, 55]), + root: new TernaryOperationNode( + condition: new ExpressionNode( + rangeInSource: $this->range([0, 35], [0, 37]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 35], [0, 37]), + name: VariableName::from('foo') + ), + ), + trueBranch: new ExpressionNode( + rangeInSource: $this->range([0, 42], [0, 46]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 42], [0, 46]), + value: 'maybe' + ) + ), + falseBranch: new ExpressionNode( + rangeInSource: $this->range([0, 52], [0, 53]), + root: new StringLiteralNode( + rangeInSource: $this->range([0, 52], [0, 53]), + value: 'no' + ) + ) + ) + ) + ) + ) + ), + ) + ); + + $this->assertEquals( + $expectedTemplateLiteralNode, + $templateLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTemplateLiteralWithEmbeddedTemplateLiteral(): void + { + $templateLiteralParser = TemplateLiteralParser::singleton(); + $tokens = $this->createTokenIterator('`Lorem ${`ipsum ${foo} sit`} amet`'); + + $expectedTemplateLiteralNode = new TemplateLiteralNode( + rangeInSource: $this->range([0, 0], [0, 33]), + segments: new TemplateLiteralSegments( + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 1], [0, 6]), + value: 'Lorem ' + ), + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 7], [0, 27]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 9], [0, 26]), + root: new TemplateLiteralNode( + rangeInSource: $this->range([0, 9], [0, 26]), + segments: new TemplateLiteralSegments( + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 10], [0, 15]), + value: 'ipsum ' + ), + new TemplateLiteralExpressionSegmentNode( + rangeInSource: $this->range([0, 16], [0, 21]), + expression: new ExpressionNode( + rangeInSource: $this->range([0, 18], [0, 20]), + root: new ValueReferenceNode( + rangeInSource: $this->range([0, 18], [0, 20]), + name: VariableName::from('foo') + ) + ) + ), + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 22], [0, 25]), + value: ' sit' + ) + ) + ) + ) + ), + new TemplateLiteralStringSegmentNode( + rangeInSource: $this->range([0, 28], [0, 32]), + value: ' amet' + ) + ) + ); + + $this->assertEquals( + $expectedTemplateLiteralNode, + $templateLiteralParser->parse($tokens) + ); + } + + /** + * @test + */ + public function toleratesIsolatedDollarSigns(): void + { + $this->markTestSkipped('@TODO: This will require significant redesign of the tokenizer.'); + + // $templateLiteralParser = TemplateLiteralParser::singleton(); + // $tokens = $this->createTokenIterator('`$$$$$$$$`'); + + // $expectedTemplateLiteralNode = new TemplateLiteralNode( + // rangeInSource: $this->range([0, 0], [0, 9]), + // segments: new TemplateLiteralSegments( + // new TemplateLiteralStringSegmentNode( + // rangeInSource: $this->range([0, 1], [0, 8]), + // value: '$$$$$$$$' + // ) + // ) + // ); + + // $this->assertEquals( + // $expectedTemplateLiteralNode, + // $templateLiteralParser->parse($tokens) + // ); + } +} diff --git a/test/Unit/Language/Parser/Text/TextParserTest.php b/test/Unit/Language/Parser/Text/TextParserTest.php new file mode 100644 index 00000000..04edcbe6 --- /dev/null +++ b/test/Unit/Language/Parser/Text/TextParserTest.php @@ -0,0 +1,334 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\Text; + +use PackageFactory\ComponentEngine\Language\AST\Node\Text\TextNode; +use PackageFactory\ComponentEngine\Language\Parser\Text\TextParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class TextParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesEmptyStringToNull(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(''); + + $this->assertNull( + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesTextWithSpacesOnlyToNull(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" \t \n \t "); + + $this->assertNull($textParser->parse($tokens)); + } + + /** + * @test + */ + public function parsesTrivialText(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator('Hello World'); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [0, 10]), + value: 'Hello World' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function trimsLeadingAndTrailingSpaces(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" \t\t Hello World \t\t "); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [0, 22]), + value: 'Hello World' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function trimsLeadingLineBreak(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("\nHello World"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [1, 10]), + value: 'Hello World' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function trimsLeadingLineBreakAndIndentation(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("\n Hello World"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [1, 14]), + value: 'Hello World' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function preservesLeadingSpaceIfFlagIsSet(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" \t\t Hello World \t\t "); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [0, 22]), + value: ' Hello World' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens, true) + ); + } + + /** + * @test + */ + public function reducesInnerSpacesToSingleSpaceCharacterEach(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("Hello \t \n \t folks and\t\t\tpeople"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [1, 22]), + value: 'Hello folks and people' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function terminatesAtEmbeddedExpressionAndTrimsLeadingSpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" Hello{"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [0, 8]), + value: 'Hello' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function terminatesAtEmbeddedExpressionAndKeepsTrailingSpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("Hello \t {foo}!"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [0, 7]), + value: 'Hello ' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function terminatesAtEmbeddedExpressionAndTrimsTrailingSpaceIfItContainsLineBreaks(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("Hello \n\t {foo}!"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [1, 1]), + value: 'Hello' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function returnsNullAtEmbeddedExpressionIfTheresOnlySpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" \n\t {foo}!"); + + $this->assertNull($textParser->parse($tokens)); + } + + /** + * @test + */ + public function terminatesAtOpeningTagAndTrimsLeadingSpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" Hello"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [0, 8]), + value: 'Hello' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function terminatesAtOpeningTagAndKeepsTrailingSpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("Hello \t World"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [0, 7]), + value: 'Hello ' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function terminatesAtOpeningTagAndTrimsTrailingSpaceIfItContainsLineBreaks(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("Hello \n\t World"); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [1, 1]), + value: 'Hello' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function returnsNullAtOpeningTagIfTheresOnlySpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" \n\t "); + + $this->assertNull($textParser->parse($tokens)); + } + + /** + * @test + */ + public function terminatesAtClosingTagAndTrimsTrailingSpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator("World \n\t "); + + $expectedTextNode = new TextNode( + rangeInSource: $this->range([0, 0], [1, 1]), + value: 'World' + ); + + $this->assertEquals( + $expectedTextNode, + $textParser->parse($tokens) + ); + } + + /** + * @test + */ + public function returnsNullAtClosingTagIfTheresOnlySpace(): void + { + $textParser = TextParser::singleton(); + $tokens = $this->createTokenIterator(" \n\t "); + + $this->assertNull($textParser->parse($tokens)); + } +} diff --git a/test/Unit/Language/Parser/TypeReference/TypeReferenceParserTest.php b/test/Unit/Language/Parser/TypeReference/TypeReferenceParserTest.php new file mode 100644 index 00000000..d0f95713 --- /dev/null +++ b/test/Unit/Language/Parser/TypeReference/TypeReferenceParserTest.php @@ -0,0 +1,194 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\TypeReference; + +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\InvalidTypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeNameNodes; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\TypeReference\TypeReferenceParser; +use PackageFactory\ComponentEngine\Language\Parser\ParserException; +use PackageFactory\ComponentEngine\Language\Parser\TypeReference\TypeReferenceCouldNotBeParsed; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class TypeReferenceParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesSimpleTypeReference(): void + { + $typeReferenceParser = TypeReferenceParser::singleton(); + $tokens = $this->createTokenIterator('Foo'); + + $expectedTypeReferenceNode = new TypeReferenceNode( + rangeInSource: $this->range([0, 0], [0, 2]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 0], [0, 2]), + value: TypeName::from('Foo') + ) + ), + isArray: false, + isOptional: false + ); + + $this->assertEquals( + $expectedTypeReferenceNode, + $typeReferenceParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesArrayTypeReference(): void + { + $typeReferenceParser = TypeReferenceParser::singleton(); + $tokens = $this->createTokenIterator('Foo[]'); + + $expectedTypeReferenceNode = new TypeReferenceNode( + rangeInSource: $this->range([0, 0], [0, 4]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 0], [0, 2]), + value: TypeName::from('Foo') + ) + ), + isArray: true, + isOptional: false + ); + + $this->assertEquals( + $expectedTypeReferenceNode, + $typeReferenceParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesOptionalTypeReference(): void + { + $typeReferenceParser = TypeReferenceParser::singleton(); + $tokens = $this->createTokenIterator('?Foo'); + + $expectedTypeReferenceNode = new TypeReferenceNode( + rangeInSource: $this->range([0, 0], [0, 3]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 1], [0, 3]), + value: TypeName::from('Foo') + ) + ), + isArray: false, + isOptional: true + ); + + $this->assertEquals( + $expectedTypeReferenceNode, + $typeReferenceParser->parse($tokens) + ); + } + + /** + * @test + */ + public function parsesUnionTypeReference(): void + { + $typeReferenceParser = TypeReferenceParser::singleton(); + $tokens = $this->createTokenIterator('Foo|Bar|Baz'); + + $expectedTypeReferenceNode = new TypeReferenceNode( + rangeInSource: $this->range([0, 0], [0, 10]), + names: new TypeNameNodes( + new TypeNameNode( + rangeInSource: $this->range([0, 0], [0, 2]), + value: TypeName::from('Foo') + ), + new TypeNameNode( + rangeInSource: $this->range([0, 4], [0, 6]), + value: TypeName::from('Bar') + ), + new TypeNameNode( + rangeInSource: $this->range([0, 8], [0, 10]), + value: TypeName::from('Baz') + ) + ), + isArray: false, + isOptional: false + ); + + $this->assertEquals( + $expectedTypeReferenceNode, + $typeReferenceParser->parse($tokens) + ); + } + + /** + * @test + */ + public function throwsIfInvalidTypeReferenceOccurs(): void + { + $typeReferenceParser = TypeReferenceParser::singleton(); + $tokens = $this->createTokenIterator('?Foo[]'); + + $this->expectException(ParserException::class); + $this->expectExceptionObject( + TypeReferenceCouldNotBeParsed::becauseOfInvalidTypeReferenceNode( + cause: InvalidTypeReferenceNode::becauseItWasOptionalAndArrayAtTheSameTime( + affectedTypeNames: new TypeNames(TypeName::from('Foo')), + affectedRangeInSource: $this->range([0, 0], [0, 4]), + ) + ) + ); + + $typeReferenceParser->parse($tokens); + } + + /** + * @test + */ + public function throwsIfDuplicatesOccurInUnionTypeReference(): void + { + $typeReferenceParser = TypeReferenceParser::singleton(); + $tokens = $this->createTokenIterator('Foo|Bar|Foo|Baz'); + + $this->expectException(ParserException::class); + $this->expectExceptionObject( + TypeReferenceCouldNotBeParsed::becauseOfInvalidTypeTypeNameNodes( + cause: InvalidTypeNameNodes::becauseTheyContainDuplicates( + duplicateTypeNameNode: new TypeNameNode( + rangeInSource: $this->range([0, 9], [0, 11]), + value: TypeName::from('Foo') + ) + ) + ) + ); + + $typeReferenceParser->parse($tokens); + } +} diff --git a/test/Unit/Language/Parser/ValueReference/ValueReferenceParserTest.php b/test/Unit/Language/Parser/ValueReference/ValueReferenceParserTest.php new file mode 100644 index 00000000..a73e70da --- /dev/null +++ b/test/Unit/Language/Parser/ValueReference/ValueReferenceParserTest.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ValueReference; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Language\AST\Node\ValueReference\ValueReferenceNode; +use PackageFactory\ComponentEngine\Language\Parser\ValueReference\ValueReferenceParser; +use PackageFactory\ComponentEngine\Test\Unit\Language\Parser\ParserTestCase; + +final class ValueReferenceParserTest extends ParserTestCase +{ + /** + * @test + */ + public function parsesValueReference(): void + { + $valueReferenceParser = ValueReferenceParser::singleton(); + $tokens = $this->createTokenIterator('foo'); + + $expectedValueReferenceNode = new ValueReferenceNode( + rangeInSource: $this->range([0, 0], [0, 2]), + name: VariableName::from('foo') + ); + + $this->assertEquals( + $expectedValueReferenceNode, + $valueReferenceParser->parse($tokens) + ); + } +} diff --git a/test/Unit/Module/Fixtures/DummyModule.php b/test/Unit/Module/Fixtures/DummyModule.php new file mode 100644 index 00000000..5d5b97d6 --- /dev/null +++ b/test/Unit/Module/Fixtures/DummyModule.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Module\Fixtures; + +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Module\ModuleInterface; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; + +final class DummyModule implements ModuleInterface +{ + /** + * @param array $data + */ + public function __construct(private readonly array $data = []) + { + } + + public function getTypeOf(VariableName $exportName): AtomicTypeInterface + { + if ($type = $this->data[$exportName->value] ?? null) { + return $type; + } + + throw new \Exception( + '[DummyModule] Module does not export "' . $exportName->value . '".' + ); + } +} diff --git a/test/Unit/Module/Loader/Fixtures/DummyLoader.php b/test/Unit/Module/Loader/Fixtures/DummyLoader.php index f74a6a80..42a8b2cc 100644 --- a/test/Unit/Module/Loader/Fixtures/DummyLoader.php +++ b/test/Unit/Module/Loader/Fixtures/DummyLoader.php @@ -23,32 +23,38 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Module\Loader\Fixtures; use PackageFactory\ComponentEngine\Module\LoaderInterface; -use PackageFactory\ComponentEngine\Parser\Ast\ImportNode; -use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; +use PackageFactory\ComponentEngine\Module\ModuleInterface; +use PackageFactory\ComponentEngine\Test\Unit\Module\Fixtures\DummyModule; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; final class DummyLoader implements LoaderInterface { /** - * @param array> $data + * @var array */ - public function __construct(private readonly array $data = []) + private readonly array $data; + + /** + * @param array> $data + */ + public function __construct(array $data = []) { + $thisData = []; + foreach ($data as $key => $moduleData) { + $thisData[$key] = new DummyModule($moduleData); + } + + $this->data = $thisData; } - public function resolveTypeOfImport(ImportNode $importNode): TypeInterface + public function loadModule(string $pathToModule): ModuleInterface { - if ($moduleData = $this->data[$importNode->path] ?? null) { - if ($type = $moduleData[$importNode->name->value] ?? null) { - return $type; - } - - throw new \Exception( - '[DummyLoader] Cannot import "' . $importNode->name->value . '" from "' . $importNode->source->path->value . '"' - ); + if ($module = $this->data[$pathToModule] ?? null) { + return $module; } throw new \Exception( - '[DummyLoader] Unknown Import: ' . json_encode($importNode, JSON_PRETTY_PRINT) + '[DummyLoader] Module at path "' . $pathToModule . '" does not exist' ); } } diff --git a/test/Unit/Parser/Source/PathTest.php b/test/Unit/Parser/Source/PathTest.php index 0c94b311..f426ceba 100644 --- a/test/Unit/Parser/Source/PathTest.php +++ b/test/Unit/Parser/Source/PathTest.php @@ -94,4 +94,40 @@ public function resolvesRelationToOtherPath(string $pathAsString, string $otherP $path->resolveRelationTo($otherPath)->value ); } + + /** + * @test + */ + public function equalsOtherPathIfValuesAreIdentical(): void + { + $path = Path::fromString('/some/where/in/the/filesystem'); + $otherPath = Path::fromString('/some/where/in/the/filesystem'); + + $this->assertTrue($path->equals($otherPath)); + $this->assertTrue($otherPath->equals($path)); + } + + /** + * @test + */ + public function doesNotEqualOtherPathIfValuesAreNotIdentical(): void + { + $path = Path::fromString('/some/where/in/the/filesystem'); + $otherPath = Path::fromString('/else/where/in/the/filesystem'); + + $this->assertFalse($path->equals($otherPath)); + $this->assertFalse($otherPath->equals($path)); + } + + /** + * @test + */ + public function memoryPathsEqualEachOther(): void + { + $path = Path::createMemory(); + $otherPath = Path::createMemory(); + + $this->assertTrue($path->equals($otherPath)); + $this->assertTrue($otherPath->equals($path)); + } } diff --git a/test/Unit/Parser/Source/PositionTest.php b/test/Unit/Parser/Source/PositionTest.php new file mode 100644 index 00000000..1edadb08 --- /dev/null +++ b/test/Unit/Parser/Source/PositionTest.php @@ -0,0 +1,38 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Parser\Source; + +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PHPUnit\Framework\TestCase; + +final class PositionTest extends TestCase +{ + /** + * @test + */ + public function providesDebugStringRepresentingItself(): void + { + $position = new Position(42, 102); + $this->assertEquals('line 42, column 102', $position->toDebugString()); + } +} diff --git a/test/Unit/Parser/Tokenizer/Fixtures.php b/test/Unit/Parser/Tokenizer/Fixtures.php new file mode 100644 index 00000000..cfe7a57b --- /dev/null +++ b/test/Unit/Parser/Tokenizer/Fixtures.php @@ -0,0 +1,42 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Parser\Tokenizer; + +use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; + +final class Fixtures +{ + /** + * @param string $sourceAsString + * @return \Iterator + */ + public static function tokens(string $sourceAsString): \Iterator + { + $source = Source::fromString($sourceAsString); + $tokenizer = Tokenizer::fromSource($source); + + return $tokenizer->getIterator(); + } +} diff --git a/test/Unit/Definition/AccessTypeTest.php b/test/Unit/Parser/Tokenizer/TokenTest.php similarity index 50% rename from test/Unit/Definition/AccessTypeTest.php rename to test/Unit/Parser/Tokenizer/TokenTest.php index 2b79eee4..44159ce8 100644 --- a/test/Unit/Definition/AccessTypeTest.php +++ b/test/Unit/Parser/Tokenizer/TokenTest.php @@ -2,7 +2,7 @@ /** * PackageFactory.ComponentEngine - Universal View Components for PHP - * Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine + * Copyright (C) 2023 Contributors of PackageFactory.ComponentEngine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,34 +20,36 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Test\Unit\Definition; +namespace PackageFactory\ComponentEngine\Test\Unit\Parser\Tokenizer; -use PackageFactory\ComponentEngine\Definition\AccessType; +use PackageFactory\ComponentEngine\Parser\Source\Path; +use PackageFactory\ComponentEngine\Parser\Source\Position; +use PackageFactory\ComponentEngine\Parser\Source\Range; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Token; use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; use PHPUnit\Framework\TestCase; -final class AccessTypeTest extends TestCase +final class TokenTest extends TestCase { - /** - * @return array - */ - public static function tokenTypeToAccessTypeExamples(): array - { - return [ - TokenType::PERIOD->name => [TokenType::PERIOD, AccessType::MANDATORY], - TokenType::OPTCHAIN->name => [TokenType::OPTCHAIN, AccessType::OPTIONAL], - ]; - } - /** * @test - * @dataProvider tokenTypeToAccessTypeExamples - * @param TokenType $givenTokenType - * @param AccessType $expectedAccessType - * @return void */ - public function canBeCreatedFromTokenType(TokenType $givenTokenType, AccessType $expectedAccessType): void + public function providesDebugString(): void { - $this->assertSame($expectedAccessType, AccessType::fromTokenType($givenTokenType)); + $token = new Token( + type: TokenType::COMMENT, + value: '# This is a comment', + boundaries: Range::from( + new Position(0, 0), + new Position(0, 0) + ), + sourcePath: Path::createMemory() + ); + + $this->assertEquals( + 'COMMENT ("# This is a comment")', + $token->toDebugString() + ); } } diff --git a/test/Unit/Parser/Tokenizer/TokenTypesTest.php b/test/Unit/Parser/Tokenizer/TokenTypesTest.php new file mode 100644 index 00000000..5afe1e4d --- /dev/null +++ b/test/Unit/Parser/Tokenizer/TokenTypesTest.php @@ -0,0 +1,110 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Parser\Tokenizer; + +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenTypes; +use PHPUnit\Framework\TestCase; + +final class TokenTypesTest extends TestCase +{ + /** + * @test + */ + public function providesDebugStringForSingleItem(): void + { + $tokenTypes = TokenTypes::from(TokenType::COLON); + + $this->assertEquals( + 'COLON (":")', + $tokenTypes->toDebugString() + ); + } + + /** + * @test + */ + public function providesDebugStringForTwoItems(): void + { + $tokenTypes = TokenTypes::from(TokenType::PERIOD, TokenType::COMMA); + + $this->assertEquals( + 'PERIOD (".") or COMMA (",")', + $tokenTypes->toDebugString() + ); + } + + /** + * @test + */ + public function providesDebugStringForThreeOrMoreItems(): void + { + $tokenTypes = TokenTypes::from( + TokenType::PERIOD, + TokenType::COMMA, + TokenType::COLON, + TokenType::DOLLAR + ); + + $this->assertEquals( + 'PERIOD ("."), COMMA (","), COLON (":") or DOLLAR ("$")', + $tokenTypes->toDebugString() + ); + } + + /** + * @test + */ + public function containsReturnsTrueIfCollectionContainsGivenTokenType(): void + { + $tokenTypes = TokenTypes::from( + TokenType::PERIOD, + TokenType::COMMA, + TokenType::COLON, + TokenType::DOLLAR + ); + + $this->assertTrue($tokenTypes->contains(TokenType::PERIOD)); + $this->assertTrue($tokenTypes->contains(TokenType::COMMA)); + $this->assertTrue($tokenTypes->contains(TokenType::COLON)); + $this->assertTrue($tokenTypes->contains(TokenType::DOLLAR)); + } + + /** + * @test + */ + public function containsReturnsFalseIfCollectionDoesNotContainGivenTokenType(): void + { + $tokenTypes = TokenTypes::from( + TokenType::PERIOD, + TokenType::COMMA, + TokenType::COLON, + TokenType::DOLLAR + ); + + $this->assertFalse($tokenTypes->contains(TokenType::SLASH_FORWARD)); + $this->assertFalse($tokenTypes->contains(TokenType::COMMENT)); + $this->assertFalse($tokenTypes->contains(TokenType::STRING)); + $this->assertFalse($tokenTypes->contains(TokenType::EQUALS)); + } +} diff --git a/test/Unit/Parser/Tokenizer/TokenizerTest.php b/test/Unit/Parser/Tokenizer/TokenizerTest.php new file mode 100644 index 00000000..27ab6601 --- /dev/null +++ b/test/Unit/Parser/Tokenizer/TokenizerTest.php @@ -0,0 +1,168 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Parser\Tokenizer; + +use PackageFactory\ComponentEngine\Parser\Source\Source; +use PackageFactory\ComponentEngine\Parser\Tokenizer\Tokenizer; +use PackageFactory\ComponentEngine\Parser\Tokenizer\TokenType; +use PHPUnit\Framework\TestCase; + +final class TokenizerTest extends TestCase +{ + /** + * @test + */ + public function tokenizesEmptySourceToEmptyIterator(): void + { + $source = Source::fromString(''); + $tokenizer = Tokenizer::fromSource($source); + $iterator = $tokenizer->getIterator(); + + $this->assertFalse($iterator->valid()); + } + + /** + * @test + */ + public function tokenizesOpeningTag(): void + { + $source = Source::fromString(''); + $tokenizer = Tokenizer::fromSource($source); + $tokens = \iterator_to_array($tokenizer->getIterator(), false); + + $this->assertEquals(TokenType::TAG_START_OPENING, $tokens[0]->type); + $this->assertEquals(TokenType::STRING, $tokens[1]->type); + $this->assertEquals(TokenType::TAG_END, $tokens[2]->type); + } + + /** + * @test + */ + public function tokenizesClosingTag(): void + { + $source = Source::fromString(''); + $tokenizer = Tokenizer::fromSource($source); + $tokens = \iterator_to_array($tokenizer->getIterator(), false); + + $this->assertEquals(TokenType::TAG_START_CLOSING, $tokens[0]->type); + $this->assertEquals(TokenType::STRING, $tokens[1]->type); + $this->assertEquals(TokenType::TAG_END, $tokens[2]->type); + } + + /** + * @test + */ + public function tokenizesMultipleBracketedStatements(): void + { + $source = Source::fromString('(a ? b : c) ? (d ? e : f) : (g ? h : i)'); + $tokenizer = Tokenizer::fromSource($source); + $tokens = \iterator_to_array($tokenizer->getIterator(), false); + + $this->assertEquals(TokenType::BRACKET_ROUND_OPEN, $tokens[0]->type); + + $this->assertEquals(TokenType::STRING, $tokens[1]->type); + $this->assertEquals('a', $tokens[1]->value); + + $this->assertEquals(TokenType::SPACE, $tokens[2]->type); + + $this->assertEquals(TokenType::QUESTIONMARK, $tokens[3]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[4]->type); + + $this->assertEquals(TokenType::STRING, $tokens[5]->type); + $this->assertEquals('b', $tokens[5]->value); + + $this->assertEquals(TokenType::SPACE, $tokens[6]->type); + + $this->assertEquals(TokenType::COLON, $tokens[7]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[8]->type); + + $this->assertEquals(TokenType::STRING, $tokens[9]->type); + $this->assertEquals('c', $tokens[9]->value); + + $this->assertEquals(TokenType::BRACKET_ROUND_CLOSE, $tokens[10]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[11]->type); + + $this->assertEquals(TokenType::QUESTIONMARK, $tokens[12]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[13]->type); + + $this->assertEquals(TokenType::BRACKET_ROUND_OPEN, $tokens[14]->type); + + $this->assertEquals(TokenType::STRING, $tokens[15]->type); + $this->assertEquals('d', $tokens[15]->value); + + $this->assertEquals(TokenType::SPACE, $tokens[16]->type); + + $this->assertEquals(TokenType::QUESTIONMARK, $tokens[17]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[18]->type); + + $this->assertEquals(TokenType::STRING, $tokens[19]->type); + $this->assertEquals('e', $tokens[19]->value); + + $this->assertEquals(TokenType::SPACE, $tokens[20]->type); + + $this->assertEquals(TokenType::COLON, $tokens[21]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[22]->type); + + $this->assertEquals(TokenType::STRING, $tokens[23]->type); + $this->assertEquals('f', $tokens[23]->value); + + $this->assertEquals(TokenType::BRACKET_ROUND_CLOSE, $tokens[24]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[25]->type); + + $this->assertEquals(TokenType::COLON, $tokens[26]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[27]->type); + + $this->assertEquals(TokenType::BRACKET_ROUND_OPEN, $tokens[28]->type); + + $this->assertEquals(TokenType::STRING, $tokens[29]->type); + $this->assertEquals('g', $tokens[29]->value); + + $this->assertEquals(TokenType::SPACE, $tokens[30]->type); + + $this->assertEquals(TokenType::QUESTIONMARK, $tokens[31]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[32]->type); + + $this->assertEquals(TokenType::STRING, $tokens[33]->type); + $this->assertEquals('h', $tokens[33]->value); + + $this->assertEquals(TokenType::SPACE, $tokens[34]->type); + + $this->assertEquals(TokenType::COLON, $tokens[35]->type); + + $this->assertEquals(TokenType::SPACE, $tokens[36]->type); + + $this->assertEquals(TokenType::STRING, $tokens[37]->type); + $this->assertEquals('i', $tokens[37]->value); + + $this->assertEquals(TokenType::BRACKET_ROUND_CLOSE, $tokens[38]->type); + } +} diff --git a/test/Unit/Target/Php/Transpiler/Access/AccessTranspilerTest.php b/test/Unit/Target/Php/Transpiler/Access/AccessTranspilerTest.php index 582ce398..b77baecd 100644 --- a/test/Unit/Target/Php/Transpiler/Access/AccessTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/Access/AccessTranspilerTest.php @@ -22,15 +22,19 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\Access; +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Access\AccessTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Properties; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Property; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; +use PackageFactory\ComponentEngine\TypeSystem\TypeReference; use PHPUnit\Framework\TestCase; final class AccessTranspilerTest extends TestCase @@ -60,31 +64,70 @@ public static function accessExamples(): array public function transpilesAccessNodes(string $accessAsString, string $expectedTranspilationResult): void { $accessTranspiler = new AccessTranspiler( - scope: new DummyScope([ - 'a' => StructType::fromStructDeclarationNode( - StructDeclarationNode::fromString( - 'struct A { b: B }' - ) - ), - 'SomeEnum' => EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A B C }' - ) - ), - 'someStruct' => StructType::fromStructDeclarationNode( - StructDeclarationNode::fromString(<<<'AFX' - struct SomeStruct { - foo: string - deep: ?SomeStruct - } - AFX - ) - ) - ]) + scope: new DummyScope( + [ + $structTypeA = new StructType( + name: StructName::from('A'), + properties: new Properties( + new Property( + name: PropertyName::from('b'), + type: new TypeReference( + names: new TypeNames(TypeName::from('B')), + isOptional: false, + isArray: false + ) + ) + ) + ), + $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration( + 'enum SomeEnum { A B C }' + ) + ), + $structTypeSomeStruct = new StructType( + name: StructName::from('SomeStruct'), + properties: new Properties( + new Property( + name: PropertyName::from('b'), + type: new TypeReference( + names: new TypeNames(TypeName::from('foo')), + isOptional: false, + isArray: false + ) + ), + new Property( + name: PropertyName::from('deep'), + type: new TypeReference( + names: new TypeNames(TypeName::from('SomeStruct')), + isOptional: true, + isArray: false + ) + ) + ) + ), + new StructType( + name: StructName::from('B'), + properties: new Properties( + new Property( + name: PropertyName::from('c'), + type: new TypeReference( + names: new TypeNames(TypeName::from('string')), + isOptional: false, + isArray: false + ) + ) + ) + ), + ], + [ + 'a' => $structTypeA, + 'SomeEnum' => $enumStaticType, + 'someStruct' => $structTypeSomeStruct + ] + ) ); - $accessNode = ExpressionNode::fromString($accessAsString)->root; - assert($accessNode instanceof AccessNode); + $accessNode = ASTNodeFixtures::Access($accessAsString); $actualTranspilationResult = $accessTranspiler->transpile( $accessNode diff --git a/test/Unit/Target/Php/Transpiler/Attribute/AttributeTranspilerTest.php b/test/Unit/Target/Php/Transpiler/Attribute/AttributeTranspilerTest.php index ea760619..8f0ebcba 100644 --- a/test/Unit/Target/Php/Transpiler/Attribute/AttributeTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/Attribute/AttributeTranspilerTest.php @@ -22,9 +22,9 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\Attribute; -use PackageFactory\ComponentEngine\Parser\Ast\AttributeNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Attribute\AttributeTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class AttributeTranspilerTest extends TestCase @@ -53,7 +53,8 @@ public function transpilesAttributeNodes(string $attributeAsString, string $expe $attributeTranspiler = new AttributeTranspiler( scope: new DummyScope() ); - $attributeNode = AttributeNode::fromString($attributeAsString); + $attributeNode = ASTNodeFixtures::Attribute($attributeAsString); + assert($attributeNode !== null); $actualTranspilationResult = $attributeTranspiler->transpile( $attributeNode diff --git a/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php index 55c98e87..9a599d19 100644 --- a/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/BinaryOperation/BinaryOperationTranspilerTest.php @@ -26,6 +26,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\Target\Php\Transpiler\BinaryOperation\BinaryOperationTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class BinaryOperationTranspilerTest extends TestCase @@ -91,8 +92,7 @@ public function transpilesBinaryOperationNodes(string $binaryOperationAsString, $binaryOperationTranspiler = new BinaryOperationTranspiler( scope: new DummyScope() ); - $binaryOperationNode = ExpressionNode::fromString($binaryOperationAsString)->root; - assert($binaryOperationNode instanceof BinaryOperationNode); + $binaryOperationNode = ASTNodeFixtures::BinaryOperation($binaryOperationAsString); $actualTranspilationResult = $binaryOperationTranspiler->transpile( $binaryOperationNode diff --git a/test/Unit/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspilerTest.php b/test/Unit/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspilerTest.php index 555b9a0c..3c424b91 100644 --- a/test/Unit/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/BooleanLiteral/BooleanLiteralTranspilerTest.php @@ -22,9 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\BooleanLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\BooleanLiteral\BooleanLiteralTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class BooleanLiteralTranspilerTest extends TestCase @@ -50,8 +49,7 @@ public static function booleanLiteralExamples(): array public function transpilesBooleanLiteralNodes(string $booleanLiteralAsString, string $expectedTranspilationResult): void { $booleanLiteralTranspiler = new BooleanLiteralTranspiler(); - $booleanLiteralNode = ExpressionNode::fromString($booleanLiteralAsString)->root; - assert($booleanLiteralNode instanceof BooleanLiteralNode); + $booleanLiteralNode = ASTNodeFixtures::BooleanLiteral($booleanLiteralAsString); $actualTranspilationResult = $booleanLiteralTranspiler->transpile( $booleanLiteralNode diff --git a/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTestStrategy.php b/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTestStrategy.php index 4a526c8d..c908a152 100644 --- a/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTestStrategy.php +++ b/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTestStrategy.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\ComponentDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\TargetSpecific\ClassName; use PackageFactory\ComponentEngine\Target\Php\Transpiler\ComponentDeclaration\ComponentDeclarationStrategyInterface; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceStrategyInterface; @@ -32,7 +32,7 @@ final class ComponentDeclarationTestStrategy implements ComponentDeclarationStra { public function getClassNameFor(ComponentDeclarationNode $componentDeclarationNode): ClassName { - return ClassName::fromString('Vendor\\Project\\Component\\' . $componentDeclarationNode->componentName); + return ClassName::fromString('Vendor\\Project\\Component\\' . $componentDeclarationNode->name->value->value); } public function getBaseClassNameFor(ComponentDeclarationNode $componentDeclarationNode): ?ClassName diff --git a/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspilerTest.php index af69182e..87616253 100644 --- a/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/ComponentDeclaration/ComponentDeclarationTranspilerTest.php @@ -22,9 +22,9 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\ComponentDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; +use PackageFactory\ComponentEngine\Language\AST\Node\ComponentDeclaration\ComponentDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\ComponentDeclaration\ComponentDeclarationTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Scope\GlobalScope\GlobalScope; use PHPUnit\Framework\TestCase; @@ -43,13 +43,13 @@ public function transpilesComponentDeclarationNodes(): void return

Hello, {name}

} EOT; - $moduleNode = ModuleNode::fromString($componentModuleAsString); + $moduleNode = ASTNodeFixtures::Module($componentModuleAsString); $componentDeclarationTranspiler = new ComponentDeclarationTranspiler( - scope: GlobalScope::get(), + scope: GlobalScope::singleton(), module: $moduleNode, strategy: new ComponentDeclarationTestStrategy() ); - $componentDeclarationNode = $moduleNode->exports->get('Greeter')?->declaration; + $componentDeclarationNode = $moduleNode->export->declaration; assert($componentDeclarationNode instanceof ComponentDeclarationNode); $expectedTranspilationResult = <<<'PHP' diff --git a/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTestStrategy.php b/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTestStrategy.php index f6cc259e..01edbd6d 100644 --- a/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTestStrategy.php +++ b/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTestStrategy.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\EnumDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\EnumDeclaration\EnumDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\TargetSpecific\ClassName; use PackageFactory\ComponentEngine\Target\Php\Transpiler\EnumDeclaration\EnumDeclarationStrategyInterface; @@ -30,6 +30,6 @@ final class EnumDeclarationTestStrategy implements EnumDeclarationStrategyInterf { public function getClassNameFor(EnumDeclarationNode $enumDeclarationNode): ClassName { - return ClassName::fromString('Vendor\\Project\\Component\\' . $enumDeclarationNode->enumName); + return ClassName::fromString('Vendor\\Project\\Component\\' . $enumDeclarationNode->name->value->value); } -} \ No newline at end of file +} diff --git a/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspilerTest.php index 8db6f7d1..f93cfac4 100644 --- a/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/EnumDeclaration/EnumDeclarationTranspilerTest.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\EnumDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\EnumDeclaration\EnumDeclarationTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class EnumDeclarationTranspilerTest extends TestCase @@ -43,7 +43,7 @@ enum ButtonType { $enumDeclarationTranspiler = new EnumDeclarationTranspiler( strategy: new EnumDeclarationTestStrategy() ); - $enumDeclarationNode = EnumDeclarationNode::fromString($enumDeclarationAsString); + $enumDeclarationNode = ASTNodeFixtures::EnumDeclaration($enumDeclarationAsString); $expectedTranspilationResult = <<. - */ - -declare(strict_types=1); - -namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\Identifier; - -use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; -use PackageFactory\ComponentEngine\Target\Php\Transpiler\Expression\ExpressionTranspiler; -use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; -use PackageFactory\ComponentEngine\Target\Php\Transpiler\Identifier\IdentifierTranspiler; -use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; -use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; -use PHPUnit\Framework\TestCase; - -final class IdentifierTranspilerTest extends TestCase -{ - /** - * @test - * @return void - */ - public function transpilesIdentifierNodes(): void - { - $identifierTranspiler = new IdentifierTranspiler( - scope: new DummyScope() - ); - $identifierNode = IdentifierNode::fromString('foo'); - - $expectedTranspilationResult = '$this->foo'; - $actualTranspilationResult = $identifierTranspiler->transpile( - $identifierNode - ); - - $this->assertEquals( - $expectedTranspilationResult, - $actualTranspilationResult - ); - } - - /** - * @test - * @return void - */ - public function transpilesIdentifierNodesReferringToEnums(): void - { - $identifierTranspiler = new IdentifierTranspiler( - scope: new DummyScope([ - 'SomeEnum' => EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A B C }' - ) - ) - ]) - ); - $identifierNode = IdentifierNode::fromString('SomeEnum'); - - $expectedTranspilationResult = 'SomeEnum'; - $actualTranspilationResult = $identifierTranspiler->transpile( - $identifierNode - ); - - $this->assertEquals( - $expectedTranspilationResult, - $actualTranspilationResult - ); - } - - public static function identifierInParenthesisExamples(): mixed - { - // @todo find a better place for these tests, as we actually test the ExpressionNode - return [ - '(foo)' => ['(foo)', '$this->foo'], - '((foo))' => ['((foo))', '$this->foo'], - '(((foo)))' => ['(((foo)))', '$this->foo'] - ]; - } - - /** - * @dataProvider identifierInParenthesisExamples - * @test - */ - public function identifierInParenthesis(string $expression, string $expectedTranspilationResult): void - { - $expressionTranspiler = new ExpressionTranspiler( - scope: new DummyScope([ - "foo" => StringType::get() - ]) - ); - - $actualTranspilationResult = $expressionTranspiler->transpile( - ExpressionNode::fromString($expression) - ); - - $this->assertEquals( - $expectedTranspilationResult, - $actualTranspilationResult - ); - } -} diff --git a/test/Unit/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspilerTest.php b/test/Unit/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspilerTest.php index 58eeff3b..b44c24ea 100644 --- a/test/Unit/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/IntegerLiteral/IntegerLiteralTranspilerTest.php @@ -22,9 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\IntegerLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\IntegerLiteralNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\IntegerLiteral\IntegerLiteralTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class IntegerLiteralTranspilerTest extends TestCase @@ -66,8 +65,7 @@ public static function integerLiteralExamples(): array public function transpilesIntegerLiteralNodes(string $integerLiteralAsString, string $expectedTranspilationResult): void { $integerLiteralTranspiler = new IntegerLiteralTranspiler(); - $integerLiteralNode = ExpressionNode::fromString($integerLiteralAsString)->root; - assert($integerLiteralNode instanceof IntegerLiteralNode); + $integerLiteralNode = ASTNodeFixtures::IntegerLiteral($integerLiteralAsString); $actualTranspilationResult = $integerLiteralTranspiler->transpile( $integerLiteralNode diff --git a/test/Unit/Target/Php/Transpiler/Match/MatchTranspilerTest.php b/test/Unit/Target/Php/Transpiler/Match/MatchTranspilerTest.php index 418c9865..b6f90cb1 100644 --- a/test/Unit/Target/Php/Transpiler/Match/MatchTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/Match/MatchTranspilerTest.php @@ -23,11 +23,9 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\Match; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Match\MatchTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; use PHPUnit\Framework\TestCase; @@ -72,17 +70,19 @@ public static function matchExamples(): array public function transpilesMatchNodes(string $matchAsString, string $expectedTranspilationResult): void { $matchTranspiler = new MatchTranspiler( - scope: new DummyScope([ - 'SomeEnum' => EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A B C }' + scope: new DummyScope( + [ + $someEnumType = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration( + 'enum SomeEnum { A B C }' + ) ) - ) - ]) + ], + ['SomeEnum' => $someEnumType] + ) ); - $matchNode = ExpressionNode::fromString($matchAsString)->root; - assert($matchNode instanceof MatchNode); + $matchNode = ASTNodeFixtures::Match($matchAsString); $actualTranspilationResult = $matchTranspiler->transpile( $matchNode diff --git a/test/Unit/Target/Php/Transpiler/Module/ModuleTestStrategy.php b/test/Unit/Target/Php/Transpiler/Module/ModuleTestStrategy.php index 4853745e..31823b98 100644 --- a/test/Unit/Target/Php/Transpiler/Module/ModuleTestStrategy.php +++ b/test/Unit/Target/Php/Transpiler/Module/ModuleTestStrategy.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\Module; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; +use PackageFactory\ComponentEngine\Language\AST\Node\Module\ModuleNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\ComponentDeclaration\ComponentDeclarationStrategyInterface; use PackageFactory\ComponentEngine\Target\Php\Transpiler\EnumDeclaration\EnumDeclarationStrategyInterface; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Module\ModuleStrategyInterface; diff --git a/test/Unit/Target/Php/Transpiler/Module/ModuleTranspilerTest.php b/test/Unit/Target/Php/Transpiler/Module/ModuleTranspilerTest.php index df92e4c2..ad3ad3fb 100644 --- a/test/Unit/Target/Php/Transpiler/Module/ModuleTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/Module/ModuleTranspilerTest.php @@ -25,6 +25,7 @@ use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; use PackageFactory\ComponentEngine\Test\Unit\Module\Loader\Fixtures\DummyLoader; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Module\ModuleTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Scope\GlobalScope\GlobalScope; use PHPUnit\Framework\TestCase; @@ -43,10 +44,10 @@ public function transpilesModuleNodesThatContainComponentDeclarations(): void EOT; $moduleTranspiler = new ModuleTranspiler( loader: new DummyLoader(), - globalScope: GlobalScope::get(), + globalScope: GlobalScope::singleton(), strategy: new ModuleTestStrategy() ); - $moduleNode = ModuleNode::fromString($moduleNodeAsString); + $moduleNode = ASTNodeFixtures::Module($moduleNodeAsString); $expectedTranspilationResult = <<root; - assert($nullLiteralNode instanceof NullLiteralNode); + $nullLiteralNode = ASTNodeFixtures::NullLiteral('null'); $expectedTranspilationResult = 'null'; $actualTranspilationResult = $nullLiteralTranspiler->transpile( diff --git a/test/Unit/Target/Php/Transpiler/StringLiteral/StringLiteralTranspilerTest.php b/test/Unit/Target/Php/Transpiler/StringLiteral/StringLiteralTranspilerTest.php index 254ee416..3e507cff 100644 --- a/test/Unit/Target/Php/Transpiler/StringLiteral/StringLiteralTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/StringLiteral/StringLiteralTranspilerTest.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\StringLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\StringLiteral\StringLiteralTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class StringLiteralTranspilerTest extends TestCase @@ -67,7 +67,7 @@ public static function stringExamples(): array public function transpilesStringLiteralNodes(string $stringLiteralAsString, string $expectedTranspilationResult): void { $stringLiteralTranspiler = new StringLiteralTranspiler(); - $stringLiteralNode = StringLiteralNode::fromString($stringLiteralAsString); + $stringLiteralNode = ASTNodeFixtures::StringLiteral($stringLiteralAsString); $actualTranspilationResult = $stringLiteralTranspiler->transpile( $stringLiteralNode @@ -120,7 +120,7 @@ public function addsQuotesIfNecessary(string $stringLiteralAsString, string $exp $stringLiteralTranspiler = new StringLiteralTranspiler( shouldAddQuotes: true ); - $stringLiteralNode = StringLiteralNode::fromString($stringLiteralAsString); + $stringLiteralNode = ASTNodeFixtures::StringLiteral($stringLiteralAsString); $actualTranspilationResult = $stringLiteralTranspiler->transpile( $stringLiteralNode diff --git a/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTestStrategy.php b/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTestStrategy.php index 4ad632f8..5dc165f4 100644 --- a/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTestStrategy.php +++ b/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTestStrategy.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\StructDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Language\AST\Node\StructDeclaration\StructDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\TargetSpecific\ClassName; use PackageFactory\ComponentEngine\Target\Php\Transpiler\StructDeclaration\StructDeclarationStrategyInterface; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceStrategyInterface; @@ -32,7 +32,7 @@ final class StructDeclarationTestStrategy implements StructDeclarationStrategyIn { public function getClassNameFor(StructDeclarationNode $structDeclarationNode): ClassName { - return ClassName::fromString('Vendor\\Project\\Component\\' . $structDeclarationNode->structName); + return ClassName::fromString('Vendor\\Project\\Component\\' . $structDeclarationNode->name->value->value); } public function getBaseClassNameFor(StructDeclarationNode $structDeclarationNode): ?ClassName diff --git a/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspilerTest.php index 7038e291..2510f492 100644 --- a/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/StructDeclaration/StructDeclarationTranspilerTest.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\StructDeclaration; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\StructDeclaration\StructDeclarationTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; @@ -43,10 +43,10 @@ public function transpilesStructDeclarationNodes(): void } EOT; $structDeclarationTranspiler = new StructDeclarationTranspiler( - scope: new DummyScope([], ['string' => StringType::get()]), + scope: new DummyScope([StringType::singleton()]), strategy: new StructDeclarationTestStrategy() ); - $structDeclarationNode = StructDeclarationNode::fromString($structDeclarationAsString); + $structDeclarationNode = ASTNodeFixtures::StructDeclaration($structDeclarationAsString); $expectedTranspilationResult = <<<'PHP' StringType::get() - ]) + scope: new DummyScope([StringType::singleton()], ['someValue' => StringType::singleton()]) ); - $tagNode = ExpressionNode::fromString($tagAsString)->root; - assert($tagNode instanceof TagNode); + $tagNode = ASTNodeFixtures::Tag($tagAsString); $actualTranspilationResult = $tagTranspiler->transpile($tagNode); diff --git a/test/Unit/Target/Php/Transpiler/TagContent/TagContentTranspilerTest.php b/test/Unit/Target/Php/Transpiler/TagContent/TagContentTranspilerTest.php index 3569db33..1f12df51 100644 --- a/test/Unit/Target/Php/Transpiler/TagContent/TagContentTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/TagContent/TagContentTranspilerTest.php @@ -22,32 +22,15 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\TagContent; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\TagContentNode; -use PackageFactory\ComponentEngine\Parser\Ast\TagNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TagContent\TagContentTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; final class TagContentTranspilerTest extends TestCase { - private static function tagContentNodeFromString(string $tagContentAsString): TagContentNode - { - $expressionNode = ExpressionNode::fromString( - sprintf('
%s
', $tagContentAsString) - ); - $tagNode = $expressionNode->root; - assert($tagNode instanceof TagNode); - - $tagContentNode = $tagNode->children->items[0]; - assert($tagContentNode instanceof TagContentNode); - - return $tagContentNode; - } - /** * @return array */ @@ -79,11 +62,10 @@ public static function tagContentExamples(): array public function transpilesTagContentNodes(string $tagContentAsString, string $expectedTranspilationResult): void { $tagContentTranspiler = new TagContentTranspiler( - scope: new DummyScope([ - 'someValue' => StringType::get() - ]) + scope: new DummyScope([StringType::singleton()], ['someValue' => StringType::singleton()]) ); - $tagContentNode = self::tagContentNodeFromString($tagContentAsString); + $tagContentNode = ASTNodeFixtures::TagContent($tagContentAsString); + assert($tagContentNode !== null); $actualTranspilationResult = $tagContentTranspiler->transpile( $tagContentNode @@ -102,15 +84,19 @@ public function transpilesTagContentNodes(string $tagContentAsString, string $ex public function addsCallToRenderFunctionIfInterpolatedValueIsOfTypeComponent(): void { $tagContentTranspiler = new TagContentTranspiler( - scope: new DummyScope([ - 'button' => ComponentType::fromComponentDeclarationNode( - ComponentDeclarationNode::fromString( - 'component Button { return }' + scope: new DummyScope( + [ + $buttonType = ComponentType::fromComponentDeclarationNode( + ASTNodeFixtures::ComponentDeclaration( + 'component Button { return }' + ) ) - ) - ]) + ], + ['button' => $buttonType] + ) ); - $tagContentNode = self::tagContentNodeFromString('{button}'); + $tagContentNode = ASTNodeFixtures::TagContent('{button}'); + assert($tagContentNode !== null); $expectedTranspilationResult = '\' . $this->button->render() . \''; $actualTranspilationResult = $tagContentTranspiler->transpile( diff --git a/test/Unit/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspilerTest.php b/test/Unit/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspilerTest.php index f5bb4a22..fbe8bda4 100644 --- a/test/Unit/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/TemplateLiteral/TemplateLiteralTranspilerTest.php @@ -22,9 +22,9 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\TemplateLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TemplateLiteral\TemplateLiteralTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class TemplateLiteralTranspilerTest extends TestCase @@ -70,7 +70,7 @@ public function transpilesTemplateLiteralNodes(string $templateLiteralAsString, $templateLiteralTranspiler = new TemplateLiteralTranspiler( scope: new DummyScope() ); - $templateLiteralNode = TemplateLiteralNode::fromString($templateLiteralAsString); + $templateLiteralNode = ASTNodeFixtures::TemplateLiteral($templateLiteralAsString); $actualTranspilationResult = $templateLiteralTranspiler->transpile( $templateLiteralNode diff --git a/test/Unit/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspilerTest.php index 05e2ee0f..ec89074d 100644 --- a/test/Unit/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/TernaryOperation/TernaryOperationTranspilerTest.php @@ -22,13 +22,18 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\TernaryOperation; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TernaryOperation\TernaryOperationTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Properties; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Property; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; +use PackageFactory\ComponentEngine\TypeSystem\TypeReference; use PHPUnit\Framework\TestCase; final class TernaryOperationTranspilerTest extends TestCase @@ -76,20 +81,38 @@ public static function ternaryOperationWithVariablesInConditionExamples(): array public function transpilesTernaryOperationNodes(string $ternaryOperationAsString, string $expectedTranspilationResult): void { $ternaryOperationTranspiler = new TernaryOperationTranspiler( - scope: new DummyScope([ - "someString" => StringType::get(), - "someStruct" => StructType::fromStructDeclarationNode( - StructDeclarationNode::fromString(<<<'AFX' - struct SomeStruct { - foo: string - deep: ?SomeStruct - } - AFX) - ) - ]) + scope: new DummyScope( + [ + StringType::singleton(), + $someStructType = new StructType( + name: StructName::from('SomeStruct'), + properties: new Properties( + new Property( + name: PropertyName::from('foo'), + type: new TypeReference( + names: new TypeNames(TypeName::from('string')), + isOptional: false, + isArray: false + ) + ), + new Property( + name: PropertyName::from('deep'), + type: new TypeReference( + names: new TypeNames(TypeName::from('SomeStruct')), + isOptional: true, + isArray: false + ) + ) + ) + ) + ], + [ + 'someString' => StringType::singleton(), + 'someStruct' => $someStructType + ] + ) ); - $ternaryOperationNode = ExpressionNode::fromString($ternaryOperationAsString)->root; - assert($ternaryOperationNode instanceof TernaryOperationNode); + $ternaryOperationNode = ASTNodeFixtures::TernaryOperation($ternaryOperationAsString); $actualTranspilationResult = $ternaryOperationTranspiler->transpile( $ternaryOperationNode diff --git a/test/Unit/Target/Php/Transpiler/Text/TextTranspilerTest.php b/test/Unit/Target/Php/Transpiler/Text/TextTranspilerTest.php index 92c9ed54..3e41b494 100644 --- a/test/Unit/Target/Php/Transpiler/Text/TextTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/Text/TextTranspilerTest.php @@ -22,8 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\Text; -use PackageFactory\ComponentEngine\Parser\Ast\TextNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\Text\TextTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PHPUnit\Framework\TestCase; final class TextTranspilerTest extends TestCase @@ -35,17 +35,15 @@ final class TextTranspilerTest extends TestCase public function transpilesTextNodes(): void { $textTranspiler = new TextTranspiler(); - $textNode = TextNode::fromString('Hello World!'); + $textNode = ASTNodeFixtures::Text('Hello World!'); assert($textNode !== null); $expectedTranspilationResult = 'Hello World!'; - $actualTranspilationResult = $textTranspiler->transpile( - $textNode - ); + $actualTranspilationResult = $textTranspiler->transpile($textNode); $this->assertEquals( $expectedTranspilationResult, $actualTranspilationResult ); } -} \ No newline at end of file +} diff --git a/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTestStrategy.php b/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTestStrategy.php index 46fa8b62..e387565f 100644 --- a/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTestStrategy.php +++ b/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTestStrategy.php @@ -22,13 +22,13 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\TypeReference; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Language\AST\Node\TypeReference\TypeReferenceNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceStrategyInterface; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; -use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class TypeReferenceTestStrategy implements TypeReferenceStrategyInterface { @@ -39,21 +39,21 @@ public function getPhpTypeReferenceForSlotType(SlotType $slotType, TypeReference public function getPhpTypeReferenceForComponentType(ComponentType $componentType, TypeReferenceNode $typeReferenceNode): string { - return $componentType->componentName . 'Component'; + return $componentType->getName()->value . 'Component'; } public function getPhpTypeReferenceForEnumType(EnumStaticType $enumType, TypeReferenceNode $typeReferenceNode): string { - return $enumType->enumName . 'Enum'; + return $enumType->getName()->value . 'Enum'; } public function getPhpTypeReferenceForStructType(StructType $structType, TypeReferenceNode $typeReferenceNode): string { - return $structType->structName . 'Struct'; + return $structType->getName()->value . 'Struct'; } - public function getPhpTypeReferenceForCustomType(TypeInterface $customType, TypeReferenceNode $typeReferenceNode): string + public function getPhpTypeReferenceForCustomType(AtomicTypeInterface $customType, TypeReferenceNode $typeReferenceNode): string { - return $typeReferenceNode->name . 'Custom'; + return $customType->getName()->value . 'Custom'; } } diff --git a/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTranspilerTest.php b/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTranspilerTest.php index 6332b4bd..92979f06 100644 --- a/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/TypeReference/TypeReferenceTranspilerTest.php @@ -22,42 +22,64 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\TypeReference; +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\TypeReference\TypeReferenceTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Properties; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; use PHPUnit\Framework\TestCase; final class TypeReferenceTranspilerTest extends TestCase { + private function mockAtomicType(string $name): AtomicTypeInterface + { + return new class($name) implements AtomicTypeInterface + { + public function __construct(private readonly string $name) + { + } + + public function getName(): TypeName + { + return TypeName::from($this->name); + } + + public function is(TypeInterface $other): bool + { + return $other === $this; + } + }; + } + protected function getTypeReferenceTranspiler(): TypeReferenceTranspiler { return new TypeReferenceTranspiler( - scope: new DummyScope([], [ - 'string' => StringType::get(), - 'boolean' => BooleanType::get(), - 'number' => IntegerType::get(), - 'Button' => ComponentType::fromComponentDeclarationNode( - ComponentDeclarationNode::fromString('component Button { return "" }') + scope: new DummyScope([ + StringType::singleton(), + BooleanType::singleton(), + IntegerType::singleton(), + ComponentType::fromComponentDeclarationNode( + ASTNodeFixtures::ComponentDeclaration('component Button { return "" }') ), - 'DayOfWeek' => EnumStaticType::fromModuleIdAndDeclaration( + EnumStaticType::fromModuleIdAndDeclaration( ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString('enum DayOfWeek {}') + ASTNodeFixtures::EnumDeclaration('enum DayOfWeek {}') ), - 'Link' => StructType::fromStructDeclarationNode( - StructDeclarationNode::fromString('struct Link {}') + new StructType( + name: StructName::from('Link'), + properties: new Properties() ), - 'SomeType' => $this->createMock(TypeInterface::class) + $this->mockAtomicType('SomeType') ]), strategy: new TypeReferenceTestStrategy() ); @@ -89,7 +111,7 @@ public static function primitiveTypeReferenceExamples(): array public function transpilesReferencesToPrimitiveTypes(string $typeReferenceAsString, string $expectedTranspilationResult): void { $typeReferenceTranspiler = $this->getTypeReferenceTranspiler(); - $typeReferenceNode = TypeReferenceNode::fromString($typeReferenceAsString); + $typeReferenceNode = ASTNodeFixtures::TypeReference($typeReferenceAsString); $actualTranspilationResult = $typeReferenceTranspiler->transpile( $typeReferenceNode @@ -127,7 +149,7 @@ public static function optionalTypeReferenceExamples(): array public function transpilesReferencesToOptionalTypes(string $typeReferenceAsString, string $expectedTranspilationResult): void { $typeReferenceTranspiler = $this->getTypeReferenceTranspiler(); - $typeReferenceNode = TypeReferenceNode::fromString($typeReferenceAsString); + $typeReferenceNode = ASTNodeFixtures::TypeReference($typeReferenceAsString); $actualTranspilationResult = $typeReferenceTranspiler->transpile( $typeReferenceNode diff --git a/test/Unit/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspilerTest.php b/test/Unit/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspilerTest.php index 9e342bc2..2bbb7b32 100644 --- a/test/Unit/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspilerTest.php +++ b/test/Unit/Target/Php/Transpiler/UnaryOperation/UnaryOperationTranspilerTest.php @@ -22,9 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\UnaryOperation; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\UnaryOperationNode; use PackageFactory\ComponentEngine\Target\Php\Transpiler\UnaryOperation\UnaryOperationTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; @@ -51,14 +50,11 @@ public static function unaryOperationExamples(): array public function transpilesUnaryOperationNodes(string $unaryOperationAsString, string $expectedTranspilationResult): void { $transpiler = new UnaryOperationTranspiler( - scope: new DummyScope(['foo' => StringType::get()]) + scope: new DummyScope([StringType::singleton()], ['foo' => StringType::singleton()]) ); - $node = ExpressionNode::fromString($unaryOperationAsString)->root; - assert($node instanceof UnaryOperationNode); + $node = ASTNodeFixtures::UnaryOperation($unaryOperationAsString); - $actualTranspilationResult = $transpiler->transpile( - $node - ); + $actualTranspilationResult = $transpiler->transpile($node); $this->assertEquals( $expectedTranspilationResult, diff --git a/test/Unit/Target/Php/Transpiler/ValueReference/ValueReferenceTranspilerTest.php b/test/Unit/Target/Php/Transpiler/ValueReference/ValueReferenceTranspilerTest.php new file mode 100644 index 00000000..f6245c54 --- /dev/null +++ b/test/Unit/Target/Php/Transpiler/ValueReference/ValueReferenceTranspilerTest.php @@ -0,0 +1,87 @@ +. + */ + +declare(strict_types=1); + +namespace PackageFactory\ComponentEngine\Test\Unit\Target\Php\Transpiler\ValueReference; + +use PackageFactory\ComponentEngine\Module\ModuleId; +use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; +use PackageFactory\ComponentEngine\Target\Php\Transpiler\ValueReference\ValueReferenceTranspiler; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; +use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; +use PHPUnit\Framework\TestCase; + +final class ValueReferenceTranspilerTest extends TestCase +{ + /** + * @test + * @return void + */ + public function transpilesValueReferenceNodes(): void + { + $identifierTranspiler = new ValueReferenceTranspiler( + scope: new DummyScope() + ); + $identifierNode = ASTNodeFixtures::ValueReference('foo'); + + $expectedTranspilationResult = '$this->foo'; + $actualTranspilationResult = $identifierTranspiler->transpile( + $identifierNode + ); + + $this->assertEquals( + $expectedTranspilationResult, + $actualTranspilationResult + ); + } + + /** + * @test + * @return void + */ + public function transpilesValueReferenceNodesReferringToEnums(): void + { + $identifierTranspiler = new ValueReferenceTranspiler( + scope: new DummyScope( + [ + $someEnumType = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration( + 'enum SomeEnum { A B C }' + ) + ) + ], + ['SomeEnum' => $someEnumType] + ) + ); + $identifierNode = ASTNodeFixtures::ValueReference('SomeEnum'); + + $expectedTranspilationResult = 'SomeEnum'; + $actualTranspilationResult = $identifierTranspiler->transpile( + $identifierNode + ); + + $this->assertEquals( + $expectedTranspilationResult, + $actualTranspilationResult + ); + } +} diff --git a/test/Unit/TypeSystem/Resolver/Access/AccessTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/Access/AccessTypeResolverTest.php index 78b548fe..19483ab2 100644 --- a/test/Unit/TypeSystem/Resolver/Access/AccessTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/Access/AccessTypeResolverTest.php @@ -22,10 +22,9 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\Access; +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\AccessNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Access\AccessTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; @@ -60,11 +59,9 @@ public static function invalidAccessExamples(): iterable private function resolveAccessType(string $accessAsString, ScopeInterface $scope): TypeInterface { - $accessTypeResolver = new AccessTypeResolver( - scope: $scope - ); - $accessNode = ExpressionNode::fromString($accessAsString)->root; - assert($accessNode instanceof AccessNode); + $accessTypeResolver = new AccessTypeResolver(scope: $scope); + $accessNode = ASTNodeFixtures::Access($accessAsString); + return $accessTypeResolver->resolveTypeOf($accessNode); } @@ -73,17 +70,18 @@ private function resolveAccessType(string $accessAsString, ScopeInterface $scope */ public function enumMemberAccessOnStaticEnum(): void { - $someEnum = EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A("Hi") }' - ) + $scope = new DummyScope( + [ + $someEnum = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration( + 'enum SomeEnum { A("Hi") }' + ) + ) + ], + ['SomeEnum' => $someEnum] ); - $scope = new DummyScope([ - 'SomeEnum' => $someEnum - ]); - $accessType = $this->resolveAccessType( 'SomeEnum.A', $scope @@ -94,7 +92,7 @@ public function enumMemberAccessOnStaticEnum(): void $this->assertTrue($accessType->enumStaticType->is($someEnum)); - $this->assertEquals("A", $accessType->getMemberName()); + $this->assertEquals(EnumMemberName::from('A'), $accessType->getMemberName()); } /** @@ -105,22 +103,24 @@ public function invalidAccessResultsInError(string $accessAsString, string $expe { $this->expectExceptionMessage($expectedErrorMessage); - $someEnum = EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A }' - ) - ); - $scope = new DummyScope([ - 'someString' => StringType::get(), - 'SomeEnum' => $someEnum, - 'someEnumValue' => $someEnum->toEnumInstanceType() - ]); - $accessTypeResolver = new AccessTypeResolver( - scope: $scope + $scope = new DummyScope( + [ + StringType::singleton(), + $someEnum = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration( + 'enum SomeEnum { A }' + ) + ) + ], + [ + 'someString' => StringType::singleton(), + 'SomeEnum' => $someEnum, + 'someEnumValue' => $someEnum->toEnumInstanceType() + ] ); - $accessNode = ExpressionNode::fromString($accessAsString)->root; - assert($accessNode instanceof AccessNode); + $accessTypeResolver = new AccessTypeResolver(scope: $scope); + $accessNode = ASTNodeFixtures::Access($accessAsString); $accessTypeResolver->resolveTypeOf($accessNode); } diff --git a/test/Unit/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolverTest.php index 54f851ac..d88e62bf 100644 --- a/test/Unit/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/BinaryOperation/BinaryOperationTypeResolverTest.php @@ -22,8 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\BinaryOperation; -use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Resolver\BinaryOperation\BinaryOperationTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; @@ -41,22 +40,22 @@ final class BinaryOperationTypeResolverTest extends TestCase public static function binaryOperationExamples(): array { return [ - 'true && false' => ['true && false', BooleanType::get()], - 'true || false' => ['true || false', BooleanType::get()], - 'true && "foo"' => ['true && "foo"', UnionType::of(BooleanType::get(), StringType::get())], - 'true || "foo"' => ['true || "foo"', UnionType::of(BooleanType::get(), StringType::get())], - 'true && 42' => ['true && 42', UnionType::of(BooleanType::get(), IntegerType::get())], - 'true || 42' => ['true || 42', UnionType::of(BooleanType::get(), IntegerType::get())], + 'true && false' => ['true && false', BooleanType::singleton()], + 'true || false' => ['true || false', BooleanType::singleton()], + 'true && "foo"' => ['true && "foo"', UnionType::of(BooleanType::singleton(), StringType::singleton())], + 'true || "foo"' => ['true || "foo"', UnionType::of(BooleanType::singleton(), StringType::singleton())], + 'true && 42' => ['true && 42', UnionType::of(BooleanType::singleton(), IntegerType::singleton())], + 'true || 42' => ['true || 42', UnionType::of(BooleanType::singleton(), IntegerType::singleton())], - '4 === 2' => ['4 === 2', BooleanType::get()], - '4 !== 2' => ['4 !== 2', BooleanType::get()], - '4 > 2' => ['4 > 2', BooleanType::get()], - '4 >= 2' => ['4 >= 2', BooleanType::get()], - '4 < 2' => ['4 < 2', BooleanType::get()], - '4 <= 2' => ['4 <= 2', BooleanType::get()], + '4 === 2' => ['4 === 2', BooleanType::singleton()], + '4 !== 2' => ['4 !== 2', BooleanType::singleton()], + '4 > 2' => ['4 > 2', BooleanType::singleton()], + '4 >= 2' => ['4 >= 2', BooleanType::singleton()], + '4 < 2' => ['4 < 2', BooleanType::singleton()], + '4 <= 2' => ['4 <= 2', BooleanType::singleton()], - 'true && true && true' => ['true && true && true', BooleanType::get()], - '1 === 1 === true' => ['1 === 1 === true', BooleanType::get()], + 'true && true && true' => ['true && true && true', BooleanType::singleton()], + '1 === 1 === true' => ['1 === 1 === true', BooleanType::singleton()], ]; } @@ -73,8 +72,7 @@ public function resolvesBinaryOperationToResultingType(string $binaryOperationAs $binaryOperationTypeResolver = new BinaryOperationTypeResolver( scope: $scope ); - $binaryOperationNode = ExpressionNode::fromString($binaryOperationAsString)->root; - assert($binaryOperationNode instanceof BinaryOperationNode); + $binaryOperationNode = ASTNodeFixtures::BinaryOperation($binaryOperationAsString); $actualType = $binaryOperationTypeResolver->resolveTypeOf($binaryOperationNode); diff --git a/test/Unit/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolverTest.php index 6726da1b..107d54a4 100644 --- a/test/Unit/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/BooleanLiteral/BooleanLiteralTypeResolverTest.php @@ -22,8 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\BooleanLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Resolver\BooleanLiteral\BooleanLiteralTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PHPUnit\Framework\TestCase; @@ -36,10 +35,9 @@ final class BooleanLiteralTypeResolverTest extends TestCase public function resolvesBooleanLiteralToBooleanType(): void { $booleanLiteralTypeResolver = new BooleanLiteralTypeResolver(); - $booleanLiteralNode = ExpressionNode::fromString('true')->root; - assert($booleanLiteralNode instanceof BooleanLiteralNode); + $booleanLiteralNode = ASTNodeFixtures::BooleanLiteral('true'); - $expectedType = BooleanType::get(); + $expectedType = BooleanType::singleton(); $actualType = $booleanLiteralTypeResolver->resolveTypeOf($booleanLiteralNode); $this->assertTrue( diff --git a/test/Unit/TypeSystem/Resolver/Expression/ExpressionTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/Expression/ExpressionTypeResolverTest.php index ca05a579..e5d7f9dd 100644 --- a/test/Unit/TypeSystem/Resolver/Expression/ExpressionTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/Expression/ExpressionTypeResolverTest.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\Expression; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; @@ -41,19 +41,19 @@ final class ExpressionTypeResolverTest extends TestCase public static function binaryOperationExamples(): array { return [ - 'true && false' => ['true && false', BooleanType::get()], - 'true || false' => ['true || false', BooleanType::get()], - 'true && "foo"' => ['true && "foo"', UnionType::of(BooleanType::get(), StringType::get())], - 'true || "foo"' => ['true || "foo"', UnionType::of(BooleanType::get(), StringType::get())], - 'true && 42' => ['true && 42', UnionType::of(BooleanType::get(), IntegerType::get())], - 'true || 42' => ['true || 42', UnionType::of(BooleanType::get(), IntegerType::get())], - - '4 === 2' => ['4 === 2', BooleanType::get()], - '4 !== 2' => ['4 !== 2', BooleanType::get()], - '4 > 2' => ['4 > 2', BooleanType::get()], - '4 >= 2' => ['4 >= 2', BooleanType::get()], - '4 < 2' => ['4 < 2', BooleanType::get()], - '4 <= 2' => ['4 <= 2', BooleanType::get()], + 'true && false' => ['true && false', BooleanType::singleton()], + 'true || false' => ['true || false', BooleanType::singleton()], + 'true && "foo"' => ['true && "foo"', UnionType::of(BooleanType::singleton(), StringType::singleton())], + 'true || "foo"' => ['true || "foo"', UnionType::of(BooleanType::singleton(), StringType::singleton())], + 'true && 42' => ['true && 42', UnionType::of(BooleanType::singleton(), IntegerType::singleton())], + 'true || 42' => ['true || 42', UnionType::of(BooleanType::singleton(), IntegerType::singleton())], + + '4 === 2' => ['4 === 2', BooleanType::singleton()], + '4 !== 2' => ['4 !== 2', BooleanType::singleton()], + '4 > 2' => ['4 > 2', BooleanType::singleton()], + '4 >= 2' => ['4 >= 2', BooleanType::singleton()], + '4 < 2' => ['4 < 2', BooleanType::singleton()], + '4 <= 2' => ['4 <= 2', BooleanType::singleton()], ]; } @@ -68,7 +68,7 @@ public function resolvesBinaryOperationToResultingType(string $binaryExpressionA { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString($binaryExpressionAsString); + $expressionNode = ASTNodeFixtures::Expression($binaryExpressionAsString); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); @@ -86,9 +86,9 @@ public function resolvesBooleanLiteralToBooleanType(): void { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString('true'); + $expressionNode = ASTNodeFixtures::Expression('true'); - $expectedType = BooleanType::get(); + $expectedType = BooleanType::singleton(); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); $this->assertTrue( @@ -103,11 +103,11 @@ public function resolvesBooleanLiteralToBooleanType(): void */ public function resolvesKnownIdentifierToItsType(): void { - $scope = new DummyScope(['foo' => StringType::get()]); + $scope = new DummyScope([StringType::singleton()], ['foo' => StringType::singleton()]); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString('foo'); + $expressionNode = ASTNodeFixtures::Expression('foo'); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); $this->assertTrue( @@ -124,19 +124,19 @@ public static function matchExamples(): array return [ 'match (true) { true -> 42 false -> "foo" }' => [ 'match (true) { true -> 42 false -> "foo" }', - IntegerType::get() + IntegerType::singleton() ], 'match (false) { true -> 42 false -> "foo" }' => [ 'match (false) { true -> 42 false -> "foo" }', - StringType::get() + StringType::singleton() ], 'match (variableOfTypeBoolean) { true -> 42 false -> "foo" }' => [ 'match (variableOfTypeBoolean) { true -> 42 false -> "foo" }', - UnionType::of(IntegerType::get(), StringType::get()) + UnionType::of(IntegerType::singleton(), StringType::singleton()) ], 'match (variableOfTypeBoolean) { true -> variableOfTypeNumber false -> variableOfTypeString }' => [ 'match (variableOfTypeBoolean) { true -> variableOfTypeNumber false -> variableOfTypeString }', - UnionType::of(IntegerType::get(), StringType::get()) + UnionType::of(IntegerType::singleton(), StringType::singleton()) ], ]; } @@ -150,13 +150,16 @@ public static function matchExamples(): array */ public function resolvesMatchToResultingType(string $matchAsString, TypeInterface $expectedType): void { - $scope = new DummyScope([ - 'variableOfTypeBoolean' => BooleanType::get(), - 'variableOfTypeString' => StringType::get(), - 'variableOfTypeNumber' => IntegerType::get(), - ]); + $scope = new DummyScope( + [BooleanType::singleton(), StringType::singleton(), IntegerType::singleton()], + [ + 'variableOfTypeBoolean' => BooleanType::singleton(), + 'variableOfTypeString' => StringType::singleton(), + 'variableOfTypeNumber' => IntegerType::singleton() + ] + ); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString($matchAsString); + $expressionNode = ASTNodeFixtures::Expression($matchAsString); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); @@ -174,9 +177,9 @@ public function resolvesNullLiteralToNullType(): void { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString('null'); + $expressionNode = ASTNodeFixtures::Expression('null'); - $expectedType = NullType::get(); + $expectedType = NullType::singleton(); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); $this->assertTrue( @@ -193,9 +196,9 @@ public function resolvesNumberLiteralToIntegerType(): void { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString('42'); + $expressionNode = ASTNodeFixtures::Expression('42'); - $expectedType = IntegerType::get(); + $expectedType = IntegerType::singleton(); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); $this->assertTrue( @@ -212,9 +215,9 @@ public function resolvesStringLiteralToStringType(): void { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString('"foo"'); + $expressionNode = ASTNodeFixtures::Expression('"foo"'); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); $this->assertTrue( @@ -231,9 +234,9 @@ public function resolvesTagToStringType(): void { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString('
'); + $expressionNode = ASTNodeFixtures::Expression('
'); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); $this->assertTrue( @@ -265,9 +268,9 @@ public function resolvesTemplateLiteralToStringType(string $templateLiteralAsStr { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString($templateLiteralAsString); + $expressionNode = ASTNodeFixtures::Expression($templateLiteralAsString); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); $this->assertTrue( @@ -282,9 +285,9 @@ public function resolvesTemplateLiteralToStringType(string $templateLiteralAsStr public static function ternaryOperationExamples(): array { return [ - 'true ? 42 : "foo"' => ['true ? 42 : "foo"', IntegerType::get()], - 'false ? 42 : "foo"' => ['false ? 42 : "foo"', StringType::get()], - '1 < 2 ? 42 : "foo"' => ['1 < 2 ? 42 : "foo"', UnionType::of(IntegerType::get(), StringType::get())] + 'true ? 42 : "foo"' => ['true ? 42 : "foo"', IntegerType::singleton()], + 'false ? 42 : "foo"' => ['false ? 42 : "foo"', StringType::singleton()], + '1 < 2 ? 42 : "foo"' => ['1 < 2 ? 42 : "foo"', UnionType::of(IntegerType::singleton(), StringType::singleton())] ]; } @@ -299,7 +302,7 @@ public function resolvesTernaryOperationToResultingType(string $ternaryOperation { $scope = new DummyScope(); $expressionTypeResolver = new ExpressionTypeResolver(scope: $scope); - $expressionNode = ExpressionNode::fromString($ternaryOperationAsString); + $expressionNode = ASTNodeFixtures::Expression($ternaryOperationAsString); $actualType = $expressionTypeResolver->resolveTypeOf($expressionNode); diff --git a/test/Unit/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolverTest.php index cc1499ef..dd763f1b 100644 --- a/test/Unit/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/IntegerLiteral/IntegerLiteralTypeResolverTest.php @@ -22,8 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\NumberLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\IntegerLiteralNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Resolver\IntegerLiteral\IntegerLiteralTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; use PHPUnit\Framework\TestCase; @@ -36,10 +35,9 @@ final class IntegerLiteralTypeResolverTest extends TestCase public function resolvesIntegerLiteralToIntegerType(): void { $integerLiteralTypeResolver = new IntegerLiteralTypeResolver(); - $integerLiteralNode = ExpressionNode::fromString('42')->root; - assert($integerLiteralNode instanceof IntegerLiteralNode); + $integerLiteralNode = ASTNodeFixtures::IntegerLiteral('42'); - $expectedType = IntegerType::get(); + $expectedType = IntegerType::singleton(); $actualType = $integerLiteralTypeResolver->resolveTypeOf($integerLiteralNode); $this->assertTrue( diff --git a/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php index c5a97431..94aa8751 100644 --- a/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/Match/MatchTypeResolverTest.php @@ -23,9 +23,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\Match; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\MatchNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Match\MatchTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; @@ -46,39 +44,39 @@ public static function matchExamples(): array return [ 'match (true) { true -> 42 false -> "foo" }' => [ 'match (true) { true -> 42 false -> "foo" }', - IntegerType::get() + IntegerType::singleton() ], 'match (false) { true -> 42 false -> "foo" }' => [ 'match (false) { true -> 42 false -> "foo" }', - StringType::get() + StringType::singleton() ], 'match (variableOfTypeBoolean) { true -> 42 false -> "foo" }' => [ 'match (variableOfTypeBoolean) { true -> 42 false -> "foo" }', - UnionType::of(IntegerType::get(), StringType::get()) + UnionType::of(IntegerType::singleton(), StringType::singleton()) ], 'match (variableOfTypeBoolean) { true -> variableOfTypeNumber false -> variableOfTypeString }' => [ 'match (variableOfTypeBoolean) { true -> variableOfTypeNumber false -> variableOfTypeString }', - UnionType::of(IntegerType::get(), StringType::get()) + UnionType::of(IntegerType::singleton(), StringType::singleton()) ], 'match enum with all declared members' => [ - <<<'EOF' - match (someEnumValue) { - SomeEnum.A -> variableOfTypeNumber - SomeEnum.B -> variableOfTypeString - SomeEnum.C -> variableOfTypeBoolean - } + << variableOfTypeNumber + SomeEnum.B -> variableOfTypeString + SomeEnum.C -> variableOfTypeBoolean + } EOF, - UnionType::of(IntegerType::get(), StringType::get(), BooleanType::get()) + UnionType::of(IntegerType::singleton(), StringType::singleton(), BooleanType::singleton()) ], 'match enum with some declared members and default' => [ - <<<'EOF' - match (someEnumValue) { - SomeEnum.A -> variableOfTypeNumber - SomeEnum.B -> variableOfTypeString - default -> variableOfTypeBoolean - } + << variableOfTypeNumber + SomeEnum.B -> variableOfTypeString + default -> variableOfTypeBoolean + } EOF, - UnionType::of(IntegerType::get(), StringType::get(), BooleanType::get()) + UnionType::of(IntegerType::singleton(), StringType::singleton(), BooleanType::singleton()) ], ]; } @@ -92,24 +90,28 @@ public static function matchExamples(): array */ public function resolvesMatchToResultingType(string $matchAsString, TypeInterface $expectedType): void { - $someStaticEnumType = EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A B C }' - ) - ); - $scope = new DummyScope([ - 'variableOfTypeBoolean' => BooleanType::get(), - 'variableOfTypeString' => StringType::get(), - 'variableOfTypeNumber' => IntegerType::get(), - 'someEnumValue' => $someStaticEnumType->toEnumInstanceType(), - 'SomeEnum' => $someStaticEnumType - ]); - $matchTypeResolver = new MatchTypeResolver( - scope: $scope + $scope = new DummyScope( + [ + BooleanType::singleton(), + StringType::singleton(), + IntegerType::singleton(), + $someStaticEnumType = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration( + 'enum SomeEnum { A B C }' + ) + ) + ], + [ + 'variableOfTypeBoolean' => BooleanType::singleton(), + 'variableOfTypeString' => StringType::singleton(), + 'variableOfTypeNumber' => IntegerType::singleton(), + 'someEnumValue' => $someStaticEnumType->toEnumInstanceType(), + 'SomeEnum' => $someStaticEnumType + ] ); - $matchNode = ExpressionNode::fromString($matchAsString)->root; - assert($matchNode instanceof MatchNode); + $matchTypeResolver = new MatchTypeResolver(scope: $scope); + $matchNode = ASTNodeFixtures::Match($matchAsString); $actualType = $matchTypeResolver->resolveTypeOf($matchNode); @@ -124,19 +126,8 @@ public function resolvesMatchToResultingType(string $matchAsString, TypeInterfac */ public static function malformedEnumExamples(): iterable { - yield "Multiple default keys" => [ - <<<'EOF' - match (someEnumValue) { - SomeEnum.A -> "a" - default -> "b" - default -> "c" - } - EOF, - "@TODO Error: Multiple illegal default arms" - ]; - yield "Missing match" => [ - <<<'EOF' + << "a" SomeEnum.B -> "a" @@ -146,7 +137,7 @@ public static function malformedEnumExamples(): iterable ]; yield "Non existent enum member access" => [ - <<<'EOF' + << "a" SomeEnum.B -> "a" @@ -158,7 +149,7 @@ public static function malformedEnumExamples(): iterable ]; yield "Duplicate match 1" => [ - <<<'EOF' + << "a" SomeEnum.A -> "a" @@ -168,7 +159,7 @@ public static function malformedEnumExamples(): iterable ]; yield "Duplicate match 2" => [ - <<<'EOF' + << "a" } @@ -177,7 +168,7 @@ public static function malformedEnumExamples(): iterable ]; yield "Incompatible enum types" => [ - <<<'EOF' + << "a" } @@ -186,7 +177,7 @@ public static function malformedEnumExamples(): iterable ]; yield "Cant match enum and string" => [ - <<<'EOF' + << "a" } @@ -195,7 +186,7 @@ public static function malformedEnumExamples(): iterable ]; yield "Matching enum value should be referenced statically" => [ - <<<'EOF' + << "a" } @@ -211,27 +202,30 @@ public static function malformedEnumExamples(): iterable public function malformedMatchCannotBeResolved(string $matchAsString, string $expectedErrorMessage): void { $this->expectExceptionMessage($expectedErrorMessage); - $someStaticEnumType = EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A B C }' - ) + $scope = new DummyScope( + [ + $someStaticEnumType = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration( + 'enum SomeEnum { A B C }' + ) + ), + $otherStaticEnumType = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString("module-a"), + ASTNodeFixtures::EnumDeclaration('enum OtherEnum { A }') + ) + ], + [ + 'SomeEnum' => $someStaticEnumType, + 'someEnumValue' => $someStaticEnumType->toEnumInstanceType(), + 'OtherEnum' => $otherStaticEnumType + ] ); - $scope = new DummyScope([ - 'SomeEnum' => $someStaticEnumType, - 'someEnumValue' => $someStaticEnumType->toEnumInstanceType(), - 'OtherEnum' => EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString('enum OtherEnum { A }') - ) - - ]); $matchTypeResolver = new MatchTypeResolver( scope: $scope ); - $matchNode = ExpressionNode::fromString($matchAsString)->root; - assert($matchNode instanceof MatchNode); + $matchNode = ASTNodeFixtures::Match($matchAsString); $matchTypeResolver->resolveTypeOf($matchNode); } diff --git a/test/Unit/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolverTest.php index 26750ae1..f4a4c2f9 100644 --- a/test/Unit/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/NullLiteral/NullLiteralTypeResolverTest.php @@ -22,8 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\NullLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Resolver\NullLiteral\NullLiteralTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PHPUnit\Framework\TestCase; @@ -36,10 +35,9 @@ final class NullLiteralTypeResolverTest extends TestCase public function resolvesNullLiteralToNullType(): void { $nullLiteralTypeResolver = new NullLiteralTypeResolver(); - $nullLiteralNode = ExpressionNode::fromString('null')->root; - assert($nullLiteralNode instanceof NullLiteralNode); + $nullLiteralNode = ASTNodeFixtures::NullLiteral('null'); - $expectedType = NullType::get(); + $expectedType = NullType::singleton(); $actualType = $nullLiteralTypeResolver->resolveTypeOf($nullLiteralNode); $this->assertTrue( diff --git a/test/Unit/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolverTest.php index ee4d6b95..caeb4ab2 100644 --- a/test/Unit/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/StringLiteral/StringLiteralTypeResolverTest.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\StringLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\StringLiteralNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Resolver\StringLiteral\StringLiteralTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; @@ -36,9 +36,9 @@ final class StringLiteralTypeResolverTest extends TestCase public function resolvesStringLiteralToStringType(): void { $stringLiteralTypeResolver = new StringLiteralTypeResolver(); - $stringLiteralNode = StringLiteralNode::fromString('"foo"'); + $stringLiteralNode = ASTNodeFixtures::StringLiteral('"foo"'); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $stringLiteralTypeResolver->resolveTypeOf($stringLiteralNode); $this->assertTrue( diff --git a/test/Unit/TypeSystem/Resolver/Tag/TagTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/Tag/TagTypeResolverTest.php index 1374dfbf..48402cce 100644 --- a/test/Unit/TypeSystem/Resolver/Tag/TagTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/Tag/TagTypeResolverTest.php @@ -22,8 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\Tag; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\TagNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Resolver\Tag\TagTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; @@ -37,10 +36,9 @@ final class TagTypeResolverTest extends TestCase public function resolvesTagToStringType(): void { $tagTypeResolver = new TagTypeResolver(); - $tagNode = ExpressionNode::fromString('
')->root; - assert($tagNode instanceof TagNode); + $tagNode = ASTNodeFixtures::Tag('
'); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $tagTypeResolver->resolveTypeOf($tagNode); $this->assertTrue( diff --git a/test/Unit/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolverTest.php index e8914008..29137094 100644 --- a/test/Unit/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TemplateLiteral/TemplateLiteralTypeResolverTest.php @@ -22,7 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\TemplateLiteral; -use PackageFactory\ComponentEngine\Parser\Ast\TemplateLiteralNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Resolver\TemplateLiteral\TemplateLiteralTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; @@ -51,9 +51,9 @@ public static function templateLiteralExamples(): array public function resolvesTemplateLiteralToStringType(string $templateLiteralAsString): void { $templateLiteralTypeResolver = new TemplateLiteralTypeResolver(); - $templateLiteralNode = TemplateLiteralNode::fromString($templateLiteralAsString); + $templateLiteralNode = ASTNodeFixtures::TemplateLiteral($templateLiteralAsString); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $templateLiteralTypeResolver->resolveTypeOf($templateLiteralNode); $this->assertTrue( diff --git a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php index 5d27cf8c..ec6ddc56 100644 --- a/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolverTest.php @@ -22,8 +22,7 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\TernaryOperation; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Resolver\TernaryOperation\TernaryOperationTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; @@ -40,12 +39,12 @@ final class TernaryOperationTypeResolverTest extends TestCase public static function ternaryOperationExamples(): array { return [ - 'true ? 42 : "foo"' => ['true ? 42 : "foo"', IntegerType::get()], - 'false ? 42 : "foo"' => ['false ? 42 : "foo"', StringType::get()], - '1 < 2 ? 42 : "foo"' => ['1 < 2 ? 42 : "foo"', UnionType::of(IntegerType::get(), StringType::get())], + 'true ? 42 : "foo"' => ['true ? 42 : "foo"', IntegerType::singleton()], + 'false ? 42 : "foo"' => ['false ? 42 : "foo"', StringType::singleton()], + '1 < 2 ? 42 : "foo"' => ['1 < 2 ? 42 : "foo"', UnionType::of(IntegerType::singleton(), StringType::singleton())], '1 < 2 ? variableOfTypeString : variableOfTypeNumber' => [ '1 < 2 ? variableOfTypeString : variableOfTypeNumber', - UnionType::of(IntegerType::get(), StringType::get()) + UnionType::of(IntegerType::singleton(), StringType::singleton()) ] ]; } @@ -59,15 +58,19 @@ public static function ternaryOperationExamples(): array */ public function resolvesTernaryOperationToResultingType(string $ternaryExpressionAsString, TypeInterface $expectedType): void { - $scope = new DummyScope([ - 'variableOfTypeString' => StringType::get(), - 'variableOfTypeNumber' => IntegerType::get(), - ]); + $scope = new DummyScope( + [StringType::singleton(), IntegerType::singleton()], + [ + 'variableOfTypeString' => StringType::singleton(), + 'variableOfTypeNumber' => IntegerType::singleton() + ] + ); $ternaryOperationTypeResolver = new TernaryOperationTypeResolver( scope: $scope ); - $ternaryOperationNode = ExpressionNode::fromString($ternaryExpressionAsString)->root; - assert($ternaryOperationNode instanceof TernaryOperationNode); + $ternaryOperationNode = ASTNodeFixtures::TernaryOperation( + $ternaryExpressionAsString + ); $actualType = $ternaryOperationTypeResolver->resolveTypeOf($ternaryOperationNode); diff --git a/test/Unit/TypeSystem/Resolver/Identifier/IdentifierTypeResolverTest.php b/test/Unit/TypeSystem/Resolver/ValueReference/ValueReferenceTypeResolverTest.php similarity index 64% rename from test/Unit/TypeSystem/Resolver/Identifier/IdentifierTypeResolverTest.php rename to test/Unit/TypeSystem/Resolver/ValueReference/ValueReferenceTypeResolverTest.php index 2b548f9d..3de632d4 100644 --- a/test/Unit/TypeSystem/Resolver/Identifier/IdentifierTypeResolverTest.php +++ b/test/Unit/TypeSystem/Resolver/ValueReference/ValueReferenceTypeResolverTest.php @@ -20,29 +20,27 @@ declare(strict_types=1); -namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\Identifier; +namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Resolver\ValueReference; -use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; -use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; -use PackageFactory\ComponentEngine\TypeSystem\Resolver\Identifier\IdentifierTypeResolver; +use PackageFactory\ComponentEngine\TypeSystem\Resolver\ValueReference\ValueReferenceTypeResolver; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; -final class IdentifierTypeResolverTest extends TestCase +final class ValueReferenceTypeResolverTest extends TestCase { /** * @test * @return void */ - public function resolvesKnownIdentifierToItsType(): void + public function resolvesKnownValueReferenceToItsType(): void { - $scope = new DummyScope(['foo' => StringType::get()]); - $identifierTypeResolver = new IdentifierTypeResolver(scope: $scope); - $identifierNode = ExpressionNode::fromString('foo')->root; - assert($identifierNode instanceof IdentifierNode); + $scope = new DummyScope([StringType::singleton()], ['foo' => StringType::singleton()]); + $identifierTypeResolver = new ValueReferenceTypeResolver(scope: $scope); + $identifierNode = ASTNodeFixtures::ValueReference('foo'); - $expectedType = StringType::get(); + $expectedType = StringType::singleton(); $actualType = $identifierTypeResolver->resolveTypeOf($identifierNode); $this->assertTrue( @@ -55,12 +53,11 @@ public function resolvesKnownIdentifierToItsType(): void * @test * @return void */ - public function throwsIfGivenIdentifierIsUnknown(): void + public function throwsIfGivenValueReferenceIsUnknown(): void { $scope = new DummyScope(); - $identifierTypeResolver = new IdentifierTypeResolver(scope: $scope); - $identifierNode = ExpressionNode::fromString('foo')->root; - assert($identifierNode instanceof IdentifierNode); + $identifierTypeResolver = new ValueReferenceTypeResolver(scope: $scope); + $identifierNode = ASTNodeFixtures::ValueReference('foo'); $this->expectExceptionMessageMatches('/unknown identifier/i'); diff --git a/test/Unit/TypeSystem/Scope/ComponentScope/ComponentScopeTest.php b/test/Unit/TypeSystem/Scope/ComponentScope/ComponentScopeTest.php index 4ccc7e31..67b120a8 100644 --- a/test/Unit/TypeSystem/Scope/ComponentScope/ComponentScopeTest.php +++ b/test/Unit/TypeSystem/Scope/ComponentScope/ComponentScopeTest.php @@ -22,10 +22,10 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\ComponentScope; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; use PackageFactory\ComponentEngine\TypeSystem\Scope\ComponentScope\ComponentScope; use PackageFactory\ComponentEngine\TypeSystem\Scope\GlobalScope\GlobalScope; @@ -49,14 +49,16 @@ public function providesTheTypesOfComponentApiMembers(): void return
{foo}
} EOT; - $componentDeclarationNode = ComponentDeclarationNode::fromString($componentDeclarationAsString); + $componentDeclarationNode = ASTNodeFixtures::ComponentDeclaration( + $componentDeclarationAsString + ); $componentScope = new ComponentScope( componentDeclarationNode: $componentDeclarationNode, - parentScope: GlobalScope::get() + parentScope: GlobalScope::singleton() ); - $expectedType = StringType::get(); - $actualType = $componentScope->lookupTypeFor('foo'); + $expectedType = StringType::singleton(); + $actualType = $componentScope->getTypeOf(VariableName::from('foo')); $this->assertNotNull($actualType); @@ -72,6 +74,10 @@ public function providesTheTypesOfComponentApiMembers(): void */ public function providesTheEnumInstanceTypeWhenAnStaticEnumTypeIsReferencedInTheComponentApi(): void { + $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( + ModuleId::fromString('module-a'), + ASTNodeFixtures::EnumDeclaration('enum SomeEnum { A B C }') + ); $componentDeclarationAsString = <<{foo}
} EOT; - $componentDeclarationNode = ComponentDeclarationNode::fromString($componentDeclarationAsString); + $componentDeclarationNode = ASTNodeFixtures::ComponentDeclaration( + $componentDeclarationAsString + ); $componentScope = new ComponentScope( componentDeclarationNode: $componentDeclarationNode, - parentScope: new DummyScope([], [ - 'SomeEnum' => $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( - ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( - 'enum SomeEnum { A B C }' - ) - ) - ]) + parentScope: new DummyScope([$enumStaticType->toEnumInstanceType()]) ); $expectedType = $enumStaticType->toEnumInstanceType(); - $actualType = $componentScope->lookupTypeFor('foo'); + $actualType = $componentScope->getTypeOf(VariableName::from('foo')); $this->assertNotNull($actualType); @@ -116,14 +117,19 @@ public function fallsBackToParentScope(): void return
{foo}
} EOT; - $componentDeclarationNode = ComponentDeclarationNode::fromString($componentDeclarationAsString); + $componentDeclarationNode = ASTNodeFixtures::ComponentDeclaration( + $componentDeclarationAsString + ); $componentScope = new ComponentScope( componentDeclarationNode: $componentDeclarationNode, - parentScope: new DummyScope(['bar' => IntegerType::get()], []) + parentScope: new DummyScope( + [IntegerType::singleton()], + ['bar' => IntegerType::singleton()] + ) ); - $expectedType = IntegerType::get(); - $actualType = $componentScope->lookupTypeFor('bar'); + $expectedType = IntegerType::singleton(); + $actualType = $componentScope->getTypeOf(VariableName::from('bar')); $this->assertNotNull($actualType); @@ -146,16 +152,16 @@ public function resolvesTypeReferencesUsingParentScope(): void return
{foo}
} EOT; - $componentDeclarationNode = ComponentDeclarationNode::fromString($componentDeclarationAsString); + $componentDeclarationNode = ASTNodeFixtures::ComponentDeclaration( + $componentDeclarationAsString + ); $componentScope = new ComponentScope( componentDeclarationNode: $componentDeclarationNode, - parentScope: new DummyScope([], ['StringAlias' => StringType::get()]) + parentScope: new DummyScope([StringType::singleton()]) ); - $expectedType = StringType::get(); - $actualType = $componentScope->resolveTypeReference( - TypeReferenceNode::fromString('StringAlias') - ); + $expectedType = StringType::singleton(); + $actualType = $componentScope->getType(TypeName::from('string')); $this->assertTrue( $expectedType->is($actualType), diff --git a/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php b/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php index 954a1f3c..166d8dce 100644 --- a/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php +++ b/test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php @@ -22,33 +22,47 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; final class DummyScope implements ScopeInterface { /** - * @param array $identifierToTypeMap - * @param array $typeNameToTypeMap + * @var array + */ + private readonly array $typeNameToTypeMap; + + /** + * @param AtomicTypeInterface[] $knownTypes + * @param array $identifierToTypeReferenceMap */ public function __construct( - private readonly array $identifierToTypeMap = [], - private readonly array $typeNameToTypeMap = [] + array $knownTypes = [], + private readonly array $identifierToTypeReferenceMap = [], ) { - } + $typeNameToTypeMap = []; + foreach ($knownTypes as $type) { + /** @var AtomicTypeInterface $type */ + $typeNameToTypeMap[$type->getName()->value] = $type; + } - public function lookupTypeFor(string $name): ?TypeInterface - { - return $this->identifierToTypeMap[$name] ?? null; + $this->typeNameToTypeMap = $typeNameToTypeMap; } - public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface + public function getType(TypeName $typeName): AtomicTypeInterface { - if ($type = $this->typeNameToTypeMap[$typeReferenceNode->name] ?? null) { + if ($type = $this->typeNameToTypeMap[$typeName->value] ?? null) { return $type; } - throw new \Exception('DummyScope: Unknown type ' . $typeReferenceNode->name); + throw new \Exception('DummyScope: Unknown type ' . $typeName->value); + } + + public function getTypeOf(VariableName $variableName): ?TypeInterface + { + return $this->identifierToTypeReferenceMap[$variableName->value] ?? null; } } diff --git a/test/Unit/TypeSystem/Scope/GlobalScope/GlobalScopeTest.php b/test/Unit/TypeSystem/Scope/GlobalScope/GlobalScopeTest.php index f145e7da..42501d6f 100644 --- a/test/Unit/TypeSystem/Scope/GlobalScope/GlobalScopeTest.php +++ b/test/Unit/TypeSystem/Scope/GlobalScope/GlobalScopeTest.php @@ -22,7 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\GlobalScope; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; use PackageFactory\ComponentEngine\TypeSystem\Scope\GlobalScope\GlobalScope; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; @@ -30,41 +31,27 @@ final class GlobalScopeTest extends TestCase { - /** - * @test - * @return void - */ - public function isSingleton(): void - { - $globalScope1 = GlobalScope::get(); - $globalScope2 = GlobalScope::get(); - - $this->assertSame($globalScope1, $globalScope2); - } - /** * @return array */ public static function primitiveTypeExamples(): array { return [ - 'string' => ['string', StringType::get()] + 'string' => ['string', StringType::singleton()] ]; } /** * @dataProvider primitiveTypeExamples * @test - * @param string $typeReferenceAsString + * @param string $typeNameAsString * @param TypeInterface $expectedType * @return void */ - public function resolvesPrimitiveTypes(string $typeReferenceAsString, TypeInterface $expectedType): void + public function resolvesPrimitiveTypes(string $typeNameAsString, TypeInterface $expectedType): void { - $globalScope = GlobalScope::get(); - $typeReferenceNode = TypeReferenceNode::fromString($typeReferenceAsString); - - $actualType = $globalScope->resolveTypeReference($typeReferenceNode); + $globalScope = GlobalScope::singleton(); + $actualType = $globalScope->getType(TypeName::from($typeNameAsString)); $this->assertTrue( $expectedType->is($actualType), @@ -78,8 +65,8 @@ public function resolvesPrimitiveTypes(string $typeReferenceAsString, TypeInterf */ public function knowsNoLocalNames(): void { - $globalScope = GlobalScope::get(); + $globalScope = GlobalScope::singleton(); - $this->assertNull($globalScope->lookupTypeFor('someVariable')); + $this->assertNull($globalScope->getTypeOf(VariableName::from('someVariable'))); } } diff --git a/test/Unit/TypeSystem/Scope/ModuleScope/ModuleScopeTest.php b/test/Unit/TypeSystem/Scope/ModuleScope/ModuleScopeTest.php index 27c68069..6d5a9b6f 100644 --- a/test/Unit/TypeSystem/Scope/ModuleScope/ModuleScopeTest.php +++ b/test/Unit/TypeSystem/Scope/ModuleScope/ModuleScopeTest.php @@ -22,16 +22,38 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\ModuleScope; -use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode; -use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\VariableName\VariableName; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\Test\Unit\Module\Loader\Fixtures\DummyLoader; use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope; +use PackageFactory\ComponentEngine\TypeSystem\AtomicTypeInterface; use PackageFactory\ComponentEngine\TypeSystem\Scope\ModuleScope\ModuleScope; use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; use PHPUnit\Framework\TestCase; final class ModuleScopeTest extends TestCase { + private function mockAtomicType(string $name): AtomicTypeInterface + { + return new class($name) implements AtomicTypeInterface + { + public function __construct(private readonly string $name) + { + } + + public function getName(): TypeName + { + return TypeName::from($this->name); + } + + public function is(TypeInterface $other): bool + { + return $other === $this; + } + }; + } + /** * @test * @return void @@ -41,16 +63,18 @@ public function resolvesTypeReferencesForModuleImports(): void $moduleAsString = << [ - 'Foo' => $typeOfFoo = $this->createStub(TypeInterface::class), + 'Foo' => $typeOfFoo = $this->createStub(AtomicTypeInterface::class), ], './Bar.afx' => [ - 'Bar' => $typeOfBar = $this->createStub(TypeInterface::class), - 'Baz' => $typeOfBaz = $this->createStub(TypeInterface::class) + 'Bar' => $typeOfBar = $this->createStub(AtomicTypeInterface::class), + 'Baz' => $typeOfBaz = $this->createStub(AtomicTypeInterface::class) ], ]), moduleNode: $moduleNode, @@ -59,23 +83,17 @@ public function resolvesTypeReferencesForModuleImports(): void $this->assertSame( $typeOfFoo, - $moduleScope->resolveTypeReference( - TypeReferenceNode::fromString('Foo') - ) + $moduleScope->getType(TypeName::from('Foo')) ); $this->assertSame( $typeOfBar, - $moduleScope->resolveTypeReference( - TypeReferenceNode::fromString('Bar') - ) + $moduleScope->getType(TypeName::from('Bar')) ); $this->assertSame( $typeOfBaz, - $moduleScope->resolveTypeReference( - TypeReferenceNode::fromString('Baz') - ) + $moduleScope->getType(TypeName::from('Baz')) ); } @@ -85,18 +103,23 @@ public function resolvesTypeReferencesForModuleImports(): void */ public function fallsBackToParentScopeWhenProvidingTypesForValues(): void { - $moduleNode = ModuleNode::fromString('from "y" import { y }'); + $moduleNode = ASTNodeFixtures::Module('from "y" import { y } export struct Qux {}'); $moduleScope = new ModuleScope( loader: new DummyLoader(), moduleNode: $moduleNode, - parentScope: new DummyScope([ - 'foo' => $typeOfFoo = $this->createStub(TypeInterface::class), - ]) + parentScope: new DummyScope( + [ + $typeOfFoo = $this->mockAtomicType('Foo') + ], + [ + 'foo' => $typeOfFoo + ] + ) ); $this->assertSame( $typeOfFoo, - $moduleScope->lookupTypeFor('foo') + $moduleScope->getTypeOf(VariableName::from('foo')) ); } } diff --git a/test/Unit/TypeSystem/Type/BooleanType/BooleanTypeTest.php b/test/Unit/TypeSystem/Type/BooleanType/BooleanTypeTest.php index b49b51d6..354a4f52 100644 --- a/test/Unit/TypeSystem/Type/BooleanType/BooleanTypeTest.php +++ b/test/Unit/TypeSystem/Type/BooleanType/BooleanTypeTest.php @@ -22,18 +22,28 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\BooleanType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; final class BooleanTypeTest extends TestCase { + /** + * @test + * @return void + */ + public function providesItsTypeName(): void + { + $this->assertEquals(TypeName::from('boolean'), BooleanType::singleton()->getName()); + } + /** * @test */ public function booleanTypeIsSingleton(): void { - $this->assertSame(BooleanType::get(), BooleanType::get()); + $this->assertSame(BooleanType::singleton(), BooleanType::singleton()); } /** @@ -41,7 +51,7 @@ public function booleanTypeIsSingleton(): void */ public function isReturnsTrueIfGivenTypeIsBooleanType(): void { - $this->assertTrue(BooleanType::get()->is(BooleanType::get())); + $this->assertTrue(BooleanType::singleton()->is(BooleanType::singleton())); } /** @@ -49,6 +59,6 @@ public function isReturnsTrueIfGivenTypeIsBooleanType(): void */ public function isReturnsFalseIfGivenTypeIsNotBooleanType(): void { - $this->assertFalse(BooleanType::get()->is(StringType::get())); + $this->assertFalse(BooleanType::singleton()->is(StringType::singleton())); } } diff --git a/test/Unit/TypeSystem/Type/ComponentType/ComponentTypeTest.php b/test/Unit/TypeSystem/Type/ComponentType/ComponentTypeTest.php index af7ecffc..4aff9a8c 100644 --- a/test/Unit/TypeSystem/Type/ComponentType/ComponentTypeTest.php +++ b/test/Unit/TypeSystem/Type/ComponentType/ComponentTypeTest.php @@ -22,7 +22,8 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\ComponentType; -use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Type\ComponentType\ComponentType; use PHPUnit\Framework\TestCase; @@ -34,8 +35,8 @@ final class ComponentTypeTest extends TestCase */ public function canBeCreatedFromComponentDeclarationNode(): void { - $componentDeclarationNode = ComponentDeclarationNode::fromString( - 'component Foo { a : string b : number return
{a} and {b}
}' + $componentDeclarationNode = ASTNodeFixtures::ComponentDeclaration( + 'component Foo { a: string b: number return
{a} and {b}
}' ); $componentType = ComponentType::fromComponentDeclarationNode( $componentDeclarationNode @@ -50,14 +51,17 @@ public function canBeCreatedFromComponentDeclarationNode(): void */ public function providesNameOfTheComponent(): void { - $componentDeclarationNode = ComponentDeclarationNode::fromString( + $componentDeclarationNode = ASTNodeFixtures::ComponentDeclaration( 'component SomeComponent { return "" }' ); $componentType = ComponentType::fromComponentDeclarationNode( $componentDeclarationNode ); - $this->assertEquals('SomeComponent', $componentType->componentName); + $this->assertEquals( + TypeName::from('SomeComponent'), + $componentType->getName() + ); } /** @@ -66,7 +70,7 @@ public function providesNameOfTheComponent(): void */ public function isEquivalentToItself(): void { - $componentDeclarationNode = ComponentDeclarationNode::fromString( + $componentDeclarationNode = ASTNodeFixtures::ComponentDeclaration( 'component SomeComponent { return "" }' ); $componentType = ComponentType::fromComponentDeclarationNode( diff --git a/test/Unit/TypeSystem/Type/EnumType/EnumStaticTypeTest.php b/test/Unit/TypeSystem/Type/EnumType/EnumStaticTypeTest.php index 6b630e01..4357b382 100644 --- a/test/Unit/TypeSystem/Type/EnumType/EnumStaticTypeTest.php +++ b/test/Unit/TypeSystem/Type/EnumType/EnumStaticTypeTest.php @@ -22,8 +22,10 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\EnumType; +use PackageFactory\ComponentEngine\Domain\EnumMemberName\EnumMemberName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; use PackageFactory\ComponentEngine\Module\ModuleId; -use PackageFactory\ComponentEngine\Parser\Ast\EnumDeclarationNode; +use PackageFactory\ComponentEngine\Test\Unit\Language\ASTNodeFixtures; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumInstanceType; use PackageFactory\ComponentEngine\TypeSystem\Type\EnumType\EnumStaticType; use PHPUnit\Framework\TestCase; @@ -36,7 +38,7 @@ final class EnumStaticTypeTest extends TestCase */ public function canBeCreatedFromEnumDeclarationNode(): void { - $enumDeclarationNode = EnumDeclarationNode::fromString( + $enumDeclarationNode = ASTNodeFixtures::EnumDeclaration( 'enum Foo { BAR BAZ }' ); $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( @@ -53,7 +55,7 @@ public function canBeCreatedFromEnumDeclarationNode(): void */ public function providesNameOfTheEnum(): void { - $enumDeclarationNode = EnumDeclarationNode::fromString( + $enumDeclarationNode = ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum {}' ); $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( @@ -61,7 +63,10 @@ public function providesNameOfTheEnum(): void $enumDeclarationNode ); - $this->assertEquals('SomeEnum', $enumStaticType->enumName); + $this->assertEquals( + TypeName::from('SomeEnum'), + $enumStaticType->getName() + ); } /** @@ -71,7 +76,7 @@ public function providesMemberNames(): void { $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( + ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum { A B C }' ) ); @@ -86,16 +91,16 @@ public function providesMemberType(): void { $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( + ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum { A B C }' ) ); - $enumMemberType = $enumStaticType->getMemberType('A'); + $enumMemberType = $enumStaticType->getMemberType(EnumMemberName::from('A')); $this->assertInstanceOf(EnumInstanceType::class, $enumMemberType); - $this->assertSame($enumStaticType, $enumMemberType->enumStaticType); - $this->assertSame('A', $enumMemberType->getMemberName()); + $this->assertEquals($enumStaticType, $enumMemberType->enumStaticType); + $this->assertEquals(EnumMemberName::from('A'), $enumMemberType->getMemberName()); } /** @@ -107,12 +112,12 @@ public function canOnlyAccessValidMemberType(): void $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( ModuleId::fromString("module-a"), - EnumDeclarationNode::fromString( + ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum { A B C }' ) ); - $enumStaticType->getMemberType('NonExistent'); + $enumStaticType->getMemberType(EnumMemberName::from('NonExistent')); } /** @@ -121,7 +126,7 @@ public function canOnlyAccessValidMemberType(): void */ public function canBeTransformedIntoInstanceType(): void { - $enumDeclarationNode = EnumDeclarationNode::fromString( + $enumDeclarationNode = ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum { A }' ); $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( @@ -144,7 +149,7 @@ public function canBeTransformedIntoInstanceType(): void */ public function isEquivalentToItself(): void { - $enumDeclarationNode = EnumDeclarationNode::fromString( + $enumDeclarationNode = ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum { A }' ); $enumStaticType = EnumStaticType::fromModuleIdAndDeclaration( @@ -161,14 +166,14 @@ public function isEquivalentToItself(): void */ public function canBeComparedToOther(): void { - $enumDeclarationNode1 = EnumDeclarationNode::fromString( + $enumDeclarationNode1 = ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum { A }' ); $enumStaticType1 = EnumStaticType::fromModuleIdAndDeclaration( ModuleId::fromString("module-a"), $enumDeclarationNode1 ); - $enumDeclarationNode2 = EnumDeclarationNode::fromString( + $enumDeclarationNode2 = ASTNodeFixtures::EnumDeclaration( 'enum SomeEnum { A }' ); $enumStaticType2 = EnumStaticType::fromModuleIdAndDeclaration( diff --git a/test/Unit/TypeSystem/Type/IntegerType/IntegerTypeTest.php b/test/Unit/TypeSystem/Type/IntegerType/IntegerTypeTest.php index 2c67e3cd..a31d5d6c 100644 --- a/test/Unit/TypeSystem/Type/IntegerType/IntegerTypeTest.php +++ b/test/Unit/TypeSystem/Type/IntegerType/IntegerTypeTest.php @@ -22,18 +22,28 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\IntegerType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; final class IntegerTypeTest extends TestCase { + /** + * @test + * @return void + */ + public function providesItsTypeName(): void + { + $this->assertEquals(TypeName::from('number'), IntegerType::singleton()->getName()); + } + /** * @test */ public function integerTypeIsSingleton(): void { - $this->assertSame(IntegerType::get(), IntegerType::get()); + $this->assertSame(IntegerType::singleton(), IntegerType::singleton()); } /** @@ -41,7 +51,7 @@ public function integerTypeIsSingleton(): void */ public function isReturnsTrueIfGivenTypeIsIntegerType(): void { - $this->assertTrue(IntegerType::get()->is(IntegerType::get())); + $this->assertTrue(IntegerType::singleton()->is(IntegerType::singleton())); } /** @@ -49,6 +59,6 @@ public function isReturnsTrueIfGivenTypeIsIntegerType(): void */ public function isReturnsFalseIfGivenTypeIsNotIntegerType(): void { - $this->assertFalse(IntegerType::get()->is(StringType::get())); + $this->assertFalse(IntegerType::singleton()->is(StringType::singleton())); } } diff --git a/test/Unit/TypeSystem/Type/NullType/NullTypeTest.php b/test/Unit/TypeSystem/Type/NullType/NullTypeTest.php index b5b82d8c..706ec820 100644 --- a/test/Unit/TypeSystem/Type/NullType/NullTypeTest.php +++ b/test/Unit/TypeSystem/Type/NullType/NullTypeTest.php @@ -22,18 +22,28 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\NullType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; final class NullTypeTest extends TestCase { + /** + * @test + * @return void + */ + public function providesItsTypeName(): void + { + $this->assertEquals(TypeName::from('null'), NullType::singleton()->getName()); + } + /** * @test */ public function nullTypeIsSingleton(): void { - $this->assertSame(NullType::get(), NullType::get()); + $this->assertSame(NullType::singleton(), NullType::singleton()); } /** @@ -41,7 +51,7 @@ public function nullTypeIsSingleton(): void */ public function isReturnsTrueIfGivenTypeIsNullType(): void { - $this->assertTrue(NullType::get()->is(NullType::get())); + $this->assertTrue(NullType::singleton()->is(NullType::singleton())); } /** @@ -49,6 +59,6 @@ public function isReturnsTrueIfGivenTypeIsNullType(): void */ public function isReturnsFalseIfGivenTypeIsNotNullType(): void { - $this->assertFalse(NullType::get()->is(StringType::get())); + $this->assertFalse(NullType::singleton()->is(StringType::singleton())); } } diff --git a/test/Unit/TypeSystem/Type/SlotType/SlotTypeTest.php b/test/Unit/TypeSystem/Type/SlotType/SlotTypeTest.php index d2c7e5c7..9a0cb27d 100644 --- a/test/Unit/TypeSystem/Type/SlotType/SlotTypeTest.php +++ b/test/Unit/TypeSystem/Type/SlotType/SlotTypeTest.php @@ -22,18 +22,28 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\SlotType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType; use PHPUnit\Framework\TestCase; final class SlotTypeTest extends TestCase { + /** + * @test + * @return void + */ + public function providesItsTypeName(): void + { + $this->assertEquals(TypeName::from('slot'), SlotType::singleton()->getName()); + } + /** * @test */ public function stringTypeIsSingleton(): void { - $this->assertSame(SlotType::get(), SlotType::get()); + $this->assertSame(SlotType::singleton(), SlotType::singleton()); } /** @@ -41,7 +51,7 @@ public function stringTypeIsSingleton(): void */ public function isReturnsTrueIfGivenTypeIsSlotType(): void { - $this->assertTrue(SlotType::get()->is(SlotType::get())); + $this->assertTrue(SlotType::singleton()->is(SlotType::singleton())); } /** @@ -49,6 +59,6 @@ public function isReturnsTrueIfGivenTypeIsSlotType(): void */ public function isReturnsFalseIfGivenTypeIsNotSlotType(): void { - $this->assertFalse(SlotType::get()->is(IntegerType::get())); + $this->assertFalse(SlotType::singleton()->is(IntegerType::singleton())); } } diff --git a/test/Unit/TypeSystem/Type/StringType/StringTypeTest.php b/test/Unit/TypeSystem/Type/StringType/StringTypeTest.php index 44c6e812..9359f92e 100644 --- a/test/Unit/TypeSystem/Type/StringType/StringTypeTest.php +++ b/test/Unit/TypeSystem/Type/StringType/StringTypeTest.php @@ -22,18 +22,28 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\StringType; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; use PackageFactory\ComponentEngine\TypeSystem\Type\IntegerType\IntegerType; use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; use PHPUnit\Framework\TestCase; final class StringTypeTest extends TestCase { + /** + * @test + * @return void + */ + public function providesItsTypeName(): void + { + $this->assertEquals(TypeName::from('string'), StringType::singleton()->getName()); + } + /** * @test */ public function stringTypeIsSingleton(): void { - $this->assertSame(StringType::get(), StringType::get()); + $this->assertSame(StringType::singleton(), StringType::singleton()); } /** @@ -41,7 +51,7 @@ public function stringTypeIsSingleton(): void */ public function isReturnsTrueIfGivenTypeIsStringType(): void { - $this->assertTrue(StringType::get()->is(StringType::get())); + $this->assertTrue(StringType::singleton()->is(StringType::singleton())); } /** @@ -49,6 +59,6 @@ public function isReturnsTrueIfGivenTypeIsStringType(): void */ public function isReturnsFalseIfGivenTypeIsNotStringType(): void { - $this->assertFalse(StringType::get()->is(IntegerType::get())); + $this->assertFalse(StringType::singleton()->is(IntegerType::singleton())); } } diff --git a/test/Unit/TypeSystem/Type/StructType/StructTypeTest.php b/test/Unit/TypeSystem/Type/StructType/StructTypeTest.php index 23313e95..99c85267 100644 --- a/test/Unit/TypeSystem/Type/StructType/StructTypeTest.php +++ b/test/Unit/TypeSystem/Type/StructType/StructTypeTest.php @@ -22,8 +22,15 @@ namespace PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Type\StructType; -use PackageFactory\ComponentEngine\Parser\Ast\StructDeclarationNode; +use PackageFactory\ComponentEngine\Domain\PropertyName\PropertyName; +use PackageFactory\ComponentEngine\Domain\StructName\StructName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeName; +use PackageFactory\ComponentEngine\Domain\TypeName\TypeNames; +use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Properties; +use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\Property; use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType; +use PackageFactory\ComponentEngine\TypeSystem\TypeReference; use PHPUnit\Framework\TestCase; final class StructTypeTest extends TestCase @@ -32,45 +39,56 @@ final class StructTypeTest extends TestCase * @test * @return void */ - public function canBeCreatedFromStructDeclarationNode(): void + public function providesNameOfTheStruct(): void { - $structDeclarationNode = StructDeclarationNode::fromString( - 'struct Foo { a : string b : number }' + $structType = new StructType( + name: StructName::from('SomeStruct'), + properties: new Properties() ); - $structType = StructType::fromStructDeclarationNode($structDeclarationNode); - $this->assertInstanceOf(StructType::class, $structType); + $this->assertEquals( + TypeName::from('SomeStruct'), + $structType->getName() + ); } /** * @test * @return void */ - public function providesNameOfTheStruct(): void + public function isEquivalentToItself(): void { - $structDeclarationNode = StructDeclarationNode::fromString( - 'struct SomeStruct {}' - ); - $structStaticType = StructType::fromStructDeclarationNode( - $structDeclarationNode + $structType = new StructType( + name: StructName::from('SomeStruct'), + properties: new Properties() ); - $this->assertEquals('SomeStruct', $structStaticType->structName); + $this->assertTrue($structType->is($structType)); } /** * @test * @return void */ - public function isEquivalentToItself(): void + public function providesTypeOfProperty(): void { - $structDeclarationNode = StructDeclarationNode::fromString( - 'struct SomeStruct {}' - ); - $structStaticType = StructType::fromStructDeclarationNode( - $structDeclarationNode + $structType = new StructType( + name: StructName::from('SomeStruct'), + properties: new Properties( + new Property( + name: PropertyName::from('foo'), + type: $typeOfFoo = new TypeReference( + names: new TypeNames(TypeName::from('string')), + isOptional: false, + isArray: false + ) + ) + ) ); - $this->assertTrue($structStaticType->is($structStaticType)); + $this->assertEquals( + $typeOfFoo, + $structType->getTypeOfProperty(PropertyName::from('foo')) + ); } } diff --git a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php index 3604ae66..437af0eb 100644 --- a/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php +++ b/test/Unit/TypeSystem/Type/UnionType/UnionTypeTest.php @@ -35,21 +35,13 @@ final class UnionTypeTest extends TestCase */ public function staticOfResolvesToGivenTypeIfOnlyOneTypeIsGiven(): void { - $unionType = UnionType::of(StringType::get()); - $this->assertTrue($unionType->is(StringType::get())); - $this->assertTrue(StringType::get()->is($unionType)); + $unionType = UnionType::of(StringType::singleton()); + $this->assertTrue($unionType->is(StringType::singleton())); + $this->assertTrue(StringType::singleton()->is($unionType)); - $unionType = UnionType::of(IntegerType::get()); - $this->assertTrue($unionType->is(IntegerType::get())); - $this->assertTrue(IntegerType::get()->is($unionType)); - - $unionType = UnionType::of(UnionType::of(StringType::get())); - $this->assertTrue($unionType->is(StringType::get())); - $this->assertTrue(StringType::get()->is($unionType)); - - $unionType = UnionType::of(UnionType::of(IntegerType::get())); - $this->assertTrue($unionType->is(IntegerType::get())); - $this->assertTrue(IntegerType::get()->is($unionType)); + $unionType = UnionType::of(IntegerType::singleton()); + $this->assertTrue($unionType->is(IntegerType::singleton())); + $this->assertTrue(IntegerType::singleton()->is($unionType)); } /** @@ -57,13 +49,9 @@ public function staticOfResolvesToGivenTypeIfOnlyOneTypeIsGiven(): void */ public function staticOfResolvesToGivenTypeIfAllGivenTypesAreIdentical(): void { - $unionType = UnionType::of(StringType::get(), StringType::get()); - $this->assertTrue($unionType->is(StringType::get())); - $this->assertTrue(StringType::get()->is($unionType)); - - $unionType = UnionType::of(StringType::get(), StringType::get(), UnionType::of(StringType::get(), StringType::get())); - $this->assertTrue($unionType->is(StringType::get())); - $this->assertTrue(StringType::get()->is($unionType)); + $unionType = UnionType::of(StringType::singleton(), StringType::singleton(), StringType::singleton()); + $this->assertTrue($unionType->is(StringType::singleton())); + $this->assertTrue(StringType::singleton()->is($unionType)); } /** @@ -71,8 +59,8 @@ public function staticOfResolvesToGivenTypeIfAllGivenTypesAreIdentical(): void */ public function isReturnsTrueIfGivenTypeIsCongruentUnionType(): void { - $unionType = UnionType::of(StringType::get(), IntegerType::get()); - $otherUnionType = UnionType::of(IntegerType::get(), StringType::get()); + $unionType = UnionType::of(StringType::singleton(), IntegerType::singleton()); + $otherUnionType = UnionType::of(IntegerType::singleton(), StringType::singleton()); $this->assertTrue($unionType->is($otherUnionType)); } @@ -82,8 +70,8 @@ public function isReturnsTrueIfGivenTypeIsCongruentUnionType(): void */ public function isReturnsTrueIfGivenTypeIsCongruentUnionTypeWithRedundantMembers(): void { - $unionType = UnionType::of(StringType::get(), IntegerType::get()); - $otherUnionType = UnionType::of(IntegerType::get(), StringType::get(), IntegerType::get(), StringType::get()); + $unionType = UnionType::of(StringType::singleton(), IntegerType::singleton()); + $otherUnionType = UnionType::of(IntegerType::singleton(), StringType::singleton(), IntegerType::singleton(), StringType::singleton()); $this->assertTrue($unionType->is($otherUnionType)); } @@ -93,10 +81,10 @@ public function isReturnsTrueIfGivenTypeIsCongruentUnionTypeWithRedundantMembers */ public function isReturnsFalseIfGivenTypeIsNotAUnionType(): void { - $unionType = UnionType::of(StringType::get(), IntegerType::get()); + $unionType = UnionType::of(StringType::singleton(), IntegerType::singleton()); - $this->assertFalse($unionType->is(IntegerType::get())); - $this->assertFalse($unionType->is(StringType::get())); + $this->assertFalse($unionType->is(IntegerType::singleton())); + $this->assertFalse($unionType->is(StringType::singleton())); } /** @@ -104,8 +92,8 @@ public function isReturnsFalseIfGivenTypeIsNotAUnionType(): void */ public function isReturnsFalseIfGivenTypeIsANonCongruentUnionType(): void { - $unionType = UnionType::of(StringType::get(), IntegerType::get()); - $otherUnionType = UnionType::of(StringType::get(), BooleanType::get()); + $unionType = UnionType::of(StringType::singleton(), IntegerType::singleton()); + $otherUnionType = UnionType::of(StringType::singleton(), BooleanType::singleton()); $this->assertFalse($unionType->is($otherUnionType)); }