├── 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 |
9 |
10 |
11 |
12 |
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 |