├── LICENSE ├── README.md ├── bin └── build ├── composer.json ├── phpstan.neon ├── resources ├── .meta-storm.xml ├── grammar.php ├── grammar.pp2 └── grammar │ ├── attribute.pp2 │ ├── callable.pp2 │ ├── common.pp2 │ ├── lexemes.pp2 │ ├── literals.pp2 │ ├── named-type.pp2 │ ├── shape-fields.pp2 │ ├── template-arguments.pp2 │ └── ternary.pp2 └── src ├── Exception ├── FeatureNotAllowedException.php ├── Formatter.php ├── ParseException.php ├── ParserExceptionInterface.php └── SemanticException.php ├── InMemoryCachedParser.php ├── MutableTraverserInterface.php ├── Node ├── FullQualifiedName.php ├── Identifier.php ├── Literal │ ├── BoolLiteralNode.php │ ├── FloatLiteralNode.php │ ├── IntLiteralNode.php │ ├── LiteralNode.php │ ├── LiteralNodeInterface.php │ ├── NullLiteralNode.php │ ├── ParsableLiteralNodeInterface.php │ ├── StringLiteralNode.php │ └── VariableLiteralNode.php ├── Name.php ├── Node.php ├── NodeInterface.php ├── NodeList.php ├── Statement.php └── Stmt │ ├── Attribute │ ├── AttributeArgumentNode.php │ ├── AttributeArgumentsListNode.php │ ├── AttributeGroupNode.php │ ├── AttributeGroupsListNode.php │ └── AttributeNode.php │ ├── Callable │ ├── CallableParameterNode.php │ ├── CallableParametersListNode.php │ ├── ParameterNode.php │ └── ParametersListNode.php │ ├── CallableTypeNode.php │ ├── ClassConstMaskNode.php │ ├── ClassConstNode.php │ ├── Condition │ ├── Condition.php │ ├── EqualConditionNode.php │ ├── GreaterOrEqualThanConditionNode.php │ ├── GreaterThanConditionNode.php │ ├── LessOrEqualThanConditionNode.php │ ├── LessThanConditionNode.php │ └── NotEqualConditionNode.php │ ├── ConstMaskNode.php │ ├── GenericTypeNode.php │ ├── GenericTypeStmt.php │ ├── IntersectionTypeNode.php │ ├── LogicalTypeNode.php │ ├── NamedTypeNode.php │ ├── NullableTypeNode.php │ ├── Shape │ ├── ExplicitFieldNode.php │ ├── FieldNode.php │ ├── FieldsListNode.php │ ├── ImplicitFieldNode.php │ ├── NamedFieldNode.php │ ├── NumericFieldNode.php │ └── StringNamedFieldNode.php │ ├── Template │ ├── ArgumentNode.php │ ├── ArgumentsListNode.php │ ├── TemplateArgumentNode.php │ └── TemplateArgumentsListNode.php │ ├── TernaryConditionNode.php │ ├── TypeOffsetAccessNode.php │ ├── TypeStatement.php │ ├── TypesListNode.php │ └── UnionTypeNode.php ├── Parser.php ├── ParserInterface.php ├── Traverser.php ├── Traverser ├── ClassNameMatcherVisitor.php ├── Command.php ├── DumperVisitor.php ├── MatcherVisitor.php ├── StreamDumperVisitor.php ├── StringDumperVisitor.php ├── TypeMapVisitor.php ├── Visitor.php └── VisitorInterface.php ├── TraverserInterface.php ├── TypeResolver.php └── TypeResolverInterface.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nesmeyanov Kirill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --- 6 | 7 |

8 | PHP 8.1+ 9 | Latest Stable Version 10 | Latest Unstable Version 11 | License MIT 12 | MetaStorm 13 |

14 |

15 | 16 | 17 | 18 |

19 | 20 | Reference implementation for TypeLang Parser. 21 | 22 | **TypeLang** is a declarative type language inspired by static analyzers 23 | like [PHPStan](https://phpstan.org/) and [Psalm](https://psalm.dev/docs/). 24 | 25 | Read [documentation pages](https://typelang.dev) for more information. 26 | 27 | ## Installation 28 | 29 | TypeLang Parser is available as Composer repository and can be installed 30 | using the following command in a root of your project: 31 | 32 | ```sh 33 | composer require type-lang/parser 34 | ``` 35 | 36 | ## Quick Start 37 | 38 | ```php 39 | $parser = new \TypeLang\Parser\Parser(); 40 | 41 | $type = $parser->parse(<<<'PHP' 42 | array{ 43 | key: callable(Example, int): mixed, 44 | ... 45 | } 46 | PHP); 47 | 48 | var_dump($type); 49 | ``` 50 | 51 | Expected Output: 52 | 53 | ```php 54 | TypeLang\Parser\Node\Stmt\NamedTypeNode { 55 | +offset: 0 56 | +name: TypeLang\Parser\Node\Name { 57 | +offset: 0 58 | -parts: array:1 [ 59 | 0 => TypeLang\Parser\Node\Identifier { 60 | +offset: 0 61 | +value: "array" 62 | } 63 | ] 64 | } 65 | +arguments: null 66 | +fields: TypeLang\Parser\Node\Stmt\Shape\FieldsListNode { 67 | +offset: 11 68 | +items: array:1 [ 69 | 0 => TypeLang\Parser\Node\Stmt\Shape\NamedFieldNode { 70 | +offset: 11 71 | +type: TypeLang\Parser\Node\Stmt\CallableTypeNode { 72 | +offset: 16 73 | +name: TypeLang\Parser\Node\Name { 74 | +offset: 16 75 | -parts: array:1 [ 76 | 0 => TypeLang\Parser\Node\Identifier { 77 | +offset: 16 78 | +value: "callable" 79 | } 80 | ] 81 | } 82 | +parameters: TypeLang\Parser\Node\Stmt\Callable\ParametersListNode { 83 | +offset: 25 84 | +items: array:2 [ 85 | 0 => TypeLang\Parser\Node\Stmt\Callable\ParameterNode { 86 | +offset: 25 87 | +type: TypeLang\Parser\Node\Stmt\NamedTypeNode { 88 | +offset: 25 89 | +name: TypeLang\Parser\Node\Name { 90 | +offset: 25 91 | -parts: array:1 [ 92 | 0 => TypeLang\Parser\Node\Identifier { 93 | +offset: 25 94 | +value: "Example" 95 | } 96 | ] 97 | } 98 | +arguments: null 99 | +fields: null 100 | } 101 | +name: null 102 | +output: false 103 | +variadic: false 104 | +optional: false 105 | } 106 | 1 => TypeLang\Parser\Node\Stmt\Callable\ParameterNode { 107 | +offset: 34 108 | +type: TypeLang\Parser\Node\Stmt\NamedTypeNode { 109 | +offset: 34 110 | +name: TypeLang\Parser\Node\Name { 111 | +offset: 34 112 | -parts: array:1 [ 113 | 0 => TypeLang\Parser\Node\Identifier { 114 | +offset: 34 115 | +value: "int" 116 | } 117 | ] 118 | } 119 | +arguments: null 120 | +fields: null 121 | } 122 | +name: null 123 | +output: false 124 | +variadic: false 125 | +optional: false 126 | } 127 | ] 128 | } 129 | +type: TypeLang\Parser\Node\Stmt\NamedTypeNode { 130 | +offset: 40 131 | +name: TypeLang\Parser\Node\Name { 132 | +offset: 40 133 | -parts: array:1 [ 134 | 0 => TypeLang\Parser\Node\Identifier { 135 | +offset: 40 136 | +value: "mixed" 137 | } 138 | ] 139 | } 140 | +arguments: null 141 | +fields: null 142 | } 143 | } 144 | +optional: false 145 | +key: TypeLang\Parser\Node\Identifier { 146 | +offset: 11 147 | +value: "key" 148 | } 149 | } 150 | ] 151 | +sealed: false 152 | } 153 | } 154 | ``` 155 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load(new SplFileInfo(__DIR__ . '/../resources/grammar.pp2')) 36 | ->build() 37 | ->withClassReference('TypeLang\\Parser\\Node') 38 | ->withClassReference('TypeLang\\Parser\\Exception') 39 | ->withClassReference('TypeLang\\Parser\\Exception\\SemanticException') 40 | ->withClassReference('TypeLang\\Parser\\Exception\\FeatureNotAllowedException') 41 | ->generate(); 42 | 43 | file_put_contents(__DIR__ . '/../resources/grammar.php', $grammar); 44 | 45 | // 46 | // Postprocess and optimize output grammar 47 | // 48 | 49 | $data = file_get_contents(__DIR__ . '/../resources/grammar.php'); 50 | 51 | // Replace functions to static one 52 | file_put_contents(__DIR__ . '/../resources/grammar.php', $data); 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-lang/parser", 3 | "type": "library", 4 | "description": "Library for parsing and validating TypeLang syntax and converting it into AST nodes", 5 | "keywords": ["parser", "language", "php", "phpdoc"], 6 | "license": "MIT", 7 | "support": { 8 | "source": "https://github.com/php-type-language/parser", 9 | "issues": "https://github.com/php-type-language/parser/issues" 10 | }, 11 | "require": { 12 | "php": "^8.1", 13 | "phplrt/lexer": "^3.7", 14 | "phplrt/parser": "^3.7", 15 | "phplrt/source": "^3.7" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "TypeLang\\Parser\\": "src" 20 | } 21 | }, 22 | "require-dev": { 23 | "friendsofphp/php-cs-fixer": "^3.70", 24 | "jetbrains/phpstorm-attributes": "^1.0", 25 | "phplrt/compiler": "^3.7", 26 | "phpstan/phpstan": "^2.1", 27 | "phpstan/phpstan-strict-rules": "^2.0", 28 | "phpunit/phpunit": "^10.5|^11.0|^12.0", 29 | "symfony/var-dumper": "^5.4|^6.0|^7.0" 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "TypeLang\\Parser\\Tests\\": "tests" 34 | } 35 | }, 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "1.x-dev", 39 | "dev-main": "1.x-dev" 40 | } 41 | }, 42 | "config": { 43 | "sort-packages": true, 44 | "platform-check": true, 45 | "bin-compat": "full", 46 | "optimize-autoloader": true, 47 | "preferred-install": { 48 | "*": "dist" 49 | } 50 | }, 51 | "scripts": { 52 | "build": "@php bin/build", 53 | 54 | "test": ["@test:unit", "@test:functional"], 55 | "test:unit": "phpunit --testdox --testsuite=unit", 56 | "test:functional": "phpunit --testsuite=functional", 57 | 58 | "linter": "@linter:check", 59 | "linter:check": "phpstan analyse --configuration phpstan.neon", 60 | "linter:baseline": "phpstan analyse --configuration phpstan.neon --generate-baseline", 61 | 62 | "phpcs": "@phpcs:check", 63 | "phpcs:check": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --dry-run --verbose --diff", 64 | "phpcs:fix": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --verbose --diff" 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true 68 | } 69 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phar://phpstan.phar/conf/bleedingEdge.neon 3 | - vendor/phpstan/phpstan-strict-rules/rules.neon 4 | parameters: 5 | level: max 6 | strictRules: 7 | allRules: true 8 | fileExtensions: 9 | - php 10 | paths: 11 | - src 12 | tmpDir: vendor/.cache.phpstan 13 | reportUnmatchedIgnoredErrors: false 14 | -------------------------------------------------------------------------------- /resources/.meta-storm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/grammar.php: -------------------------------------------------------------------------------- 1 | , 15 | * ... 16 | * }, 17 | * skip: list, 18 | * grammar: array, 19 | * reducers: array, 20 | * transitions?: array 21 | * } 22 | */ 23 | return [ 24 | 'initial' => 59, 25 | 'tokens' => [ 26 | 'default' => [ 27 | 'T_DQ_STRING_LITERAL' => '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"', 28 | 'T_SQ_STRING_LITERAL' => '\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'', 29 | 'T_PFX_FLOAT_LITERAL' => '\\-?(?i)[0-9]++\\.[0-9]*+(?:e-?[0-9]++)?', 30 | 'T_SFX_FLOAT_LITERAL' => '\\-?(?i)[0-9]*+\\.[0-9]++(?:e-?[0-9]++)?', 31 | 'T_EXP_LITERAL' => '\\-?(?i)[0-9]++e-?[0-9]++', 32 | 'T_BIN_INT_LITERAL' => '\\-?(?i)0b[0-1_]++', 33 | 'T_OCT_INT_LITERAL' => '\\-?(?i)0o[0-7_]++', 34 | 'T_HEX_INT_LITERAL' => '\\-?(?i)0x[0-9a-f_]++', 35 | 'T_DEC_INT_LITERAL' => '\\-?(?i)[0-9][0-9_]*+', 36 | 'T_BOOL_LITERAL' => '(?i)(?:true|false)(?![a-zA-Z0-9\\-_\\x80-\\xff])', 37 | 'T_NULL_LITERAL' => '(?i)(?:null)(?![a-zA-Z0-9\\-_\\x80-\\xff])', 38 | 'T_NEQ' => '(?i)is\\h+not(?![a-zA-Z0-9\\-_\\x80-\\xff])', 39 | 'T_EQ' => '(?i)is(?![a-zA-Z0-9\\-_\\x80-\\xff])', 40 | 'T_THIS' => '\\$this\\b', 41 | 'T_VARIABLE' => '\\$[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*', 42 | 'T_NAME_WITH_SPACE' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*\\s+?', 43 | 'T_NAME' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*', 44 | 'T_LTE' => '<=', 45 | 'T_GTE' => '>=', 46 | 'T_ANGLE_BRACKET_OPEN' => '<', 47 | 'T_ANGLE_BRACKET_CLOSE' => '>', 48 | 'T_PARENTHESIS_OPEN' => '\\(', 49 | 'T_PARENTHESIS_CLOSE' => '\\)', 50 | 'T_BRACE_OPEN' => '\\{', 51 | 'T_BRACE_CLOSE' => '\\}', 52 | 'T_ATTR_OPEN' => '#\\[', 53 | 'T_SQUARE_BRACKET_OPEN' => '\\[', 54 | 'T_SQUARE_BRACKET_CLOSE' => '\\]', 55 | 'T_COMMA' => ',', 56 | 'T_ELLIPSIS' => '\\.\\.\\.', 57 | 'T_SEMICOLON' => ';', 58 | 'T_DOUBLE_COLON' => '::', 59 | 'T_COLON' => ':', 60 | 'T_ASSIGN' => '=', 61 | 'T_NS_DELIMITER' => '\\\\', 62 | 'T_QMARK' => '\\?', 63 | 'T_NOT' => '\\!', 64 | 'T_OR' => '\\|', 65 | 'T_AMP' => '&', 66 | 'T_ASTERISK' => '\\*', 67 | 'T_COMMENT' => '(//|#).+?$', 68 | 'T_DOC_COMMENT' => '/\\*.*?\\*/', 69 | 'T_WHITESPACE' => '(\\xfe\\xff|\\x20|\\x09|\\x0a|\\x0d)+', 70 | ], 71 | ], 72 | 'skip' => [ 73 | 'T_COMMENT', 74 | 'T_DOC_COMMENT', 75 | 'T_WHITESPACE', 76 | ], 77 | 'transitions' => [], 78 | 'grammar' => [ 79 | new \Phplrt\Parser\Grammar\Concatenation([6, 3, 7]), 80 | new \Phplrt\Parser\Grammar\Concatenation([3, 10]), 81 | new \Phplrt\Parser\Grammar\Alternation([0, 1]), 82 | new \Phplrt\Parser\Grammar\Alternation([11, 12, 13, 14, 15]), 83 | new \Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), 84 | new \Phplrt\Parser\Grammar\Concatenation([4, 3]), 85 | new \Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), 86 | new \Phplrt\Parser\Grammar\Repetition(5, 0, INF), 87 | new \Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), 88 | new \Phplrt\Parser\Grammar\Concatenation([8, 3]), 89 | new \Phplrt\Parser\Grammar\Repetition(9, 0, INF), 90 | new \Phplrt\Parser\Grammar\Lexeme('T_NAME', true), 91 | new \Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true), 92 | new \Phplrt\Parser\Grammar\Lexeme('T_EQ', true), 93 | new \Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true), 94 | new \Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true), 95 | new \Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true), 96 | new \Phplrt\Parser\Grammar\Alternation([21, 22, 23, 24, 25]), 97 | new \Phplrt\Parser\Grammar\Concatenation([2, 39]), 98 | new \Phplrt\Parser\Grammar\Concatenation([2, 43, 44]), 99 | new \Phplrt\Parser\Grammar\Alternation([17, 18, 19]), 100 | new \Phplrt\Parser\Grammar\Alternation([30, 31]), 101 | new \Phplrt\Parser\Grammar\Alternation([32, 33, 34]), 102 | new \Phplrt\Parser\Grammar\Alternation([35, 36, 37, 38]), 103 | new \Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true), 104 | new \Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true), 105 | new \Phplrt\Parser\Grammar\Lexeme('T_VARIABLE', true), 106 | new \Phplrt\Parser\Grammar\Lexeme('T_THIS', true), 107 | new \Phplrt\Parser\Grammar\Alternation([26, 27]), 108 | new \Phplrt\Parser\Grammar\Lexeme('T_THIS', true), 109 | new \Phplrt\Parser\Grammar\Lexeme('T_DQ_STRING_LITERAL', true), 110 | new \Phplrt\Parser\Grammar\Lexeme('T_SQ_STRING_LITERAL', true), 111 | new \Phplrt\Parser\Grammar\Lexeme('T_PFX_FLOAT_LITERAL', true), 112 | new \Phplrt\Parser\Grammar\Lexeme('T_SFX_FLOAT_LITERAL', true), 113 | new \Phplrt\Parser\Grammar\Lexeme('T_EXP_LITERAL', true), 114 | new \Phplrt\Parser\Grammar\Lexeme('T_BIN_INT_LITERAL', true), 115 | new \Phplrt\Parser\Grammar\Lexeme('T_OCT_INT_LITERAL', true), 116 | new \Phplrt\Parser\Grammar\Lexeme('T_HEX_INT_LITERAL', true), 117 | new \Phplrt\Parser\Grammar\Lexeme('T_DEC_INT_LITERAL', true), 118 | new \Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', false), 119 | new \Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true), 120 | new \Phplrt\Parser\Grammar\Concatenation([3, 40]), 121 | new \Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true), 122 | new \Phplrt\Parser\Grammar\Lexeme('T_DOUBLE_COLON', false), 123 | new \Phplrt\Parser\Grammar\Alternation([41, 3, 42]), 124 | new \Phplrt\Parser\Grammar\Concatenation([57, 58]), 125 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 126 | new \Phplrt\Parser\Grammar\Concatenation([46, 45]), 127 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 128 | new \Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', false), 129 | new \Phplrt\Parser\Grammar\Repetition(47, 0, INF), 130 | new \Phplrt\Parser\Grammar\Optional(48), 131 | new \Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', false), 132 | new \Phplrt\Parser\Grammar\Concatenation([49, 45, 50, 51, 52]), 133 | new \Phplrt\Parser\Grammar\Repetition(155, 1, INF), 134 | new \Phplrt\Parser\Grammar\Concatenation([16, 59]), 135 | new \Phplrt\Parser\Grammar\Concatenation([59]), 136 | new \Phplrt\Parser\Grammar\Optional(54), 137 | new \Phplrt\Parser\Grammar\Alternation([55, 56]), 138 | new \Phplrt\Parser\Grammar\Concatenation([175]), 139 | new \Phplrt\Parser\Grammar\Concatenation([67, 71, 72]), 140 | new \Phplrt\Parser\Grammar\Concatenation([107, 59]), 141 | new \Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), 142 | new \Phplrt\Parser\Grammar\Optional(60), 143 | new \Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), 144 | new \Phplrt\Parser\Grammar\Optional(61), 145 | new \Phplrt\Parser\Grammar\Concatenation([2, 62, 63, 64, 65]), 146 | new \Phplrt\Parser\Grammar\Concatenation([74]), 147 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 148 | new \Phplrt\Parser\Grammar\Concatenation([68, 67]), 149 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 150 | new \Phplrt\Parser\Grammar\Repetition(69, 0, INF), 151 | new \Phplrt\Parser\Grammar\Optional(70), 152 | new \Phplrt\Parser\Grammar\Optional(54), 153 | new \Phplrt\Parser\Grammar\Concatenation([78, 79]), 154 | new \Phplrt\Parser\Grammar\Concatenation([93, 92]), 155 | new \Phplrt\Parser\Grammar\Alternation([83, 86, 88, 90, 80]), 156 | new \Phplrt\Parser\Grammar\Lexeme('T_ASSIGN', true), 157 | new \Phplrt\Parser\Grammar\Alternation([75, 76]), 158 | new \Phplrt\Parser\Grammar\Optional(77), 159 | new \Phplrt\Parser\Grammar\Concatenation([28]), 160 | new \Phplrt\Parser\Grammar\Lexeme('T_AMP', true), 161 | new \Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), 162 | new \Phplrt\Parser\Grammar\Concatenation([81, 82, 80]), 163 | new \Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), 164 | new \Phplrt\Parser\Grammar\Lexeme('T_AMP', true), 165 | new \Phplrt\Parser\Grammar\Concatenation([84, 85, 80]), 166 | new \Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), 167 | new \Phplrt\Parser\Grammar\Concatenation([87, 80]), 168 | new \Phplrt\Parser\Grammar\Lexeme('T_AMP', true), 169 | new \Phplrt\Parser\Grammar\Concatenation([89, 80]), 170 | new \Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), 171 | new \Phplrt\Parser\Grammar\Concatenation([94, 95]), 172 | new \Phplrt\Parser\Grammar\Optional(91), 173 | new \Phplrt\Parser\Grammar\Concatenation([96, 106]), 174 | new \Phplrt\Parser\Grammar\Optional(28), 175 | new \Phplrt\Parser\Grammar\Concatenation([59]), 176 | new \Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), 177 | new \Phplrt\Parser\Grammar\Lexeme('T_AMP', true), 178 | new \Phplrt\Parser\Grammar\Optional(97), 179 | new \Phplrt\Parser\Grammar\Concatenation([98, 99]), 180 | new \Phplrt\Parser\Grammar\Lexeme('T_AMP', true), 181 | new \Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), 182 | new \Phplrt\Parser\Grammar\Optional(101), 183 | new \Phplrt\Parser\Grammar\Concatenation([102, 103]), 184 | new \Phplrt\Parser\Grammar\Alternation([100, 104]), 185 | new \Phplrt\Parser\Grammar\Optional(105), 186 | new \Phplrt\Parser\Grammar\Lexeme('T_COLON', false), 187 | new \Phplrt\Parser\Grammar\Concatenation([123, 126]), 188 | new \Phplrt\Parser\Grammar\Concatenation([121, 122]), 189 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 190 | new \Phplrt\Parser\Grammar\Concatenation([110, 109]), 191 | new \Phplrt\Parser\Grammar\Optional(111), 192 | new \Phplrt\Parser\Grammar\Concatenation([108, 112]), 193 | new \Phplrt\Parser\Grammar\Optional(109), 194 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 195 | new \Phplrt\Parser\Grammar\Lexeme('T_BRACE_OPEN', false), 196 | new \Phplrt\Parser\Grammar\Alternation([113, 114]), 197 | new \Phplrt\Parser\Grammar\Optional(115), 198 | new \Phplrt\Parser\Grammar\Lexeme('T_BRACE_CLOSE', false), 199 | new \Phplrt\Parser\Grammar\Concatenation([116, 117, 118, 119]), 200 | new \Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), 201 | new \Phplrt\Parser\Grammar\Optional(53), 202 | new \Phplrt\Parser\Grammar\Concatenation([129, 130]), 203 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 204 | new \Phplrt\Parser\Grammar\Concatenation([124, 123]), 205 | new \Phplrt\Parser\Grammar\Repetition(125, 0, INF), 206 | new \Phplrt\Parser\Grammar\Concatenation([131, 134, 135, 133]), 207 | new \Phplrt\Parser\Grammar\Concatenation([133]), 208 | new \Phplrt\Parser\Grammar\Optional(54), 209 | new \Phplrt\Parser\Grammar\Alternation([127, 128]), 210 | new \Phplrt\Parser\Grammar\Alternation([3, 23, 21]), 211 | new \Phplrt\Parser\Grammar\Lexeme('T_QMARK', true), 212 | new \Phplrt\Parser\Grammar\Concatenation([59]), 213 | new \Phplrt\Parser\Grammar\Optional(132), 214 | new \Phplrt\Parser\Grammar\Lexeme('T_COLON', false), 215 | new \Phplrt\Parser\Grammar\Alternation([53, 120]), 216 | new \Phplrt\Parser\Grammar\Optional(136), 217 | new \Phplrt\Parser\Grammar\Concatenation([2, 137]), 218 | new \Phplrt\Parser\Grammar\Concatenation([176]), 219 | new \Phplrt\Parser\Grammar\Optional(142), 220 | new \Phplrt\Parser\Grammar\Concatenation([139, 140]), 221 | new \Phplrt\Parser\Grammar\Concatenation([145, 146, 147, 59, 148, 59]), 222 | new \Phplrt\Parser\Grammar\Concatenation([28, 142]), 223 | new \Phplrt\Parser\Grammar\Alternation([141, 143]), 224 | new \Phplrt\Parser\Grammar\Alternation([149, 150, 151, 152, 153, 154]), 225 | new \Phplrt\Parser\Grammar\Alternation([59, 28]), 226 | new \Phplrt\Parser\Grammar\Lexeme('T_QMARK', false), 227 | new \Phplrt\Parser\Grammar\Lexeme('T_COLON', false), 228 | new \Phplrt\Parser\Grammar\Lexeme('T_EQ', true), 229 | new \Phplrt\Parser\Grammar\Lexeme('T_NEQ', true), 230 | new \Phplrt\Parser\Grammar\Lexeme('T_GTE', true), 231 | new \Phplrt\Parser\Grammar\Lexeme('T_LTE', true), 232 | new \Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', true), 233 | new \Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', true), 234 | new \Phplrt\Parser\Grammar\Concatenation([158, 156, 159, 160]), 235 | new \Phplrt\Parser\Grammar\Concatenation([161, 164]), 236 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 237 | new \Phplrt\Parser\Grammar\Lexeme('T_ATTR_OPEN', false), 238 | new \Phplrt\Parser\Grammar\Optional(157), 239 | new \Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false), 240 | new \Phplrt\Parser\Grammar\Concatenation([2, 166]), 241 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 242 | new \Phplrt\Parser\Grammar\Concatenation([162, 161]), 243 | new \Phplrt\Parser\Grammar\Repetition(163, 0, INF), 244 | new \Phplrt\Parser\Grammar\Concatenation([171, 167, 172, 173, 174]), 245 | new \Phplrt\Parser\Grammar\Optional(165), 246 | new \Phplrt\Parser\Grammar\Concatenation([59]), 247 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 248 | new \Phplrt\Parser\Grammar\Concatenation([168, 167]), 249 | new \Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), 250 | new \Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), 251 | new \Phplrt\Parser\Grammar\Repetition(169, 0, INF), 252 | new \Phplrt\Parser\Grammar\Optional(170), 253 | new \Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), 254 | new \Phplrt\Parser\Grammar\Concatenation([144]), 255 | new \Phplrt\Parser\Grammar\Concatenation([177, 180]), 256 | new \Phplrt\Parser\Grammar\Concatenation([181, 184]), 257 | new \Phplrt\Parser\Grammar\Lexeme('T_OR', false), 258 | new \Phplrt\Parser\Grammar\Concatenation([178, 176]), 259 | new \Phplrt\Parser\Grammar\Optional(179), 260 | new \Phplrt\Parser\Grammar\Concatenation([185]), 261 | new \Phplrt\Parser\Grammar\Lexeme('T_AMP', false), 262 | new \Phplrt\Parser\Grammar\Concatenation([182, 177]), 263 | new \Phplrt\Parser\Grammar\Optional(183), 264 | new \Phplrt\Parser\Grammar\Alternation([188, 186]), 265 | new \Phplrt\Parser\Grammar\Concatenation([189, 191]), 266 | new \Phplrt\Parser\Grammar\Lexeme('T_QMARK', true), 267 | new \Phplrt\Parser\Grammar\Concatenation([187, 186]), 268 | new \Phplrt\Parser\Grammar\Alternation([197, 29, 20, 66, 138]), 269 | new \Phplrt\Parser\Grammar\Concatenation([192, 193, 194]), 270 | new \Phplrt\Parser\Grammar\Repetition(190, 0, INF), 271 | new \Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_OPEN', true), 272 | new \Phplrt\Parser\Grammar\Optional(59), 273 | new \Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false), 274 | new \Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), 275 | new \Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), 276 | new \Phplrt\Parser\Grammar\Concatenation([195, 59, 196]), 277 | ], 278 | 'reducers' => [ 279 | 0 => static function (\Phplrt\Parser\Context $ctx, $children) { 280 | return new Node\FullQualifiedName($children); 281 | }, 282 | 1 => static function (\Phplrt\Parser\Context $ctx, $children) { 283 | return new Node\Name($children); 284 | }, 285 | 3 => static function (\Phplrt\Parser\Context $ctx, $children) { 286 | return new Node\Identifier($children->getValue()); 287 | }, 288 | 16 => static function (\Phplrt\Parser\Context $ctx, $children) { 289 | return new Node\Identifier($children->getValue()); 290 | }, 291 | 17 => function (\Phplrt\Parser\Context $ctx, $children) { 292 | // The "$offset" variable is an auto-generated 293 | $offset = $ctx->lastProcessedToken->getOffset(); 294 | 295 | if ($this->literals === false) { 296 | throw FeatureNotAllowedException::fromFeature('literal values', $offset); 297 | } 298 | return $children; 299 | }, 300 | 18 => static function (\Phplrt\Parser\Context $ctx, $children) { 301 | return new Node\Stmt\ConstMaskNode($children[0]); 302 | }, 303 | 19 => static function (\Phplrt\Parser\Context $ctx, $children) { 304 | // :: "*" 305 | if (\count($children) === 3) { 306 | return new Node\Stmt\ClassConstMaskNode( 307 | $children[0], 308 | $children[1], 309 | ); 310 | } 311 | 312 | // :: 313 | if ($children[1] instanceof Node\Identifier) { 314 | return new Node\Stmt\ClassConstNode( 315 | $children[0], 316 | $children[1], 317 | ); 318 | } 319 | 320 | // :: "*" 321 | return new Node\Stmt\ClassConstMaskNode($children[0]); 322 | }, 323 | 21 => function (\Phplrt\Parser\Context $ctx, $children) { 324 | // The "$token" variable is an auto-generated 325 | $token = $ctx->lastProcessedToken; 326 | 327 | return $this->stringPool[$token] ??= $children; 328 | }, 329 | 22 => static function (\Phplrt\Parser\Context $ctx, $children) { 330 | // The "$token" variable is an auto-generated 331 | $token = $ctx->lastProcessedToken; 332 | 333 | return Node\Literal\FloatLiteralNode::parse($token->getValue()); 334 | }, 335 | 23 => function (\Phplrt\Parser\Context $ctx, $children) { 336 | // The "$token" variable is an auto-generated 337 | $token = $ctx->lastProcessedToken; 338 | 339 | return $this->integerPool[$token] ??= Node\Literal\IntLiteralNode::parse($token->getValue()); 340 | }, 341 | 24 => static function (\Phplrt\Parser\Context $ctx, $children) { 342 | // The "$token" variable is an auto-generated 343 | $token = $ctx->lastProcessedToken; 344 | 345 | return Node\Literal\BoolLiteralNode::parse($token->getValue()); 346 | }, 347 | 25 => static function (\Phplrt\Parser\Context $ctx, $children) { 348 | return new Node\Literal\NullLiteralNode($children->getValue()); 349 | }, 350 | 28 => static function (\Phplrt\Parser\Context $ctx, $children) { 351 | // The "$token" variable is an auto-generated 352 | $token = $ctx->lastProcessedToken; 353 | 354 | return Node\Literal\VariableLiteralNode::parse($token->getValue()); 355 | }, 356 | 29 => static function (\Phplrt\Parser\Context $ctx, $children) { 357 | // The "$token" variable is an auto-generated 358 | $token = $ctx->lastProcessedToken; 359 | 360 | return Node\Literal\VariableLiteralNode::parse($token->getValue()); 361 | }, 362 | 30 => static function (\Phplrt\Parser\Context $ctx, $children) { 363 | // The "$token" variable is an auto-generated 364 | $token = $ctx->lastProcessedToken; 365 | 366 | return Node\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue()); 367 | }, 368 | 31 => static function (\Phplrt\Parser\Context $ctx, $children) { 369 | // The "$token" variable is an auto-generated 370 | $token = $ctx->lastProcessedToken; 371 | 372 | return Node\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue()); 373 | }, 374 | 45 => function (\Phplrt\Parser\Context $ctx, $children) { 375 | // The "$offset" variable is an auto-generated 376 | $offset = $ctx->lastProcessedToken->getOffset(); 377 | 378 | $hint = $attributes = null; 379 | 380 | if (\reset($children) instanceof Node\Stmt\Attribute\AttributeGroupsListNode) { 381 | if ($this->attributes === false) { 382 | throw FeatureNotAllowedException::fromFeature('template argument attributes', $offset); 383 | } 384 | 385 | $attributes = \array_shift($children); 386 | } 387 | 388 | $type = \array_pop($children); 389 | 390 | if (\reset($children) !== false) { 391 | if ($this->hints === false) { 392 | throw FeatureNotAllowedException::fromFeature('template argument hints', $offset); 393 | } 394 | 395 | $hint = \reset($children); 396 | } 397 | 398 | return new Node\Stmt\Template\TemplateArgumentNode( 399 | $type, 400 | $hint, 401 | $attributes, 402 | ); 403 | }, 404 | 53 => function (\Phplrt\Parser\Context $ctx, $children) { 405 | // The "$offset" variable is an auto-generated 406 | $offset = $ctx->lastProcessedToken->getOffset(); 407 | 408 | if ($this->generics === false) { 409 | throw FeatureNotAllowedException::fromFeature('template arguments', $offset); 410 | } 411 | 412 | return new Node\Stmt\Template\TemplateArgumentsListNode($children); 413 | }, 414 | 54 => static function (\Phplrt\Parser\Context $ctx, $children) { 415 | return new Node\Stmt\Attribute\AttributeGroupsListNode($children); 416 | }, 417 | 60 => static function (\Phplrt\Parser\Context $ctx, $children) { 418 | return new Node\Stmt\Callable\CallableParametersListNode($children); 419 | }, 420 | 66 => function (\Phplrt\Parser\Context $ctx, $children) { 421 | // The "$offset" variable is an auto-generated 422 | $offset = $ctx->lastProcessedToken->getOffset(); 423 | 424 | $name = \array_shift($children); 425 | 426 | if ($this->callables === false) { 427 | throw FeatureNotAllowedException::fromFeature('callable types', $offset); 428 | } 429 | 430 | $parameters = isset($children[0]) && $children[0] instanceof Node\Stmt\Callable\ParametersListNode 431 | ? \array_shift($children) 432 | : new Node\Stmt\Callable\CallableParametersListNode(); 433 | 434 | return new Node\Stmt\CallableTypeNode( 435 | name: $name, 436 | parameters: $parameters, 437 | type: $children[0] ?? null, 438 | ); 439 | }, 440 | 67 => function (\Phplrt\Parser\Context $ctx, $children) { 441 | // The "$offset" variable is an auto-generated 442 | $offset = $ctx->lastProcessedToken->getOffset(); 443 | 444 | $result = \end($children); 445 | 446 | if ($children[0] instanceof Node\Stmt\Attribute\AttributeGroupsListNode) { 447 | if ($this->attributes === false) { 448 | throw FeatureNotAllowedException::fromFeature('callable parameter attributes', $offset); 449 | } 450 | 451 | $result->attributes = $children[0]; 452 | } 453 | 454 | return $result; 455 | }, 456 | 74 => static function (\Phplrt\Parser\Context $ctx, $children) { 457 | // The "$offset" variable is an auto-generated 458 | $offset = $ctx->lastProcessedToken->getOffset(); 459 | 460 | if (\count($children) === 1) { 461 | return $children[0]; 462 | } 463 | 464 | if ($children[0]->variadic) { 465 | throw SemanticException::fromVariadicWithDefault($offset); 466 | } 467 | 468 | $children[0]->optional = true; 469 | return $children[0]; 470 | }, 471 | 75 => static function (\Phplrt\Parser\Context $ctx, $children) { 472 | // The "$offset" variable is an auto-generated 473 | $offset = $ctx->lastProcessedToken->getOffset(); 474 | 475 | if (\count($children) === 1) { 476 | return $children[0]; 477 | } 478 | 479 | if ($children[1]->variadic) { 480 | throw SemanticException::fromVariadicRedefinition($offset); 481 | } 482 | 483 | $children[1]->variadic = true; 484 | return $children[1]; 485 | }, 486 | 76 => static function (\Phplrt\Parser\Context $ctx, $children) { 487 | // The "$offset" variable is an auto-generated 488 | $offset = $ctx->lastProcessedToken->getOffset(); 489 | 490 | if (!\is_array($children)) { 491 | return $children; 492 | } 493 | 494 | $result = \end($children); 495 | 496 | foreach ($children as $modifier) { 497 | if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { 498 | switch ($modifier->getName()) { 499 | case 'T_AMP': 500 | $result->output = true; 501 | break; 502 | case 'T_ELLIPSIS': 503 | if ($result->variadic) { 504 | throw SemanticException::fromVariadicRedefinition($offset); 505 | } 506 | $result->variadic = true; 507 | break; 508 | } 509 | } 510 | } 511 | 512 | return $result; 513 | }, 514 | 80 => static function (\Phplrt\Parser\Context $ctx, $children) { 515 | return new Node\Stmt\Callable\CallableParameterNode(null, $children[0]); 516 | }, 517 | 92 => static function (\Phplrt\Parser\Context $ctx, $children) { 518 | if (\count($children) === 1) { 519 | return $children[0]; 520 | } 521 | 522 | $children[0]->name = $children[1]; 523 | return $children[0]; 524 | }, 525 | 94 => static function (\Phplrt\Parser\Context $ctx, $children) { 526 | // The "$offset" variable is an auto-generated 527 | $offset = $ctx->lastProcessedToken->getOffset(); 528 | 529 | $result = \reset($children); 530 | 531 | foreach ($children as $modifier) { 532 | if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { 533 | switch ($modifier->getName()) { 534 | case 'T_AMP': 535 | $result->output = true; 536 | break; 537 | case 'T_ELLIPSIS': 538 | if ($result->variadic) { 539 | throw SemanticException::fromVariadicRedefinition($offset); 540 | } 541 | $result->variadic = true; 542 | break; 543 | } 544 | } 545 | } 546 | 547 | return $result; 548 | }, 549 | 96 => static function (\Phplrt\Parser\Context $ctx, $children) { 550 | return new Node\Stmt\Callable\CallableParameterNode($children[0]); 551 | }, 552 | 108 => static function (\Phplrt\Parser\Context $ctx, $children) { 553 | // The "$offset" variable is an auto-generated 554 | $offset = $ctx->lastProcessedToken->getOffset(); 555 | 556 | $explicit = []; 557 | $implicit = false; 558 | 559 | foreach ($children as $field) { 560 | if ($field instanceof Node\Stmt\Shape\ExplicitFieldNode) { 561 | $key = $field->getKey(); 562 | 563 | if (\in_array($key, $explicit, true)) { 564 | throw SemanticException::fromShapeFieldDuplication($key, $field->offset); 565 | } 566 | 567 | $explicit[] = $key; 568 | } else { 569 | $implicit = true; 570 | } 571 | } 572 | 573 | if ($explicit !== [] && $implicit) { 574 | throw SemanticException::fromShapeMixedKeys($offset); 575 | } 576 | 577 | return new Node\Stmt\Shape\FieldsListNode($children); 578 | }, 579 | 120 => function (\Phplrt\Parser\Context $ctx, $children) { 580 | // The "$offset" variable is an auto-generated 581 | $offset = $ctx->lastProcessedToken->getOffset(); 582 | 583 | if ($children === []) { 584 | return new Node\Stmt\Shape\FieldsListNode(); 585 | } 586 | 587 | if ($this->shapes === false) { 588 | throw FeatureNotAllowedException::fromFeature('shape fields', $offset); 589 | } 590 | 591 | $parameters = null; 592 | 593 | if (\end($children) instanceof Node\Stmt\Template\ArgumentsListNode) { 594 | $parameters = \array_pop($children); 595 | } 596 | 597 | $fields = \reset($children) instanceof Node\Stmt\Shape\FieldsListNode 598 | ? \array_shift($children) 599 | : new Node\Stmt\Shape\FieldsListNode(); 600 | 601 | if ($children !== []) { 602 | $fields->sealed = false; 603 | } 604 | 605 | return \array_filter([$parameters, $fields]); 606 | }, 607 | 123 => function (\Phplrt\Parser\Context $ctx, $children) { 608 | // The "$offset" variable is an auto-generated 609 | $offset = $ctx->lastProcessedToken->getOffset(); 610 | 611 | $result = \end($children); 612 | 613 | if ($children[0] instanceof Node\Stmt\Attribute\AttributeGroupsListNode) { 614 | if ($this->attributes === false) { 615 | throw FeatureNotAllowedException::fromFeature('shape field attributes', $offset); 616 | } 617 | 618 | $result->attributes = $children[0]; 619 | } 620 | 621 | return $result; 622 | }, 623 | 127 => static function (\Phplrt\Parser\Context $ctx, $children) { 624 | $name = $children[0]; 625 | $value = \array_pop($children); 626 | 627 | // In case of "nullable" suffix defined 628 | $optional = \count($children) === 2; 629 | 630 | return match (true) { 631 | $name instanceof Node\Literal\IntLiteralNode 632 | => new Node\Stmt\Shape\NumericFieldNode($name, $value, $optional), 633 | $name instanceof Node\Literal\StringLiteralNode 634 | => new Node\Stmt\Shape\StringNamedFieldNode($name, $value, $optional), 635 | default => new Node\Stmt\Shape\NamedFieldNode($name, $value, $optional), 636 | }; 637 | }, 638 | 128 => static function (\Phplrt\Parser\Context $ctx, $children) { 639 | return new Node\Stmt\Shape\ImplicitFieldNode($children[0]); 640 | }, 641 | 138 => static function (\Phplrt\Parser\Context $ctx, $children) { 642 | $fields = $parameters = null; 643 | 644 | // Shape fields 645 | if (\end($children) instanceof Node\Stmt\Shape\FieldsListNode) { 646 | $fields = \array_pop($children); 647 | } 648 | 649 | // Template parameters 650 | if (\end($children) instanceof Node\Stmt\Template\ArgumentsListNode) { 651 | $parameters = \array_pop($children); 652 | } 653 | 654 | return new Node\Stmt\NamedTypeNode( 655 | $children[0], 656 | $parameters, 657 | $fields, 658 | ); 659 | }, 660 | 144 => function (\Phplrt\Parser\Context $ctx, $children) { 661 | // The "$offset" variable is an auto-generated 662 | $offset = $ctx->lastProcessedToken->getOffset(); 663 | 664 | $count = \count($children); 665 | 666 | if ($count === 1) { 667 | return $children[0]; 668 | } 669 | 670 | if ($this->conditional === false) { 671 | throw FeatureNotAllowedException::fromFeature('conditional expressions', $offset); 672 | } 673 | 674 | $condition = match ($children[1]->getName()) { 675 | 'T_EQ' => new Node\Stmt\Condition\EqualConditionNode( 676 | $children[0], 677 | $children[2], 678 | ), 679 | 'T_NEQ' => new Node\Stmt\Condition\NotEqualConditionNode( 680 | $children[0], 681 | $children[2], 682 | ), 683 | 'T_GTE' => new Node\Stmt\Condition\GreaterOrEqualThanConditionNode( 684 | $children[0], 685 | $children[2], 686 | ), 687 | 'T_ANGLE_BRACKET_CLOSE' => new Node\Stmt\Condition\GreaterThanConditionNode( 688 | $children[0], 689 | $children[2], 690 | ), 691 | 'T_LTE' => new Node\Stmt\Condition\LessOrEqualThanConditionNode( 692 | $children[0], 693 | $children[2], 694 | ), 695 | 'T_ANGLE_BRACKET_OPEN' => new Node\Stmt\Condition\LessThanConditionNode( 696 | $children[0], 697 | $children[2], 698 | ), 699 | default => throw SemanticException::fromInvalidConditionalOperator( 700 | $children[1]->getValue(), 701 | $offset, 702 | ), 703 | }; 704 | 705 | return new Node\Stmt\TernaryConditionNode( 706 | $condition, 707 | $children[3], 708 | $children[4], 709 | ); 710 | }, 711 | 155 => static function (\Phplrt\Parser\Context $ctx, $children) { 712 | return new Node\Stmt\Attribute\AttributeGroupNode($children); 713 | }, 714 | 161 => static function (\Phplrt\Parser\Context $ctx, $children) { 715 | return new Node\Stmt\Attribute\AttributeNode( 716 | $children[0], 717 | ); 718 | }, 719 | 165 => static function (\Phplrt\Parser\Context $ctx, $children) { 720 | return new Node\Stmt\Attribute\AttributeArgumentsListNode($children); 721 | }, 722 | 167 => static function (\Phplrt\Parser\Context $ctx, $children) { 723 | return new Node\Stmt\Attribute\AttributeArgumentNode($children[0]); 724 | }, 725 | 176 => function (\Phplrt\Parser\Context $ctx, $children) { 726 | // The "$offset" variable is an auto-generated 727 | $offset = $ctx->lastProcessedToken->getOffset(); 728 | 729 | if (\count($children) === 2) { 730 | if ($this->union === false) { 731 | throw FeatureNotAllowedException::fromFeature('union types', $offset); 732 | } 733 | 734 | return new Node\Stmt\UnionTypeNode($children[0], $children[1]); 735 | } 736 | 737 | return $children; 738 | }, 739 | 177 => function (\Phplrt\Parser\Context $ctx, $children) { 740 | // The "$offset" variable is an auto-generated 741 | $offset = $ctx->lastProcessedToken->getOffset(); 742 | 743 | if (\count($children) === 2) { 744 | if ($this->intersection === false) { 745 | throw FeatureNotAllowedException::fromFeature('intersection types', $offset); 746 | } 747 | 748 | return new Node\Stmt\IntersectionTypeNode($children[0], $children[1]); 749 | } 750 | 751 | return $children; 752 | }, 753 | 185 => static function (\Phplrt\Parser\Context $ctx, $children) { 754 | if (\is_array($children)) { 755 | return new Node\Stmt\NullableTypeNode($children[1]); 756 | } 757 | 758 | return $children; 759 | }, 760 | 186 => function (\Phplrt\Parser\Context $ctx, $children) { 761 | // The "$offset" variable is an auto-generated 762 | $offset = $ctx->lastProcessedToken->getOffset(); 763 | 764 | $statement = \array_shift($children); 765 | 766 | foreach ($children as $child) { 767 | switch (true) { 768 | // In case of list type 769 | case $child === true: 770 | if ($this->list === false) { 771 | throw FeatureNotAllowedException::fromFeature('square bracket list types', $offset); 772 | } 773 | 774 | $statement = new Node\Stmt\TypesListNode($statement); 775 | break; 776 | // In case of offset access type 777 | case $child instanceof Node\Stmt\TypeStatement: 778 | if ($this->offsets === false) { 779 | throw FeatureNotAllowedException::fromFeature('type offsets', $offset); 780 | } 781 | 782 | $statement = new Node\Stmt\TypeOffsetAccessNode($statement, $child); 783 | break; 784 | default: 785 | throw new SemanticException($offset, \sprintf( 786 | 'Internal error, unexpected square bracket sub-node %s', 787 | \get_debug_type($child), 788 | )); 789 | } 790 | } 791 | 792 | return $statement; 793 | }, 794 | 190 => static function (\Phplrt\Parser\Context $ctx, $children) { 795 | return $children[1] ?? true; 796 | }, 797 | ], 798 | ]; -------------------------------------------------------------------------------- /resources/grammar.pp2: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * ----------------------------------------------------------------------------- 4 | * Language Syntax Summary 5 | * ----------------------------------------------------------------------------- 6 | */ 7 | 8 | %include grammar/lexemes // Lexeme/Token Definitions 9 | %include grammar/common // Common Utils 10 | %include grammar/literals // Literal Definitions 11 | %include grammar/template-arguments // T 12 | %include grammar/callable // callable(mixed): void 13 | %include grammar/shape-fields // array { key: int, ... } 14 | %include grammar/named-type // Map, non-empty-string> 15 | %include grammar/ternary // T is A ? B : C 16 | %include grammar/attribute // #[attr] 17 | 18 | %pragma root Type 19 | 20 | /** 21 | * ----------------------------------------------------------------------------- 22 | * Type Statement 23 | * ----------------------------------------------------------------------------- 24 | * 25 | * Constant references to external definitions or 26 | * describe the type in some way. 27 | * 28 | */ 29 | 30 | Type 31 | : Expression() 32 | ; 33 | 34 | /** 35 | * ----------------------------------------------------------------------------- 36 | * Ternary Expression 37 | * ----------------------------------------------------------------------------- 38 | * 39 | * Ternary conditional expressions, like: 40 | * - T is A ? B : C - for equality type conditions. 41 | * - T is not A ? B ? C - for non-equality type conditions. 42 | * 43 | */ 44 | 45 | Expression 46 | : TernaryExpressionOrLogicalType() 47 | ; 48 | 49 | 50 | /** 51 | * ----------------------------------------------------------------------------- 52 | * Logical Statements 53 | * ----------------------------------------------------------------------------- 54 | * 55 | * Logical statements denote union or intersection types, like: 56 | * - A | B | C - for union type references. 57 | * - A & B & C - for intersection type references. 58 | * 59 | */ 60 | 61 | LogicalType 62 | : UnionType() 63 | ; 64 | 65 | UnionType -> { 66 | if (\count($children) === 2) { 67 | if ($this->union === false) { 68 | throw FeatureNotAllowedException::fromFeature('union types', $offset); 69 | } 70 | 71 | return new Node\Stmt\UnionTypeNode($children[0], $children[1]); 72 | } 73 | 74 | return $children; 75 | } 76 | : IntersectionType() (::T_OR:: UnionType())? 77 | ; 78 | 79 | IntersectionType -> { 80 | if (\count($children) === 2) { 81 | if ($this->intersection === false) { 82 | throw FeatureNotAllowedException::fromFeature('intersection types', $offset); 83 | } 84 | 85 | return new Node\Stmt\IntersectionTypeNode($children[0], $children[1]); 86 | } 87 | 88 | return $children; 89 | } 90 | : UnaryType() (::T_AMP:: IntersectionType())? 91 | ; 92 | 93 | /** 94 | * ----------------------------------------------------------------------------- 95 | * Unary Statements 96 | * ----------------------------------------------------------------------------- 97 | * 98 | * Unary statements denote terminal types, like: 99 | * - A - for type reference. 100 | * - ?A - for nullable type reference. 101 | * - 'some' - for string literal reference. 102 | * - etc... 103 | * 104 | */ 105 | 106 | UnaryType 107 | : PrefixedNullableType() 108 | ; 109 | 110 | // stmt = ?Type 111 | PrefixedNullableType -> { 112 | if (\is_array($children)) { 113 | return new Node\Stmt\NullableTypeNode($children[1]); 114 | } 115 | 116 | return $children; 117 | } 118 | : TypesList() 119 | | TypesList() 120 | ; 121 | 122 | TypesList -> { 123 | $statement = \array_shift($children); 124 | 125 | foreach ($children as $child) { 126 | switch (true) { 127 | // In case of list type 128 | case $child === true: 129 | if ($this->list === false) { 130 | throw FeatureNotAllowedException::fromFeature('square bracket list types', $offset); 131 | } 132 | 133 | $statement = new Node\Stmt\TypesListNode($statement); 134 | break; 135 | // In case of offset access type 136 | case $child instanceof Node\Stmt\TypeStatement: 137 | if ($this->offsets === false) { 138 | throw FeatureNotAllowedException::fromFeature('type offsets', $offset); 139 | } 140 | 141 | $statement = new Node\Stmt\TypeOffsetAccessNode($statement, $child); 142 | break; 143 | default: 144 | throw new SemanticException($offset, \sprintf( 145 | 'Internal error, unexpected square bracket sub-node %s', 146 | \get_debug_type($child), 147 | )); 148 | } 149 | } 150 | 151 | return $statement; 152 | } 153 | : PrimaryType() 154 | TypeListOrOffsetSuffix()* 155 | ; 156 | 157 | // - Returns `true` in case of a list type (`T[]`). 158 | // - Returns `NamedTypeNode` in case of an offset access type (`T[Y]`). 159 | TypeListOrOffsetSuffix -> { 160 | return $children[1] ?? true; 161 | } 162 | : 163 | Type()? 164 | ::T_SQUARE_BRACKET_CLOSE:: 165 | ; 166 | 167 | PrimaryType 168 | : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: 169 | | ThisLiteral() 170 | | Literal() 171 | | CallableType() 172 | | NamedType() 173 | ; 174 | -------------------------------------------------------------------------------- /resources/grammar/attribute.pp2: -------------------------------------------------------------------------------- 1 | 2 | AttributeGroupsList -> { 3 | return new Node\Stmt\Attribute\AttributeGroupsListNode($children); 4 | } 5 | : AttributeGroup()+ 6 | ; 7 | 8 | AttributeGroup -> { 9 | return new Node\Stmt\Attribute\AttributeGroupNode($children); 10 | } 11 | : ::T_ATTR_OPEN:: 12 | AttributesList() ::T_COMMA::? 13 | ::T_SQUARE_BRACKET_CLOSE:: 14 | ; 15 | 16 | AttributesList 17 | : Attribute() ( 18 | ::T_COMMA:: Attribute() 19 | )* 20 | ; 21 | 22 | Attribute -> { 23 | return new Node\Stmt\Attribute\AttributeNode( 24 | $children[0], 25 | ); 26 | } 27 | : Name() AttributeArguments()? 28 | ; 29 | 30 | AttributeArguments -> { 31 | return new Node\Stmt\Attribute\AttributeArgumentsListNode($children); 32 | } 33 | : ::T_PARENTHESIS_OPEN:: 34 | AttributeArgument() ( 35 | ::T_COMMA:: AttributeArgument() 36 | )* ::T_COMMA::? 37 | ::T_PARENTHESIS_CLOSE:: 38 | ; 39 | 40 | AttributeArgument -> { 41 | return new Node\Stmt\Attribute\AttributeArgumentNode($children[0]); 42 | } 43 | : Type() 44 | ; 45 | -------------------------------------------------------------------------------- /resources/grammar/callable.pp2: -------------------------------------------------------------------------------- 1 | 2 | CallableType -> { 3 | $name = \array_shift($children); 4 | 5 | if ($this->callables === false) { 6 | throw FeatureNotAllowedException::fromFeature('callable types', $offset); 7 | } 8 | 9 | $parameters = isset($children[0]) && $children[0] instanceof Node\Stmt\Callable\ParametersListNode 10 | ? \array_shift($children) 11 | : new Node\Stmt\Callable\CallableParametersListNode(); 12 | 13 | return new Node\Stmt\CallableTypeNode( 14 | name: $name, 15 | parameters: $parameters, 16 | type: $children[0] ?? null, 17 | ); 18 | } 19 | : Name() 20 | ::T_PARENTHESIS_OPEN:: 21 | CallableParameters()? 22 | ::T_PARENTHESIS_CLOSE:: 23 | CallableReturnType()? 24 | ; 25 | 26 | CallableParameters -> { 27 | return new Node\Stmt\Callable\CallableParametersListNode($children); 28 | } 29 | : CallableParameter() (::T_COMMA:: CallableParameter())* ::T_COMMA::? 30 | ; 31 | 32 | CallableParameter -> { 33 | $result = \end($children); 34 | 35 | if ($children[0] instanceof Node\Stmt\Attribute\AttributeGroupsListNode) { 36 | if ($this->attributes === false) { 37 | throw FeatureNotAllowedException::fromFeature('callable parameter attributes', $offset); 38 | } 39 | 40 | $result->attributes = $children[0]; 41 | } 42 | 43 | return $result; 44 | } 45 | : AttributeGroupsList()? 46 | CallableParameter() 47 | ; 48 | 49 | CallableParameter 50 | : MaybeDefaultCallableParameter() 51 | ; 52 | 53 | // Expects expression: 54 | // - "" 55 | // - " =" 56 | MaybeDefaultCallableParameter -> { 57 | if (\count($children) === 1) { 58 | return $children[0]; 59 | } 60 | 61 | if ($children[0]->variadic) { 62 | throw SemanticException::fromVariadicWithDefault($offset); 63 | } 64 | 65 | $children[0]->optional = true; 66 | return $children[0]; 67 | } 68 | : ( MaybePrefixedVariadicTypedNamedCallableParameter() 69 | | MaybeModifiersNamedCallableParameter() ) 70 | ? 71 | ; 72 | 73 | // Expects expression: 74 | // - "&..." 75 | // - "...&" 76 | // - "..." 77 | // - "&" 78 | // - "" 79 | MaybeModifiersNamedCallableParameter -> { 80 | if (!\is_array($children)) { 81 | return $children; 82 | } 83 | 84 | $result = \end($children); 85 | 86 | foreach ($children as $modifier) { 87 | if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { 88 | switch ($modifier->getName()) { 89 | case 'T_AMP': 90 | $result->output = true; 91 | break; 92 | case 'T_ELLIPSIS': 93 | if ($result->variadic) { 94 | throw SemanticException::fromVariadicRedefinition($offset); 95 | } 96 | $result->variadic = true; 97 | break; 98 | } 99 | } 100 | } 101 | 102 | return $result; 103 | } 104 | : MaybeNamedCallableParameter() 105 | | MaybeNamedCallableParameter() 106 | | MaybeNamedCallableParameter() 107 | | MaybeNamedCallableParameter() 108 | | MaybeNamedCallableParameter() 109 | ; 110 | 111 | // Expects expression: 112 | // - "$" 113 | MaybeNamedCallableParameter -> { 114 | return new Node\Stmt\Callable\CallableParameterNode(null, $children[0]); 115 | } 116 | : VariableLiteral() 117 | ; 118 | 119 | MaybePrefixedVariadicTypedNamedCallableParameter -> { 120 | if (\count($children) === 1) { 121 | return $children[0]; 122 | } 123 | 124 | if ($children[1]->variadic) { 125 | throw SemanticException::fromVariadicRedefinition($offset); 126 | } 127 | 128 | $children[1]->variadic = true; 129 | return $children[1]; 130 | } 131 | : ? MaybeTypedNamedCallableParameter() 132 | ; 133 | 134 | // Expects expression: 135 | // - " $" 136 | // - "" 137 | MaybeTypedNamedCallableParameter -> { 138 | if (\count($children) === 1) { 139 | return $children[0]; 140 | } 141 | 142 | $children[0]->name = $children[1]; 143 | return $children[0]; 144 | } 145 | : MaybeModifiersTypedCallableParameter() 146 | VariableLiteral()? 147 | ; 148 | 149 | // Expects expression: 150 | // - "...&" 151 | // - "&..." 152 | // - "&" 153 | // - "..." 154 | // - "" 155 | MaybeModifiersTypedCallableParameter -> { 156 | $result = \reset($children); 157 | 158 | foreach ($children as $modifier) { 159 | if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { 160 | switch ($modifier->getName()) { 161 | case 'T_AMP': 162 | $result->output = true; 163 | break; 164 | case 'T_ELLIPSIS': 165 | if ($result->variadic) { 166 | throw SemanticException::fromVariadicRedefinition($offset); 167 | } 168 | $result->variadic = true; 169 | break; 170 | } 171 | } 172 | } 173 | 174 | return $result; 175 | } 176 | : TypedCallableParameter() 177 | ( ? 178 | | ? )? 179 | ; 180 | 181 | // Expects expression: 182 | // - "" 183 | TypedCallableParameter -> { 184 | return new Node\Stmt\Callable\CallableParameterNode($children[0]); 185 | } 186 | : Type() 187 | ; 188 | 189 | CallableReturnType 190 | : ::T_COLON:: Type() 191 | ; 192 | -------------------------------------------------------------------------------- /resources/grammar/common.pp2: -------------------------------------------------------------------------------- 1 | 2 | Name 3 | : FullQualifiedName() 4 | | RelativeName() 5 | ; 6 | 7 | FullQualifiedName -> { 8 | return new Node\FullQualifiedName($children); 9 | } 10 | : ::T_NS_DELIMITER:: Identifier() (::T_NS_DELIMITER:: Identifier())* 11 | ; 12 | 13 | RelativeName -> { 14 | return new Node\Name($children); 15 | } 16 | : Identifier() (::T_NS_DELIMITER:: Identifier())* 17 | ; 18 | 19 | Identifier -> { 20 | return new Node\Identifier($children->getValue()); 21 | } 22 | : 23 | | 24 | | 25 | | 26 | | 27 | ; 28 | 29 | IdentifierWithExtraSpace -> { 30 | return new Node\Identifier($children->getValue()); 31 | } 32 | : 33 | ; 34 | -------------------------------------------------------------------------------- /resources/grammar/lexemes.pp2: -------------------------------------------------------------------------------- 1 | 2 | // Literals 3 | 4 | %token T_DQ_STRING_LITERAL "([^"\\]*(?:\\.[^"\\]*)*)" 5 | %token T_SQ_STRING_LITERAL '([^'\\]*(?:\\.[^'\\]*)*)' 6 | %token T_PFX_FLOAT_LITERAL \-?(?i)[0-9]++\.[0-9]*+(?:e-?[0-9]++)? 7 | %token T_SFX_FLOAT_LITERAL \-?(?i)[0-9]*+\.[0-9]++(?:e-?[0-9]++)? 8 | %token T_EXP_LITERAL \-?(?i)[0-9]++e-?[0-9]++ 9 | %token T_BIN_INT_LITERAL \-?(?i)0b[0-1_]++ 10 | %token T_OCT_INT_LITERAL \-?(?i)0o[0-7_]++ 11 | %token T_HEX_INT_LITERAL \-?(?i)0x[0-9a-f_]++ 12 | %token T_DEC_INT_LITERAL \-?(?i)[0-9][0-9_]*+ 13 | %token T_BOOL_LITERAL (?i)(?:true|false)(?![a-zA-Z0-9\-_\x80-\xff]) 14 | %token T_NULL_LITERAL (?i)(?:null)(?![a-zA-Z0-9\-_\x80-\xff]) 15 | 16 | // Identifier 17 | 18 | %token T_NEQ (?i)is\h+not(?![a-zA-Z0-9\-_\x80-\xff]) 19 | %token T_EQ (?i)is(?![a-zA-Z0-9\-_\x80-\xff]) 20 | %token T_THIS \$this\b 21 | %token T_VARIABLE \$[a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]* 22 | %token T_NAME_WITH_SPACE [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*\s+? 23 | %token T_NAME [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]* 24 | 25 | // Special Chars 26 | 27 | %token T_LTE <= 28 | %token T_GTE >= 29 | %token T_ANGLE_BRACKET_OPEN < 30 | %token T_ANGLE_BRACKET_CLOSE > 31 | %token T_PARENTHESIS_OPEN \( 32 | %token T_PARENTHESIS_CLOSE \) 33 | %token T_BRACE_OPEN \{ 34 | %token T_BRACE_CLOSE \} 35 | %token T_ATTR_OPEN #\[ 36 | %token T_SQUARE_BRACKET_OPEN \[ 37 | %token T_SQUARE_BRACKET_CLOSE \] 38 | %token T_COMMA , 39 | %token T_ELLIPSIS \.\.\. 40 | %token T_SEMICOLON ; 41 | %token T_DOUBLE_COLON :: 42 | %token T_COLON : 43 | %token T_ASSIGN = 44 | %token T_NS_DELIMITER \\ 45 | %token T_QMARK \? 46 | %token T_NOT \! 47 | %token T_OR \| 48 | %token T_AMP & 49 | %token T_ASTERISK \* 50 | 51 | // Other 52 | 53 | %skip T_COMMENT (//|#).+?$ 54 | %skip T_DOC_COMMENT /\*.*?\*/ 55 | %skip T_WHITESPACE (\xfe\xff|\x20|\x09|\x0a|\x0d)+ 56 | -------------------------------------------------------------------------------- /resources/grammar/literals.pp2: -------------------------------------------------------------------------------- 1 | 2 | Literal 3 | : RawLiteral() 4 | | ConstMaskLiteral() 5 | | ClassConstLiteral() 6 | ; 7 | 8 | RawLiteral -> { 9 | if ($this->literals === false) { 10 | throw FeatureNotAllowedException::fromFeature('literal values', $offset); 11 | } 12 | return $children; 13 | } 14 | : StringLiteral() 15 | | FloatLiteral() 16 | | IntLiteral() 17 | | BoolLiteral() 18 | | NullLiteral() 19 | ; 20 | 21 | VariableLiteral -> { 22 | return Node\Literal\VariableLiteralNode::parse($token->getValue()); 23 | } 24 | : 25 | | 26 | ; 27 | 28 | ThisLiteral -> { 29 | return Node\Literal\VariableLiteralNode::parse($token->getValue()); 30 | } 31 | : 32 | ; 33 | 34 | StringLiteral -> { return $this->stringPool[$token] ??= $children; } 35 | : DoubleQuotedStringLiteral() 36 | | SingleQuotedStringLiteral() 37 | ; 38 | 39 | DoubleQuotedStringLiteral -> { 40 | return Node\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue()); 41 | } 42 | : 43 | ; 44 | 45 | SingleQuotedStringLiteral -> { 46 | return Node\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue()); 47 | } 48 | : 49 | ; 50 | 51 | FloatLiteral -> { 52 | return Node\Literal\FloatLiteralNode::parse($token->getValue()); 53 | } 54 | : 55 | | 56 | | 57 | ; 58 | 59 | IntLiteral -> { 60 | return $this->integerPool[$token] ??= Node\Literal\IntLiteralNode::parse($token->getValue()); 61 | } 62 | : 63 | | 64 | | 65 | | 66 | ; 67 | 68 | BoolLiteral -> { 69 | return Node\Literal\BoolLiteralNode::parse($token->getValue()); 70 | } 71 | : 72 | ; 73 | 74 | NullLiteral -> { 75 | return new Node\Literal\NullLiteralNode($children->getValue()); 76 | } 77 | : 78 | ; 79 | 80 | ConstMaskLiteral -> { 81 | return new Node\Stmt\ConstMaskNode($children[0]); 82 | } 83 | : Name() ::T_ASTERISK:: 84 | ; 85 | 86 | ClassConstLiteral -> { 87 | // :: "*" 88 | if (\count($children) === 3) { 89 | return new Node\Stmt\ClassConstMaskNode( 90 | $children[0], 91 | $children[1], 92 | ); 93 | } 94 | 95 | // :: 96 | if ($children[1] instanceof Node\Identifier) { 97 | return new Node\Stmt\ClassConstNode( 98 | $children[0], 99 | $children[1], 100 | ); 101 | } 102 | 103 | // :: "*" 104 | return new Node\Stmt\ClassConstMaskNode($children[0]); 105 | } 106 | : Name() ::T_DOUBLE_COLON:: 107 | ( Identifier() 108 | | Identifier() 109 | | 110 | ) 111 | ; 112 | -------------------------------------------------------------------------------- /resources/grammar/named-type.pp2: -------------------------------------------------------------------------------- 1 | 2 | NamedType -> { 3 | $fields = $parameters = null; 4 | 5 | // Shape fields 6 | if (\end($children) instanceof Node\Stmt\Shape\FieldsListNode) { 7 | $fields = \array_pop($children); 8 | } 9 | 10 | // Template parameters 11 | if (\end($children) instanceof Node\Stmt\Template\ArgumentsListNode) { 12 | $parameters = \array_pop($children); 13 | } 14 | 15 | return new Node\Stmt\NamedTypeNode( 16 | $children[0], 17 | $parameters, 18 | $fields, 19 | ); 20 | } 21 | : Name() (TemplateArguments() | ShapeFields())? 22 | ; 23 | -------------------------------------------------------------------------------- /resources/grammar/shape-fields.pp2: -------------------------------------------------------------------------------- 1 | 2 | ShapeFields -> { 3 | if ($children === []) { 4 | return new Node\Stmt\Shape\FieldsListNode(); 5 | } 6 | 7 | if ($this->shapes === false) { 8 | throw FeatureNotAllowedException::fromFeature('shape fields', $offset); 9 | } 10 | 11 | $parameters = null; 12 | 13 | if (\end($children) instanceof Node\Stmt\Template\ArgumentsListNode) { 14 | $parameters = \array_pop($children); 15 | } 16 | 17 | $fields = \reset($children) instanceof Node\Stmt\Shape\FieldsListNode 18 | ? \array_shift($children) 19 | : new Node\Stmt\Shape\FieldsListNode(); 20 | 21 | if ($children !== []) { 22 | $fields->sealed = false; 23 | } 24 | 25 | return \array_filter([$parameters, $fields]); 26 | } 27 | : ::T_BRACE_OPEN:: 28 | ( 29 | (ShapeFieldsList() (::T_COMMA:: ShapeFieldsUnsealed())?) 30 | | ShapeFieldsUnsealed()? 31 | ) 32 | ::T_COMMA::? 33 | ::T_BRACE_CLOSE:: 34 | ; 35 | 36 | ShapeFieldsUnsealed 37 | : TemplateArguments()? 38 | ; 39 | 40 | ShapeFieldsList -> { 41 | $explicit = []; 42 | $implicit = false; 43 | 44 | foreach ($children as $field) { 45 | if ($field instanceof Node\Stmt\Shape\ExplicitFieldNode) { 46 | $key = $field->getKey(); 47 | 48 | if (\in_array($key, $explicit, true)) { 49 | throw SemanticException::fromShapeFieldDuplication($key, $field->offset); 50 | } 51 | 52 | $explicit[] = $key; 53 | } else { 54 | $implicit = true; 55 | } 56 | } 57 | 58 | if ($explicit !== [] && $implicit) { 59 | throw SemanticException::fromShapeMixedKeys($offset); 60 | } 61 | 62 | return new Node\Stmt\Shape\FieldsListNode($children); 63 | } 64 | : ShapeField() ( 65 | ::T_COMMA:: ShapeField() 66 | )* 67 | ; 68 | 69 | ShapeField -> { 70 | $result = \end($children); 71 | 72 | if ($children[0] instanceof Node\Stmt\Attribute\AttributeGroupsListNode) { 73 | if ($this->attributes === false) { 74 | throw FeatureNotAllowedException::fromFeature('shape field attributes', $offset); 75 | } 76 | 77 | $result->attributes = $children[0]; 78 | } 79 | 80 | return $result; 81 | } 82 | : AttributeGroupsList()? ( 83 | ExplicitField() 84 | | ImplicitField() 85 | ) 86 | ; 87 | 88 | ExplicitField -> { 89 | $name = $children[0]; 90 | $value = \array_pop($children); 91 | 92 | // In case of "nullable" suffix defined 93 | $optional = \count($children) === 2; 94 | 95 | return match (true) { 96 | $name instanceof Node\Literal\IntLiteralNode 97 | => new Node\Stmt\Shape\NumericFieldNode($name, $value, $optional), 98 | $name instanceof Node\Literal\StringLiteralNode 99 | => new Node\Stmt\Shape\StringNamedFieldNode($name, $value, $optional), 100 | default => new Node\Stmt\Shape\NamedFieldNode($name, $value, $optional), 101 | }; 102 | } 103 | : ShapeKey() ()? ::T_COLON:: ShapeValue() 104 | ; 105 | 106 | ImplicitField -> { 107 | return new Node\Stmt\Shape\ImplicitFieldNode($children[0]); 108 | } 109 | : ShapeValue() 110 | ; 111 | 112 | ShapeKey 113 | : Identifier() 114 | | IntLiteral() 115 | | StringLiteral() 116 | ; 117 | 118 | ShapeValue 119 | : Type() 120 | ; 121 | -------------------------------------------------------------------------------- /resources/grammar/template-arguments.pp2: -------------------------------------------------------------------------------- 1 | 2 | TemplateArguments -> { 3 | if ($this->generics === false) { 4 | throw FeatureNotAllowedException::fromFeature('template arguments', $offset); 5 | } 6 | 7 | return new Node\Stmt\Template\TemplateArgumentsListNode($children); 8 | } 9 | : ::T_ANGLE_BRACKET_OPEN:: 10 | TemplateArgument() ( 11 | ::T_COMMA:: TemplateArgument() 12 | )* ::T_COMMA::? 13 | ::T_ANGLE_BRACKET_CLOSE:: 14 | ; 15 | 16 | TemplateArgument -> { 17 | $hint = $attributes = null; 18 | 19 | if (\reset($children) instanceof Node\Stmt\Attribute\AttributeGroupsListNode) { 20 | if ($this->attributes === false) { 21 | throw FeatureNotAllowedException::fromFeature('template argument attributes', $offset); 22 | } 23 | 24 | $attributes = \array_shift($children); 25 | } 26 | 27 | $type = \array_pop($children); 28 | 29 | if (\reset($children) !== false) { 30 | if ($this->hints === false) { 31 | throw FeatureNotAllowedException::fromFeature('template argument hints', $offset); 32 | } 33 | 34 | $hint = \reset($children); 35 | } 36 | 37 | return new Node\Stmt\Template\TemplateArgumentNode( 38 | $type, 39 | $hint, 40 | $attributes, 41 | ); 42 | } 43 | : AttributeGroupsList()? ( 44 | TemplateHintedArgument() 45 | | TemplateSimpleArgument() 46 | ) 47 | ; 48 | 49 | TemplateSimpleArgument 50 | : Type() 51 | ; 52 | 53 | TemplateHintedArgument 54 | : IdentifierWithExtraSpace() Type() 55 | ; 56 | -------------------------------------------------------------------------------- /resources/grammar/ternary.pp2: -------------------------------------------------------------------------------- 1 | 2 | TernaryExpressionOrLogicalType -> { 3 | $count = \count($children); 4 | 5 | if ($count === 1) { 6 | return $children[0]; 7 | } 8 | 9 | if ($this->conditional === false) { 10 | throw FeatureNotAllowedException::fromFeature('conditional expressions', $offset); 11 | } 12 | 13 | $condition = match ($children[1]->getName()) { 14 | 'T_EQ' => new Node\Stmt\Condition\EqualConditionNode( 15 | $children[0], 16 | $children[2], 17 | ), 18 | 'T_NEQ' => new Node\Stmt\Condition\NotEqualConditionNode( 19 | $children[0], 20 | $children[2], 21 | ), 22 | 'T_GTE' => new Node\Stmt\Condition\GreaterOrEqualThanConditionNode( 23 | $children[0], 24 | $children[2], 25 | ), 26 | 'T_ANGLE_BRACKET_CLOSE' => new Node\Stmt\Condition\GreaterThanConditionNode( 27 | $children[0], 28 | $children[2], 29 | ), 30 | 'T_LTE' => new Node\Stmt\Condition\LessOrEqualThanConditionNode( 31 | $children[0], 32 | $children[2], 33 | ), 34 | 'T_ANGLE_BRACKET_OPEN' => new Node\Stmt\Condition\LessThanConditionNode( 35 | $children[0], 36 | $children[2], 37 | ), 38 | default => throw SemanticException::fromInvalidConditionalOperator( 39 | $children[1]->getValue(), 40 | $offset, 41 | ), 42 | }; 43 | 44 | return new Node\Stmt\TernaryConditionNode( 45 | $condition, 46 | $children[3], 47 | $children[4], 48 | ); 49 | } 50 | : LogicalType() OptionalTernaryExpressionSuffix() 51 | | VariableLiteral() TernaryExpressionSuffix() 52 | ; 53 | 54 | OptionalTernaryExpressionSuffix 55 | : TernaryExpressionSuffix()? 56 | ; 57 | 58 | TernaryExpressionSuffix 59 | : TernaryExpressionOperator() (Type() | VariableLiteral()) 60 | ::T_QMARK:: Type() 61 | ::T_COLON:: Type() 62 | ; 63 | 64 | TernaryExpressionOperator 65 | : 66 | | 67 | | 68 | | 69 | | 70 | | 71 | ; 72 | -------------------------------------------------------------------------------- /src/Exception/FeatureNotAllowedException.php: -------------------------------------------------------------------------------- 1 | $offset 12 | * 13 | * @return static 14 | */ 15 | public static function fromFeature(string $name, int $offset = 0): self 16 | { 17 | $message = \sprintf('%s not allowed', \ucfirst($name)); 18 | 19 | return new static($offset, $message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/Formatter.php: -------------------------------------------------------------------------------- 1 | 'end of input', 24 | '"' => 'double quote (")', 25 | default => \sprintf('"%s"', \addcslashes($expr, '"')), 26 | }; 27 | } 28 | 29 | public static function source(string $statement): string 30 | { 31 | $statement = \trim($statement); 32 | 33 | if ($statement === '') { 34 | return ''; 35 | } 36 | 37 | $renderer = new Renderer(); 38 | 39 | return $renderer->value(new Token('', $statement, 0)); 40 | } 41 | 42 | /** 43 | * @param int<0, max> $offset 44 | * 45 | * @return non-empty-string 46 | */ 47 | public static function suffix(string $statement, int $offset): string 48 | { 49 | if (\str_contains($statement, "\n")) { 50 | $pos = Position::fromOffset($statement, $offset); 51 | 52 | return \sprintf('on line %d at column %d', $pos->getLine(), $pos->getColumn()); 53 | } 54 | 55 | return \sprintf('at column %d', $offset + 1); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Exception/ParseException.php: -------------------------------------------------------------------------------- 1 | $offset 34 | * 35 | * @throws SourceExceptionInterface 36 | */ 37 | public static function fromUnexpectedToken(string $token, string $statement, int $offset): static 38 | { 39 | $message = \vsprintf('Syntax error, unexpected %s%s %s', [ 40 | Formatter::token($token), 41 | $token === $statement ? '' : ' in ' . Formatter::source($statement), 42 | Formatter::suffix($statement, $offset), 43 | ]); 44 | 45 | return new static($message, self::ERROR_CODE_UNEXPECTED_TOKEN); 46 | } 47 | 48 | /** 49 | * This error occurs when unable to recognize tokens in source code. 50 | * 51 | * @param int<0, max> $offset 52 | * 53 | * @throws SourceExceptionInterface 54 | */ 55 | public static function fromUnrecognizedToken(string $token, string $statement, int $offset): static 56 | { 57 | $message = \vsprintf('Syntax error, unrecognized %s%s %s', [ 58 | Formatter::token($token), 59 | $token === $statement ? '' : ' in ' . Formatter::source($statement), 60 | Formatter::suffix($statement, $offset), 61 | ]); 62 | 63 | return new static($message, self::ERROR_CODE_UNRECOGNIZED_TOKEN); 64 | } 65 | 66 | /** 67 | * @param int<0, max> $offset 68 | * 69 | * @throws SourceExceptionInterface 70 | */ 71 | public static function fromUnrecognizedSyntaxError(string $statement, int $offset): static 72 | { 73 | $message = \vsprintf('Internal syntax error, in %s %s', [ 74 | Formatter::source($statement), 75 | Formatter::suffix($statement, $offset), 76 | ]); 77 | 78 | return new static($message, self::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR); 79 | } 80 | 81 | /** 82 | * @throws SourceExceptionInterface 83 | */ 84 | public static function fromSemanticError(SemanticException $e, ReadableInterface $source): static 85 | { 86 | $message = \vsprintf('%s in %s %s', [ 87 | \ucfirst($e->getMessage()), 88 | Formatter::source($source->getContents()), 89 | Formatter::suffix($source->getContents(), $e->getOffset()), 90 | ]); 91 | 92 | return new static($message, self::ERROR_CODE_SEMANTIC_ERROR_BASE + $e->getCode()); 93 | } 94 | 95 | public static function fromInternalError(string $statement, \Throwable $e): static 96 | { 97 | $message = 'An internal error occurred while parsing %s'; 98 | $message = \sprintf($message, Formatter::source($statement)); 99 | 100 | return new static($message, self::ERROR_CODE_INTERNAL_ERROR, $e); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Exception/ParserExceptionInterface.php: -------------------------------------------------------------------------------- 1 | $offset 23 | */ 24 | final public function __construct( 25 | public readonly int $offset, 26 | string $message, 27 | int $code = 0, 28 | ?\Throwable $previous = null, 29 | ) { 30 | parent::__construct($message, $code, $previous); 31 | } 32 | 33 | /** 34 | * @return int<0, max> 35 | */ 36 | public function getOffset(): int 37 | { 38 | return $this->offset; 39 | } 40 | 41 | /** 42 | * @param non-empty-string $key 43 | * @param int<0, max> $offset 44 | * 45 | * @return static 46 | */ 47 | public static function fromShapeFieldDuplication(string $key, int $offset = 0): self 48 | { 49 | $message = \sprintf('Duplicate key "%s"', $key); 50 | 51 | return new static($offset, $message, self::ERROR_CODE_SHAPE_KEY_DUPLICATION); 52 | } 53 | 54 | /** 55 | * @param int<0, max> $offset 56 | * 57 | * @return static 58 | */ 59 | public static function fromShapeMixedKeys(int $offset = 0): self 60 | { 61 | $message = 'Cannot mix explicit and implicit shape keys'; 62 | 63 | return new static($offset, $message, self::ERROR_CODE_SHAPE_KEY_MIX); 64 | } 65 | 66 | /** 67 | * @param int<0, max> $offset 68 | * 69 | * @return static 70 | */ 71 | public static function fromVariadicWithDefault(int $offset = 0): self 72 | { 73 | $message = 'Cannot have variadic param with a default'; 74 | 75 | return new static($offset, $message, self::ERROR_CODE_VARIADIC_WITH_DEFAULT); 76 | } 77 | 78 | /** 79 | * @param int<0, max> $offset 80 | * 81 | * @return static 82 | */ 83 | public static function fromVariadicRedefinition(int $offset = 0): self 84 | { 85 | $message = 'Either prefix or postfix variadic syntax should be used, but not both'; 86 | 87 | return new static($offset, $message, self::ERROR_CODE_VARIADIC_ALREADY_VARIADIC); 88 | } 89 | 90 | /** 91 | * @param non-empty-string $operator 92 | * @param int<0, max> $offset 93 | * 94 | * @return static 95 | */ 96 | public static function fromInvalidConditionalOperator(string $operator, int $offset = 0): self 97 | { 98 | $message = \sprintf('Invalid conditional operator "%s"', $operator); 99 | 100 | return new static($offset, $message, self::ERROR_CODE_INVALID_OPERATOR); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/InMemoryCachedParser.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | private array $types = []; 19 | 20 | public function __construct( 21 | private readonly ParserInterface $parser = new Parser(), 22 | private readonly SourceFactoryInterface $sources = new SourceFactory(), 23 | ) {} 24 | 25 | /** 26 | * @throws ParserExceptionInterface 27 | * @throws SourceExceptionInterface 28 | * @throws \Throwable 29 | */ 30 | public function parse(mixed $source): TypeStatement 31 | { 32 | $instance = $this->sources->create($source); 33 | 34 | return $this->types[$instance->getHash()] ??= $this->parser->parse($source); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MutableTraverserInterface.php: -------------------------------------------------------------------------------- 1 | isSimple() 16 | || !$this->isBuiltin() 17 | || !$this->isSpecial(); 18 | } 19 | 20 | /** 21 | * @return non-empty-string 22 | */ 23 | public function toString(): string 24 | { 25 | if ($this->isPrefixedByLeadingBackslash()) { 26 | return '\\' . parent::toString(); 27 | } 28 | 29 | return parent::toString(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Node/Identifier.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private const SPECIAL_CLASS_NAME = [ 13 | 'self', 14 | 'parent', 15 | 'static', 16 | ]; 17 | 18 | /** 19 | * @var list 20 | */ 21 | private const BUILTIN_TYPE_NAME = [ 22 | 'mixed', 23 | 'string', 24 | 'int', 25 | 'float', 26 | 'bool', 27 | 'object', 28 | 'array', 29 | 'void', 30 | 'never', 31 | 'callable', 32 | 'iterable', 33 | 'null', 34 | 'true', 35 | 'false', 36 | ]; 37 | 38 | /** 39 | * @var non-empty-string 40 | */ 41 | public readonly string $value; 42 | 43 | /** 44 | * @param non-empty-string $value 45 | */ 46 | public function __construct(string $value) 47 | { 48 | $value = \trim($value); 49 | 50 | assert($value !== '', new \InvalidArgumentException('Identifier value cannot be empty')); 51 | 52 | $this->value = $value; 53 | } 54 | 55 | /** 56 | * Returns {@see true} if the identifier contains the name of 57 | * a "virtual" type, i.e. invalid in the PHP namespace. 58 | * 59 | * - `SomeClass` - Non-virtual, can be a type in PHP. 60 | * - `false` - Non-virtual, can be a type in PHP. 61 | * - `non-empty-array` - Virtual, cannot be defined in PHP. 62 | * - `empty-string` - Virtual, cannot be defined in PHP. 63 | */ 64 | public function isVirtual(): bool 65 | { 66 | return \str_contains($this->value, '-'); 67 | } 68 | 69 | /** 70 | * Returns {@see true} in case of name contains special class reference. 71 | */ 72 | public function isSpecial(): bool 73 | { 74 | return self::looksLikeSpecial($this->value); 75 | } 76 | 77 | /** 78 | * Returns {@see true} in case of passed "$name" argument looks like 79 | * a special type name or {@see false} instead. 80 | */ 81 | public static function looksLikeSpecial(string $name): bool 82 | { 83 | return \in_array(\strtolower($name), self::SPECIAL_CLASS_NAME, true); 84 | } 85 | 86 | /** 87 | * Returns {@see true} in case of name contains builtin type name. 88 | */ 89 | public function isBuiltin(): bool 90 | { 91 | return self::looksLikeBuiltin($this->value); 92 | } 93 | 94 | /** 95 | * Returns {@see true} in case of passed "$name" argument looks like 96 | * a builtin type name or {@see false} instead. 97 | */ 98 | public static function looksLikeBuiltin(string $value): bool 99 | { 100 | return \in_array(\strtolower($value), self::BUILTIN_TYPE_NAME, true); 101 | } 102 | 103 | /** 104 | * Returns name as string. 105 | * 106 | * @return non-empty-string 107 | */ 108 | public function toString(): string 109 | { 110 | return $this->value; 111 | } 112 | 113 | /** 114 | * Returns lowercased name as string. 115 | * 116 | * @return non-empty-lowercase-string 117 | */ 118 | public function toLowerString(): string 119 | { 120 | return \strtolower($this->toString()); 121 | } 122 | 123 | public function __toString(): string 124 | { 125 | return $this->value; 126 | } 127 | 128 | /** 129 | * @return array{int<0, max>, non-empty-string} 130 | */ 131 | public function __serialize(): array 132 | { 133 | return [$this->offset, $this->value]; 134 | } 135 | 136 | /** 137 | * @param array{0?: int<0, max>, 1?: non-empty-string} $data 138 | * 139 | * @throws \UnexpectedValueException 140 | */ 141 | public function __unserialize(array $data): void 142 | { 143 | $this->offset = $data[0] ?? throw new \UnexpectedValueException( 144 | message: 'Unable to unserialize Identifier offset', 145 | ); 146 | 147 | $this->value = $data[1] ?? throw new \UnexpectedValueException( 148 | message: 'Unable to unserialize Identifier value', 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Node/Literal/BoolLiteralNode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @phpstan-consistent-constructor 11 | */ 12 | class BoolLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface 13 | { 14 | public function __construct( 15 | public readonly bool $value, 16 | ?string $raw = null, 17 | ) { 18 | parent::__construct($raw ?? ($value ? 'true' : 'false')); 19 | } 20 | 21 | public static function parse(string $value): static 22 | { 23 | return new static(\strtolower($value) === 'true', $value); 24 | } 25 | 26 | public function getValue(): bool 27 | { 28 | return $this->value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Node/Literal/FloatLiteralNode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @phpstan-consistent-constructor 11 | */ 12 | class FloatLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface 13 | { 14 | public function __construct( 15 | public readonly float $value, 16 | ?string $raw = null, 17 | ) { 18 | parent::__construct($raw ?? (string) $this->value); 19 | } 20 | 21 | public static function parse(string $value): static 22 | { 23 | if (!\is_numeric($value)) { 24 | return new static(0.0, $value); 25 | } 26 | 27 | return new static((float) $value, $value); 28 | } 29 | 30 | public function getValue(): float 31 | { 32 | return $this->value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Node/Literal/IntLiteralNode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @phpstan-consistent-constructor 11 | */ 12 | class IntLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface 13 | { 14 | /** 15 | * @var numeric-string 16 | */ 17 | public readonly string $decimal; 18 | 19 | /** 20 | * @param numeric-string|null $decimal 21 | */ 22 | public function __construct( 23 | public readonly int $value, 24 | ?string $raw = null, 25 | ?string $decimal = null, 26 | ) { 27 | $this->decimal = $decimal ?? (string) $this->value; 28 | 29 | parent::__construct($raw ?? (string) $this->value); 30 | } 31 | 32 | public static function parse(string $value): static 33 | { 34 | [$negative, $decimal] = self::split($value); 35 | 36 | $inverse = '-' . $decimal; 37 | 38 | if ($negative) { 39 | if ((string) \PHP_INT_MIN === $inverse) { 40 | return new static(\PHP_INT_MIN, $value, $inverse); 41 | } 42 | 43 | return new static((int) $inverse, $value, $inverse); 44 | } 45 | 46 | return new static((int) $decimal, $value, $decimal); 47 | } 48 | 49 | /** 50 | * @return array{bool, numeric-string} 51 | */ 52 | private static function split(string $literal): array 53 | { 54 | $literal = \str_replace('_', '', $literal); 55 | 56 | if ($negative = ($literal[0] === '-')) { 57 | $literal = \substr($literal, 1); 58 | } 59 | 60 | // One of: [ 0123, 0o23, 0x00, 0b01 ] 61 | if ($literal[0] === '0' && isset($literal[1])) { 62 | /** @var array{bool, numeric-string} */ 63 | return [$negative, match ($literal[1]) { 64 | // hexadecimal 65 | 'x', 'X' => \base_convert(\substr($literal, 2), 16, 10), 66 | // binary 67 | 'b', 'B' => \base_convert(\substr($literal, 2), 2, 10), 68 | // octal 69 | 'o', 'O' => \base_convert(\substr($literal, 2), 8, 10), 70 | // octal (legacy) 71 | default => \base_convert($literal, 8, 10), 72 | }]; 73 | } 74 | 75 | /** @var array{bool, numeric-string} */ 76 | return [$negative, $literal]; 77 | } 78 | 79 | /** 80 | * @return numeric-string 81 | */ 82 | public function getValueAsDecimalString(): string 83 | { 84 | return $this->decimal; 85 | } 86 | 87 | public function getValue(): int 88 | { 89 | return $this->value; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Node/Literal/LiteralNode.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class LiteralNode extends TypeStatement implements LiteralNodeInterface 14 | { 15 | public function __construct( 16 | public readonly string $raw, 17 | ) {} 18 | 19 | /** 20 | * Returns parsed literal value. 21 | */ 22 | abstract public function getValue(): mixed; 23 | 24 | /** 25 | * Returns raw literal value string representation. 26 | */ 27 | public function getRawValue(): string 28 | { 29 | return $this->raw; 30 | } 31 | 32 | public function __toString(): string 33 | { 34 | return $this->raw; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Node/Literal/LiteralNodeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class NullLiteralNode extends LiteralNode 11 | { 12 | public function __construct(?string $raw = null) 13 | { 14 | parent::__construct($raw ?? 'null'); 15 | } 16 | 17 | /** 18 | * @return null Note: Standalone `null` literal available since php 8.2. 19 | */ 20 | public function getValue(): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Node/Literal/ParsableLiteralNodeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @phpstan-consistent-constructor 11 | */ 12 | class StringLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface 13 | { 14 | /** 15 | * @var non-empty-string 16 | */ 17 | private const UTF_SEQUENCE_PATTERN = '/(? 26 | */ 27 | private const ESCAPED_CHARS = [ 28 | '\n' => "\n", 29 | '\r' => "\r", 30 | '\t' => "\t", 31 | '\v' => "\v", 32 | '\e' => "\e", 33 | '\f' => "\f", 34 | '\$' => '$', 35 | ]; 36 | 37 | final public function __construct( 38 | public readonly string $value, 39 | ?string $raw = null, 40 | ) { 41 | parent::__construct($raw ?? $this->value); 42 | } 43 | 44 | public static function createFromValue(string $value): static 45 | { 46 | return new static( 47 | value: $value, 48 | raw: \sprintf('"%s"', \addcslashes($value, '"')), 49 | ); 50 | } 51 | 52 | public static function parse(string $value): static 53 | { 54 | assert(\strlen($value) >= 2, new \InvalidArgumentException('Could not parse non-quoted string')); 55 | 56 | if ($value[0] === '"') { 57 | return static::createFromDoubleQuotedString($value); 58 | } 59 | 60 | return static::createFromSingleQuotedString($value); 61 | } 62 | 63 | /** 64 | * @param non-empty-string $value 65 | */ 66 | public static function createFromDoubleQuotedString(string $value): static 67 | { 68 | assert(\strlen($value) >= 2, new \InvalidArgumentException('Could not parse non-quoted string')); 69 | 70 | $body = \substr($value, 1, -1); 71 | 72 | return self::parseEncodedValue( 73 | string: \str_replace('\"', '"', $body), 74 | raw: $value, 75 | ); 76 | } 77 | 78 | /** 79 | * @param non-empty-string $value 80 | */ 81 | public static function createFromSingleQuotedString(string $value): static 82 | { 83 | assert(\strlen($value) >= 2, new \InvalidArgumentException('Could not parse non-quoted string')); 84 | 85 | $body = \substr($value, 1, -1); 86 | 87 | return new static( 88 | value: \str_replace("\'", "'", $body), 89 | raw: $value, 90 | ); 91 | } 92 | 93 | private static function parseEncodedValue(string $string, ?string $raw = null): static 94 | { 95 | $raw ??= $string; 96 | 97 | if (\str_contains($string, '\\')) { 98 | // Replace double backslash to "\0" 99 | $string = \str_replace('\\\\', "\0", $string); 100 | 101 | // Replace escaped chars (like a "\n") to real bytes 102 | $string = self::renderEscapeSequences($string); 103 | 104 | // Replace hex sequences (like a "\xFF") to real bytes 105 | $string = self::renderHexadecimalSequences($string); 106 | 107 | // Replace unicode sequences (like a "\u{FFFF}") to real bytes 108 | $string = self::renderUtfSequences($string); 109 | 110 | // Rollback double backslash escaping 111 | $string = \str_replace("\0", '\\\\', $string); 112 | } 113 | 114 | return new static($string, $raw); 115 | } 116 | 117 | /** 118 | * Method for parsing and decode special escaped character sequences 119 | * like a "\n", "\r" etc... 120 | * 121 | * @link https://www.php.net/manual/en/language.types.string.php 122 | */ 123 | private static function renderEscapeSequences(string $body): string 124 | { 125 | return \str_replace( 126 | \array_keys(self::ESCAPED_CHARS), 127 | \array_values(self::ESCAPED_CHARS), 128 | $body, 129 | ); 130 | } 131 | 132 | /** 133 | * Method for parsing and decode hexadecimal character sequences 134 | * like a "\xFF" type. 135 | * 136 | * @link https://www.php.net/manual/en/language.types.string.php 137 | */ 138 | private static function renderHexadecimalSequences(string $body): string 139 | { 140 | return @\preg_replace_callback(self::HEX_SEQUENCE_PATTERN, static fn(array $matches): string 141 | => \chr((int) \hexdec($matches[1])), $body) ?? $body; 142 | } 143 | 144 | /** 145 | * Method for parsing and decode utf-8 character sequences 146 | * like a "\u{FFFF}" type. 147 | * 148 | * @link https://www.php.net/manual/en/language.types.string.php 149 | */ 150 | private static function renderUtfSequences(string $body): string 151 | { 152 | return @\preg_replace_callback(self::UTF_SEQUENCE_PATTERN, static function (array $matches): string { 153 | $code = (int) \hexdec($matches[1]); 154 | 155 | // @phpstan-ignore-next-line : PHPStan false-positive mb_chr evaluation 156 | if (\function_exists('\\mb_chr') && ($result = \mb_chr($code)) !== false) { 157 | return $result; 158 | } 159 | 160 | if (0x80 > $code %= 0x200000) { 161 | return \chr($code); 162 | } 163 | 164 | if (0x800 > $code) { 165 | return \chr(0xC0 | $code >> 6) 166 | . \chr(0x80 | $code & 0x3F); 167 | } 168 | 169 | if (0x10000 > $code) { 170 | return \chr(0xE0 | $code >> 12) 171 | . \chr(0x80 | $code >> 6 & 0x3F) 172 | . \chr(0x80 | $code & 0x3F); 173 | } 174 | 175 | return \chr(0xF0 | $code >> 18) 176 | . \chr(0x80 | $code >> 12 & 0x3F) 177 | . \chr(0x80 | $code >> 6 & 0x3F) 178 | . \chr(0x80 | $code & 0x3F); 179 | }, $body) ?? $body; 180 | } 181 | 182 | public function getValue(): string 183 | { 184 | return $this->value; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Node/Literal/VariableLiteralNode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @phpstan-consistent-constructor 11 | */ 12 | class VariableLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface 13 | { 14 | /** 15 | * @var non-empty-string 16 | */ 17 | private readonly string $value; 18 | 19 | /** 20 | * @param non-empty-string $value 21 | */ 22 | public function __construct(string $value) 23 | { 24 | assert(\strlen($value) > 1, new \InvalidArgumentException( 25 | 'Variable name length must be greater than 1', 26 | )); 27 | 28 | assert(\str_starts_with($value, '$'), new \InvalidArgumentException( 29 | 'Variable name must start with "$" character', 30 | )); 31 | 32 | // @phpstan-ignore-next-line : Variable name gte than 2 33 | $this->value = \substr($value, 1); 34 | 35 | parent::__construct($value); 36 | } 37 | 38 | public static function parse(string $value): static 39 | { 40 | if (!\str_starts_with($value, '$')) { 41 | $value = '$' . $value; 42 | } 43 | 44 | return new static($value); 45 | } 46 | 47 | public function getValue(): string 48 | { 49 | return $this->value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Node/Name.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Name extends Node implements \IteratorAggregate, \Countable, \Stringable 11 | { 12 | /** 13 | * @var non-empty-string 14 | */ 15 | private const NAMESPACE_DELIMITER = '\\'; 16 | 17 | /** 18 | * @var non-empty-list 19 | */ 20 | public readonly array $parts; 21 | 22 | /** 23 | * @param iterable|non-empty-string|Identifier $name 24 | */ 25 | final public function __construct(iterable|string|Identifier $name) 26 | { 27 | $parts = $this->parseName($name); 28 | 29 | assert($parts !== [], new \InvalidArgumentException('Name parts count can not be empty')); 30 | 31 | $this->parts = $parts; 32 | } 33 | 34 | /** 35 | * @param iterable|non-empty-string|Identifier $name 36 | * 37 | * @return list 38 | */ 39 | private function parseName(iterable|string|Identifier $name): array 40 | { 41 | if (\is_string($name)) { 42 | $name = \array_filter(\explode('\\', $name), static fn(string $chunk): bool => $chunk !== ''); 43 | } 44 | 45 | if (\is_iterable($name)) { 46 | $result = []; 47 | 48 | foreach ($name as $chunk) { 49 | $result[] = $this->parseChunk($chunk); 50 | } 51 | 52 | return $result; 53 | } 54 | 55 | return [$this->parseChunk($name)]; 56 | } 57 | 58 | /** 59 | * @param non-empty-string|Identifier $chunk 60 | */ 61 | private function parseChunk(string|Identifier $chunk): Identifier 62 | { 63 | if (\is_string($chunk)) { 64 | return new Identifier($chunk); 65 | } 66 | 67 | return $chunk; 68 | } 69 | 70 | /** 71 | * Checks whether the name is simple (unqualified). 72 | */ 73 | public function isSimple(): bool 74 | { 75 | return \count($this->parts) === 1; 76 | } 77 | 78 | /** 79 | * Returns {@see true} in case of name is full qualified. 80 | */ 81 | public function isFullQualified(): bool 82 | { 83 | return $this instanceof FullQualifiedName; 84 | } 85 | 86 | /** 87 | * Returns {@see true} in case of name contains special class reference. 88 | */ 89 | public function isSpecial(): bool 90 | { 91 | $first = $this->getFirstPart(); 92 | 93 | return $this->isSimple() && $first->isSpecial(); 94 | } 95 | 96 | /** 97 | * Returns {@see true} in case of name contains builtin type name. 98 | */ 99 | public function isBuiltin(): bool 100 | { 101 | $first = $this->getFirstPart(); 102 | 103 | return $this->isSimple() && $first->isBuiltin(); 104 | } 105 | 106 | public function slice(int $offset = 0, ?int $length = null): self 107 | { 108 | return new static(\array_slice($this->parts, $offset, $length)); 109 | } 110 | 111 | /** 112 | * Appends the passed {@see Name} to the existing one at the end. 113 | * 114 | * ```php 115 | * $name = new Name('Some\Any'); 116 | * 117 | * echo $name->withAdded(new Name('Test\Class')); 118 | * > "Some\Any\Test\Class" 119 | * 120 | * echo $name->withAdded(new Name('Any\Class')); 121 | * > "Some\Any\Any\Class" 122 | * ``` 123 | */ 124 | public function withAdded(self $name): self 125 | { 126 | return new static([ 127 | ...$this->parts, 128 | ...$name->parts, 129 | ]); 130 | } 131 | 132 | /** 133 | * Combines two names into one (in case the last one is an alias). 134 | * 135 | * ```php 136 | * $name = new Name('Some\Any'); 137 | * 138 | * echo $name->mergeWith(new Name('Test\Class')); 139 | * > "Some\Any\Class" 140 | * 141 | * echo $name->mergeWith(new Name('Any\Class')); 142 | * > "Some\Any\Class" 143 | * ``` 144 | * 145 | * Real world use case: 146 | * ```php 147 | * // use TypeLang\Parser\Node; 148 | * // echo Node::class; 149 | * 150 | * $name = new Name('TypeLang\Parser\Node'); 151 | * echo $name->mergeWith(new Name('Node')); 152 | * 153 | * // > TypeLang\Parser\Node 154 | * ``` 155 | * 156 | * Or aliased: 157 | * ```php 158 | * // use TypeLang\Parser\Exception as Error; 159 | * // echo Error\SemanticException::class; 160 | * 161 | * $name = new Name('TypeLang\Parser\Exception'); 162 | * echo $name->mergeWith(new Name('Error\SemanticException')); 163 | * 164 | * // > TypeLang\Parser\Exception\SemanticException 165 | * ``` 166 | */ 167 | public function mergeWith(self $name): self 168 | { 169 | return new static([ 170 | ...$this->parts, 171 | ...\array_slice($name->parts, 1), 172 | ]); 173 | } 174 | 175 | /** 176 | * Convert name to full qualified name instance. 177 | */ 178 | public function toFullQualified(): FullQualifiedName 179 | { 180 | if ($this instanceof FullQualifiedName) { 181 | return clone $this; 182 | } 183 | 184 | return new FullQualifiedName($this->parts); 185 | } 186 | 187 | /** 188 | * @return non-empty-list 189 | */ 190 | public function getParts(): array 191 | { 192 | return $this->parts; 193 | } 194 | 195 | /** 196 | * @return non-empty-list 197 | */ 198 | public function getPartsAsString(): array 199 | { 200 | $result = []; 201 | 202 | foreach ($this->parts as $identifier) { 203 | $result[] = $identifier->toString(); 204 | } 205 | 206 | return $result; 207 | } 208 | 209 | public function getFirstPart(): Identifier 210 | { 211 | return $this->parts[\array_key_first($this->parts)]; 212 | } 213 | 214 | /** 215 | * @return non-empty-string 216 | */ 217 | public function getFirstPartAsString(): string 218 | { 219 | $identifier = $this->getFirstPart(); 220 | 221 | return $identifier->toString(); 222 | } 223 | 224 | public function getLastPart(): Identifier 225 | { 226 | return $this->parts[\array_key_last($this->parts)]; 227 | } 228 | 229 | /** 230 | * @return non-empty-string 231 | */ 232 | public function getLastPartAsString(): string 233 | { 234 | $identifier = $this->getLastPart(); 235 | 236 | return $identifier->toString(); 237 | } 238 | 239 | /** 240 | * Returns name as string. 241 | * 242 | * @return non-empty-string 243 | */ 244 | public function toString(): string 245 | { 246 | return \implode(self::NAMESPACE_DELIMITER, $this->getPartsAsString()); 247 | } 248 | 249 | /** 250 | * Returns lowercased name as string. 251 | * 252 | * @return non-empty-lowercase-string 253 | */ 254 | public function toLowerString(): string 255 | { 256 | return \strtolower($this->toString()); 257 | } 258 | 259 | /** 260 | * @return \Traversable 261 | */ 262 | public function getIterator(): \Traversable 263 | { 264 | return new \ArrayIterator($this->parts); 265 | } 266 | 267 | /** 268 | * @return int<1, max> 269 | */ 270 | public function count(): int 271 | { 272 | return \count($this->parts); 273 | } 274 | 275 | /** 276 | * @return non-empty-string 277 | */ 278 | public function __toString(): string 279 | { 280 | return $this->toString(); 281 | } 282 | 283 | /** 284 | * @return array{int<0, max>, non-empty-list} 285 | */ 286 | public function __serialize(): array 287 | { 288 | return [$this->offset, $this->parts]; 289 | } 290 | 291 | /** 292 | * @param array{0?: int<0, max>, 1?: non-empty-list} $data 293 | * 294 | * @throws \UnexpectedValueException 295 | */ 296 | public function __unserialize(array $data): void 297 | { 298 | $this->offset = $data[0] ?? throw new \UnexpectedValueException( 299 | message: 'Unable to unserialize Name offset', 300 | ); 301 | 302 | $this->parts = $data[1] ?? throw new \UnexpectedValueException( 303 | message: 'Unable to unserialize Name identifier parts', 304 | ); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Node/Node.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public int $offset = 0; 13 | 14 | public function getOffset(): int 15 | { 16 | return $this->offset; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Node/NodeInterface.php: -------------------------------------------------------------------------------- 1 | getOffset(), 19 | * ); 20 | * 21 | * echo 'line: ' . $position->getLine() . "\n" 22 | * 'column: ' . $position->getColumn(); 23 | * ``` 24 | * 25 | * @return int<0, max> 26 | */ 27 | public function getOffset(): int; 28 | } 29 | -------------------------------------------------------------------------------- /src/Node/NodeList.php: -------------------------------------------------------------------------------- 1 | 10 | * @template-implements \ArrayAccess, TNode> 11 | */ 12 | abstract class NodeList extends Node implements 13 | \IteratorAggregate, 14 | \ArrayAccess, 15 | \Countable 16 | { 17 | /** 18 | * @param list $items 19 | */ 20 | public function __construct( 21 | public array $items = [], 22 | ) {} 23 | 24 | /** 25 | * @return TNode|null 26 | */ 27 | public function first(): ?Node 28 | { 29 | $first = \reset($this->items); 30 | 31 | return $first instanceof Node ? $first : null; 32 | } 33 | 34 | /** 35 | * @return TNode|null 36 | */ 37 | public function last(): ?Node 38 | { 39 | $last = \end($this->items); 40 | 41 | return $last instanceof Node ? $last : null; 42 | } 43 | 44 | public function offsetExists(mixed $offset): bool 45 | { 46 | // @phpstan-ignore-next-line 47 | assert(\is_int($offset) && $offset >= 0); 48 | 49 | return isset($this->items[$offset]); 50 | } 51 | 52 | public function offsetGet(mixed $offset): ?Node 53 | { 54 | // @phpstan-ignore-next-line 55 | assert(\is_int($offset) && $offset >= 0); 56 | 57 | return $this->items[$offset] ?? null; 58 | } 59 | 60 | public function offsetSet(mixed $offset, mixed $value): void 61 | { 62 | // @phpstan-ignore-next-line 63 | assert(\is_int($offset) && $offset >= 0); 64 | // @phpstan-ignore-next-line 65 | assert($value instanceof Node); 66 | 67 | // @phpstan-ignore-next-line 68 | $this->items[$offset] = $value; 69 | 70 | if (!\array_is_list($this->items)) { 71 | $this->items = \array_values($this->items); 72 | } 73 | } 74 | 75 | public function offsetUnset(mixed $offset): void 76 | { 77 | // @phpstan-ignore-next-line 78 | assert(\is_int($offset) && $offset >= 0); 79 | 80 | $items = $this->items; 81 | unset($items[$offset]); 82 | $this->items = \array_values($items); 83 | } 84 | 85 | public function getIterator(): \Traversable 86 | { 87 | return new \ArrayIterator($this->items); 88 | } 89 | 90 | /** 91 | * @return int<0, max> 92 | */ 93 | public function count(): int 94 | { 95 | return \count($this->items); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Node/Statement.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class AttributeArgumentsListNode extends NodeList {} 13 | -------------------------------------------------------------------------------- /src/Node/Stmt/Attribute/AttributeGroupNode.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AttributeGroupNode extends NodeList {} 13 | -------------------------------------------------------------------------------- /src/Node/Stmt/Attribute/AttributeGroupsListNode.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AttributeGroupsListNode extends NodeList {} 13 | -------------------------------------------------------------------------------- /src/Node/Stmt/Attribute/AttributeNode.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CallableParametersListNode extends ParametersListNode {} 11 | -------------------------------------------------------------------------------- /src/Node/Stmt/Callable/ParameterNode.php: -------------------------------------------------------------------------------- 1 | type; 42 | } 43 | 44 | public function __toString(): string 45 | { 46 | $result = []; 47 | 48 | if ($this->output) { 49 | $result[] = 'output'; 50 | } 51 | 52 | if ($this->variadic) { 53 | $result[] = 'variadic'; 54 | } 55 | 56 | if ($this->optional) { 57 | $result[] = 'optional'; 58 | } 59 | 60 | if ($result === []) { 61 | return 'simple'; 62 | } 63 | 64 | return \implode(', ', $result); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Node/Stmt/Callable/ParametersListNode.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @deprecated Since 1.3, please use {@see CallableParametersListNode} instead. 14 | */ 15 | class ParametersListNode extends NodeList {} 16 | -------------------------------------------------------------------------------- /src/Node/Stmt/CallableTypeNode.php: -------------------------------------------------------------------------------- 1 | constant = \is_string($constant) ? new Identifier($constant) : $constant; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Node/Stmt/ClassConstNode.php: -------------------------------------------------------------------------------- 1 | name->toString() . '*'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Node/Stmt/GenericTypeNode.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class GenericTypeNode extends GenericTypeStmt {} 12 | -------------------------------------------------------------------------------- /src/Node/Stmt/GenericTypeStmt.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class IntersectionTypeNode extends LogicalTypeNode {} 12 | -------------------------------------------------------------------------------- /src/Node/Stmt/LogicalTypeNode.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class LogicalTypeNode extends TypeStatement implements \IteratorAggregate, \Countable 12 | { 13 | /** 14 | * @var non-empty-list 15 | */ 16 | public array $statements; 17 | 18 | public function __construct( 19 | TypeStatement $a, 20 | TypeStatement $b, 21 | TypeStatement ...$other, 22 | ) { 23 | // @phpstan-ignore-next-line : List of types cannot be empty 24 | $this->statements = [...$this->unwrap([$a, $b, ...$other])]; 25 | } 26 | 27 | /** 28 | * @param non-empty-list $statements 29 | * 30 | * @return iterable 31 | */ 32 | private function unwrap(array $statements): iterable 33 | { 34 | foreach ($statements as $statement) { 35 | if ($statement instanceof static) { 36 | yield from $this->unwrap($statement->statements); 37 | } else { 38 | yield $statement; 39 | } 40 | } 41 | } 42 | 43 | public function getIterator(): \Traversable 44 | { 45 | return new \ArrayIterator($this->statements); 46 | } 47 | 48 | /** 49 | * @return int<2, max> a logical statement must contain at least 2 elements 50 | */ 51 | public function count(): int 52 | { 53 | /** @var int<2, max> */ 54 | return \count($this->statements); 55 | } 56 | 57 | /** 58 | * @return array{int<0, max>, non-empty-list} 59 | */ 60 | public function __serialize(): array 61 | { 62 | return [$this->offset, $this->statements]; 63 | } 64 | 65 | /** 66 | * @param array{0?: int<0, max>, 1?: non-empty-list} $data 67 | * 68 | * @throws \UnexpectedValueException 69 | */ 70 | public function __unserialize(array $data): void 71 | { 72 | $this->offset = $data[0] ?? throw new \UnexpectedValueException(\sprintf( 73 | 'Unable to unserialize %s offset', 74 | static::class, 75 | )); 76 | 77 | $this->statements = $data[1] ?? throw new \UnexpectedValueException(\sprintf( 78 | 'Unable to unserialize %s statements', 79 | static::class, 80 | )); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Node/Stmt/NamedTypeNode.php: -------------------------------------------------------------------------------- 1 | name = $name instanceof Name ? $name : new Name($name); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Node/Stmt/NullableTypeNode.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class NullableTypeNode extends GenericTypeNode {} 12 | -------------------------------------------------------------------------------- /src/Node/Stmt/Shape/ExplicitFieldNode.php: -------------------------------------------------------------------------------- 1 | type; 27 | } 28 | 29 | public function __toString(): string 30 | { 31 | return $this->optional ? 'optional' : 'required'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Node/Stmt/Shape/FieldsListNode.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FieldsListNode extends NodeList implements \Stringable 13 | { 14 | /** 15 | * @param list $list 16 | */ 17 | public function __construct( 18 | array $list = [], 19 | public bool $sealed = true, 20 | ) { 21 | parent::__construct($list); 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return $this->sealed ? 'sealed' : 'unsealed'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Node/Stmt/Shape/ImplicitFieldNode.php: -------------------------------------------------------------------------------- 1 | key = \is_string($key) ? new Identifier($key) : $key; 25 | 26 | parent::__construct($of, $optional, $attributes); 27 | } 28 | 29 | public function getKey(): string 30 | { 31 | /** @var non-empty-string */ 32 | return $this->key->toString(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Node/Stmt/Shape/NumericFieldNode.php: -------------------------------------------------------------------------------- 1 | key->getValue(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Node/Stmt/Shape/StringNamedFieldNode.php: -------------------------------------------------------------------------------- 1 | key->getValue(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Node/Stmt/Template/ArgumentNode.php: -------------------------------------------------------------------------------- 1 | hint = \is_string($hint) ? new Identifier($hint) : $hint; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Node/Stmt/Template/ArgumentsListNode.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @deprecated Since 1.1, please use {@see TemplateArgumentsListNode} instead. 14 | */ 15 | class ArgumentsListNode extends NodeList {} 16 | -------------------------------------------------------------------------------- /src/Node/Stmt/Template/TemplateArgumentNode.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class TemplateArgumentsListNode extends ArgumentsListNode {} 11 | -------------------------------------------------------------------------------- /src/Node/Stmt/TernaryConditionNode.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class TypeOffsetAccessNode extends GenericTypeNode 12 | { 13 | public function __construct( 14 | TypeStatement $type, 15 | public readonly TypeStatement $access, 16 | ) { 17 | parent::__construct($type); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Node/Stmt/TypeStatement.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class TypesListNode extends GenericTypeNode {} 12 | -------------------------------------------------------------------------------- /src/Node/Stmt/UnionTypeNode.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UnionTypeNode extends LogicalTypeNode {} 12 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | , 35 | * ... 36 | * }, 37 | * skip: list, 38 | * grammar: array, 39 | * reducers: array|non-empty-string, callable(Context, mixed): mixed>, 40 | * transitions?: array 41 | * } 42 | */ 43 | final class Parser implements ParserInterface 44 | { 45 | /** 46 | * @var ParserCombinator 47 | */ 48 | private readonly ParserCombinator $parser; 49 | 50 | private readonly Lexer $lexer; 51 | 52 | /** 53 | * In-memory string literal pool. 54 | * 55 | * @var \WeakMap 56 | * 57 | * @api an annotation for another PhpStorm bug fix... 58 | * 59 | * @internal ...but it is truly an internal property 60 | */ 61 | protected readonly \WeakMap $stringPool; 62 | 63 | /** 64 | * In-memory integer literal pool. 65 | * 66 | * @var \WeakMap 67 | * 68 | * @api an annotation for another PhpStorm bug fix... 69 | * 70 | * @internal ...but it is truly an internal property 71 | */ 72 | protected readonly \WeakMap $integerPool; 73 | 74 | private readonly BuilderInterface $builder; 75 | 76 | /** 77 | * @var int<0, max> 78 | */ 79 | public int $lastProcessedTokenOffset = 0; 80 | 81 | /** 82 | * @param bool $tolerant Enables or disables tolerant type recognition. If 83 | * the option is {@see true}, the parser allows arbitrary text after 84 | * the type definition. This mode allows you to recognize types 85 | * specified in DocBlocks. 86 | * @param bool $conditional enables or disables support for 87 | * dependent/conditional types such as `T ? U : V` 88 | * @param bool $shapes enables or disables support for type shapes 89 | * such as `T{key: U}` 90 | * @param bool $callables enables or disables support for callable types 91 | * such as `(T, U): V` 92 | * @param bool $literals enables or disables support for literal types such 93 | * as `42` or `"string"` 94 | * @param bool $generics enables or disables support for template arguments 95 | * such as `T` 96 | * @param bool $union enables or disables support for logical union types 97 | * such as `T | U` 98 | * @param bool $intersection enables or disables support for logical 99 | * intersection types such as `T & U` 100 | * @param bool $list enables or disables support for square bracket list 101 | * types such as `T[]` 102 | * @param bool $offsets enables or disables support for square bracket 103 | * offset access types such as `T[U]` 104 | * @param bool $hints enables or disables support for template argument 105 | * hints such as `T` 106 | * @param bool $attributes enables or disables support for attributes 107 | * such as `#[attr]` 108 | */ 109 | public function __construct( 110 | public readonly bool $tolerant = false, 111 | public readonly bool $conditional = true, 112 | public readonly bool $shapes = true, 113 | public readonly bool $callables = true, 114 | public readonly bool $literals = true, 115 | public readonly bool $generics = true, 116 | public readonly bool $union = true, 117 | public readonly bool $intersection = true, 118 | public readonly bool $list = true, 119 | public readonly bool $offsets = true, 120 | public readonly bool $hints = true, 121 | public readonly bool $attributes = true, 122 | private readonly SourceFactoryInterface $sources = new SourceFactory(), 123 | ) { 124 | /** @phpstan-var GrammarConfigArray $grammar */ 125 | $grammar = require __DIR__ . '/../resources/grammar.php'; 126 | 127 | $this->stringPool = new \WeakMap(); 128 | $this->integerPool = new \WeakMap(); 129 | 130 | $this->builder = $this->createBuilder($grammar['reducers']); 131 | $this->lexer = $this->createLexer($grammar); 132 | $this->parser = $this->createParser($this->lexer, $grammar); 133 | } 134 | 135 | /** 136 | * @param array|non-empty-string, callable(Context, mixed):mixed> $reducers 137 | */ 138 | private function createBuilder(array $reducers): BuilderInterface 139 | { 140 | return new class ($reducers) implements BuilderInterface { 141 | /** 142 | * @param array|non-empty-string, callable(Context, mixed):mixed> $reducers 143 | */ 144 | public function __construct( 145 | private readonly array $reducers, 146 | ) {} 147 | 148 | public function build(Context $context, mixed $result): mixed 149 | { 150 | if (!isset($this->reducers[$context->state])) { 151 | return $result; 152 | } 153 | 154 | $result = ($this->reducers[$context->state])($context, $result); 155 | 156 | if ($result instanceof Node && $result->offset === 0) { 157 | $result->offset = $context->lastProcessedToken->getOffset(); 158 | } 159 | 160 | return $result; 161 | } 162 | }; 163 | } 164 | 165 | /** 166 | * @phpstan-param GrammarConfigArray $grammar 167 | * 168 | * @return ParserCombinator 169 | */ 170 | private function createParser(LexerInterface $lexer, array $grammar): ParserCombinator 171 | { 172 | /** @var ParserCombinator */ 173 | return new ParserCombinator( 174 | lexer: $lexer, 175 | grammar: $grammar['grammar'], 176 | options: [ 177 | ParserConfigsInterface::CONFIG_INITIAL_RULE => $grammar['initial'], 178 | ParserConfigsInterface::CONFIG_AST_BUILDER => $this->builder, 179 | ParserConfigsInterface::CONFIG_ALLOW_TRAILING_TOKENS => $this->tolerant, 180 | ], 181 | ); 182 | } 183 | 184 | /** 185 | * @phpstan-param GrammarConfigArray $grammar 186 | */ 187 | private function createLexer(array $grammar): Lexer 188 | { 189 | return new Lexer( 190 | tokens: $grammar['tokens']['default'], 191 | skip: $grammar['skip'], 192 | onUnknownToken: new PassthroughHandler(), 193 | ); 194 | } 195 | 196 | public function parse(mixed $source): TypeStatement 197 | { 198 | $this->lastProcessedTokenOffset = 0; 199 | 200 | try { 201 | $instance = $this->sources->create($source); 202 | 203 | try { 204 | foreach ($this->parser->parse($instance) as $stmt) { 205 | if ($stmt instanceof TypeStatement) { 206 | $context = $this->parser->getLastExecutionContext(); 207 | 208 | if ($context !== null) { 209 | $token = $context->buffer->current(); 210 | 211 | $this->lastProcessedTokenOffset = $token->getOffset(); 212 | } 213 | 214 | return $stmt; 215 | } 216 | } 217 | 218 | throw new ParseException( 219 | message: 'Could not read type statement', 220 | code: ParseException::ERROR_CODE_INTERNAL_ERROR, 221 | ); 222 | } catch (UnexpectedTokenException $e) { 223 | throw $this->unexpectedTokenError($e, $instance); 224 | } catch (UnrecognizedTokenException $e) { 225 | throw $this->unrecognizedTokenError($e, $instance); 226 | } catch (ParserRuntimeExceptionInterface $e) { 227 | throw $this->parserRuntimeError($e, $instance); 228 | } catch (SemanticException $e) { 229 | throw $this->semanticError($e, $instance); 230 | } catch (\Throwable $e) { 231 | throw $this->internalError($e, $instance); 232 | } 233 | } catch (SourceExceptionInterface $e) { 234 | throw new ParseException( 235 | message: $e->getMessage(), 236 | code: ParseException::ERROR_CODE_INTERNAL_ERROR, 237 | previous: $e, 238 | ); 239 | } 240 | } 241 | 242 | /** 243 | * @throws SourceExceptionInterface in case of source content reading error 244 | */ 245 | private function unexpectedTokenError(UnexpectedTokenException $e, ReadableInterface $source): ParseException 246 | { 247 | $token = $e->getToken(); 248 | 249 | return ParseException::fromUnexpectedToken( 250 | token: $token->getValue(), 251 | statement: $source->getContents(), 252 | offset: $token->getOffset(), 253 | ); 254 | } 255 | 256 | /** 257 | * @throws SourceExceptionInterface in case of source content reading error 258 | */ 259 | private function unrecognizedTokenError(UnrecognizedTokenException $e, ReadableInterface $source): ParseException 260 | { 261 | $token = $e->getToken(); 262 | 263 | return ParseException::fromUnrecognizedToken( 264 | token: $token->getValue(), 265 | statement: $source->getContents(), 266 | offset: $token->getOffset(), 267 | ); 268 | } 269 | 270 | /** 271 | * @throws SourceExceptionInterface in case of source content reading error 272 | */ 273 | private function semanticError(SemanticException $e, ReadableInterface $source): ParseException 274 | { 275 | return ParseException::fromSemanticError($e, $source); 276 | } 277 | 278 | /** 279 | * @throws SourceExceptionInterface in case of source content reading error 280 | */ 281 | private function parserRuntimeError(ParserRuntimeExceptionInterface $e, ReadableInterface $source): ParseException 282 | { 283 | $token = $e->getToken(); 284 | 285 | return ParseException::fromUnrecognizedSyntaxError( 286 | statement: $source->getContents(), 287 | offset: $token->getOffset(), 288 | ); 289 | } 290 | 291 | /** 292 | * @throws SourceExceptionInterface in case of source content reading error 293 | */ 294 | private function internalError(\Throwable $e, ReadableInterface $source): ParseException 295 | { 296 | return ParseException::fromInternalError( 297 | statement: $source->getContents(), 298 | e: $e, 299 | ); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/ParserInterface.php: -------------------------------------------------------------------------------- 1 | $lastProcessedTokenOffset 12 | */ 13 | interface ParserInterface 14 | { 15 | /** 16 | * Parses variadic sources into an abstract source tree (AST) node. 17 | * 18 | * @throws ParserExceptionInterface in case of parsing exception occurs 19 | * @throws \Throwable in case of internal error occurs 20 | */ 21 | public function parse(mixed $source): TypeStatement; 22 | } 23 | -------------------------------------------------------------------------------- /src/Traverser.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $visitors = []; 16 | 17 | /** 18 | * @param list $visitors 19 | */ 20 | public function __construct(iterable $visitors = []) 21 | { 22 | foreach ($visitors as $visitor) { 23 | $this->append($visitor); 24 | } 25 | } 26 | 27 | /** 28 | * @template TArgVisitor of VisitorInterface 29 | * 30 | * @param TArgVisitor $visitor 31 | * @param iterable $nodes 32 | * 33 | * @return TArgVisitor 34 | */ 35 | public static function through(VisitorInterface $visitor, iterable $nodes): VisitorInterface 36 | { 37 | $instance = self::new([$visitor]); 38 | $instance->traverse($nodes); 39 | 40 | return $visitor; 41 | } 42 | 43 | /** 44 | * Creates a new traverser instance. 45 | * 46 | * @param list $visitors 47 | */ 48 | public static function new(iterable $visitors = []): self 49 | { 50 | return new self($visitors); 51 | } 52 | 53 | public function with(VisitorInterface $visitor, bool $prepend = false): self 54 | { 55 | $self = clone $this; 56 | 57 | return $prepend ? $self->prepend($visitor) : $self->append($visitor); 58 | } 59 | 60 | public function append(VisitorInterface $visitor): self 61 | { 62 | $this->visitors[] = $visitor; 63 | 64 | return $this; 65 | } 66 | 67 | public function prepend(VisitorInterface $visitor): self 68 | { 69 | \array_unshift($this->visitors, $visitor); 70 | 71 | return $this; 72 | } 73 | 74 | public function traverse(iterable $nodes): void 75 | { 76 | foreach ($this->visitors as $visitor) { 77 | $visitor->before(); 78 | } 79 | 80 | $this->applyToIterable($nodes); 81 | 82 | foreach ($this->visitors as $visitor) { 83 | $visitor->after(); 84 | } 85 | } 86 | 87 | /** 88 | * @return iterable 89 | */ 90 | private function getProperties(Node $node): iterable 91 | { 92 | return \get_object_vars($node); 93 | } 94 | 95 | private function applyToNode(Node $node): void 96 | { 97 | foreach ($this->visitors as $visitor) { 98 | $command = $visitor->enter($node); 99 | 100 | if ($command === null) { 101 | foreach ($this->getProperties($node) as $property) { 102 | if ($property instanceof Node) { 103 | $this->applyToNode($property); 104 | } elseif (\is_iterable($property)) { 105 | $this->applyToIterable($property); 106 | } 107 | } 108 | } 109 | 110 | $visitor->leave($node); 111 | } 112 | } 113 | 114 | /** 115 | * @param iterable $nodes 116 | */ 117 | private function applyToIterable(iterable $nodes): void 118 | { 119 | foreach ($nodes as $node) { 120 | if (!$node instanceof Node) { 121 | break; 122 | } 123 | 124 | $this->applyToNode($node); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Traverser/ClassNameMatcherVisitor.php: -------------------------------------------------------------------------------- 1 | $class 13 | * @param (\Closure(Node):bool)|null $break 14 | */ 15 | public function __construct(string $class, ?\Closure $break = null) 16 | { 17 | $matcher = static fn(Node $node): bool => $node instanceof $class; 18 | 19 | parent::__construct($matcher, $break); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Traverser/Command.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private int $depth = 0; 20 | 21 | private readonly string $prefix; 22 | 23 | public function __construct(bool $simplifyNames = true) 24 | { 25 | $this->prefix = $simplifyNames ? self::NODE_NAMESPACE_PREFIX : ''; 26 | } 27 | 28 | abstract protected function write(string $data): void; 29 | 30 | public function before(): void 31 | { 32 | $this->depth = 0; 33 | } 34 | 35 | public function enter(Node $node): ?Command 36 | { 37 | $prefix = \str_repeat(' ', $this->depth++); 38 | $suffix = \str_replace($this->prefix, '', $node::class); 39 | 40 | if ($node instanceof \Stringable) { 41 | $suffix .= \sprintf('(%s)', (string) $node); 42 | } 43 | 44 | $this->write($prefix . $suffix . "\n"); 45 | 46 | return null; 47 | } 48 | 49 | public function leave(Node $node): void 50 | { 51 | // @phpstan-ignore-next-line : $depth is always non-negative 52 | --$this->depth; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Traverser/MatcherVisitor.php: -------------------------------------------------------------------------------- 1 | found !== null; 27 | } 28 | 29 | public function getFoundNode(): ?Node 30 | { 31 | return $this->found; 32 | } 33 | 34 | public function before(): void 35 | { 36 | $this->found = null; 37 | } 38 | 39 | public function enter(Node $node): ?Command 40 | { 41 | if ($this->found !== null || $this->shouldContinue) { 42 | return Command::SKIP_CHILDREN; 43 | } 44 | 45 | if (($this->matcher)($node)) { 46 | $this->shouldContinue = true; 47 | $this->found = $node; 48 | 49 | return Command::SKIP_CHILDREN; 50 | } 51 | 52 | if ($this->break !== null && ($this->break)($node)) { 53 | $this->shouldContinue = true; 54 | 55 | return Command::SKIP_CHILDREN; 56 | } 57 | 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Traverser/StreamDumperVisitor.php: -------------------------------------------------------------------------------- 1 | stream = $resource; 27 | } 28 | 29 | protected function write(string $data): void 30 | { 31 | \fwrite($this->stream, $data); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Traverser/StringDumperVisitor.php: -------------------------------------------------------------------------------- 1 | reset(); 14 | 15 | parent::before(); 16 | } 17 | 18 | public function getOutput(): string 19 | { 20 | return $this->output; 21 | } 22 | 23 | public function reset(): void 24 | { 25 | $this->output = ''; 26 | } 27 | 28 | protected function write(string $data): void 29 | { 30 | $this->output .= $data; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Traverser/TypeMapVisitor.php: -------------------------------------------------------------------------------- 1 | transform)($name); 26 | 27 | if ($result instanceof Name) { 28 | return $result; 29 | } 30 | 31 | return $name; 32 | } 33 | 34 | public function enter(Node $node): ?Command 35 | { 36 | switch (true) { 37 | case $node instanceof NamedTypeNode: 38 | case $node instanceof CallableTypeNode: 39 | case $node instanceof ConstMaskNode: 40 | $node->name = $this->map($node->name); 41 | 42 | return null; 43 | 44 | case $node instanceof ClassConstMaskNode: 45 | $node->class = $this->map($node->class); 46 | 47 | return null; 48 | 49 | default: 50 | return null; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Traverser/Visitor.php: -------------------------------------------------------------------------------- 1 | $nodes 16 | */ 17 | public function traverse(iterable $nodes): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/TypeResolver.php: -------------------------------------------------------------------------------- 1 | parse(<<<'PHP' 32 | * array { Node, Error\SemanticException } 33 | * PHP); 34 | * 35 | * $resolver = new \TypeLang\Parser\TypeResolver(); 36 | * $result = $resolver->resolveWith($expected, [ 37 | * 'TypeLang\Parser\Node', // use TypeLang\Parser\Node; 38 | * 'Error' => 'TypeLang\Parser\Exception', // use TypeLang\Parser\Exception as Error; 39 | * ]); 40 | * 41 | * // Expected Output: 42 | * // > array{ 43 | * // > TypeLang\Parser\Node, 44 | * // > TypeLang\Parser\Exception\SemanticException 45 | * // > } 46 | * ``` 47 | * 48 | * @param array $replacements 49 | */ 50 | public function resolveWith(TypeStatement $type, array $replacements): TypeStatement 51 | { 52 | foreach ($replacements as $key => $replacement) { 53 | // normalize value 54 | if (\is_string($replacement)) { 55 | $replacement = \str_starts_with($replacement, '\\') 56 | ? new FullQualifiedName($replacement) 57 | : new Name($replacement); 58 | } 59 | 60 | // normalize key 61 | if (\is_int($key)) { 62 | unset($replacements[$key]); 63 | 64 | $key = $replacement->getLastPartAsString(); 65 | } 66 | 67 | $replacements[\strtolower($key)] = $replacement; 68 | } 69 | 70 | /** @var array $replacements */ 71 | return $this->resolve($type, static function (Name $name) use ($replacements) { 72 | $first = \strtolower($name->getFirstPartAsString()); 73 | 74 | if (isset($replacements[$first])) { 75 | $prefix = $replacements[$first]; 76 | 77 | return $prefix->mergeWith($name); 78 | } 79 | 80 | return null; 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/TypeResolverInterface.php: -------------------------------------------------------------------------------- 1 |