xref: /PHP-Parser/grammar/phpyLang.php (revision af14fdb2)
1<?php declare(strict_types=1);
2
3///////////////////////////////
4/// Utility regex constants ///
5///////////////////////////////
6
7const LIB = '(?(DEFINE)
8    (?<singleQuotedString>\'[^\\\\\']*+(?:\\\\.[^\\\\\']*+)*+\')
9    (?<doubleQuotedString>"[^\\\\"]*+(?:\\\\.[^\\\\"]*+)*+")
10    (?<string>(?&singleQuotedString)|(?&doubleQuotedString))
11    (?<comment>/\*[^*]*+(?:\*(?!/)[^*]*+)*+\*/)
12    (?<code>\{[^\'"/{}]*+(?:(?:(?&string)|(?&comment)|(?&code)|/)[^\'"/{}]*+)*+})
13)';
14
15const PARAMS = '\[(?<params>[^[\]]*+(?:\[(?&params)\][^[\]]*+)*+)\]';
16const ARGS   = '\((?<args>[^()]*+(?:\((?&args)\)[^()]*+)*+)\)';
17
18///////////////////////////////
19/// Preprocessing functions ///
20///////////////////////////////
21
22function preprocessGrammar($code) {
23    $code = resolveNodes($code);
24    $code = resolveMacros($code);
25    $code = resolveStackAccess($code);
26    $code = str_replace('$this', '$self', $code);
27
28    return $code;
29}
30
31function resolveNodes($code) {
32    return preg_replace_callback(
33        '~\b(?<name>[A-Z][a-zA-Z_\\\\]++)\s*' . PARAMS . '~',
34        function ($matches) {
35            // recurse
36            $matches['params'] = resolveNodes($matches['params']);
37
38            $params = magicSplit(
39                '(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
40                $matches['params']
41            );
42
43            $paramCode = '';
44            foreach ($params as $param) {
45                $paramCode .= $param . ', ';
46            }
47
48            return 'new ' . $matches['name'] . '(' . $paramCode . 'attributes())';
49        },
50        $code
51    );
52}
53
54function resolveMacros($code) {
55    return preg_replace_callback(
56        '~\b(?<!::|->)(?!array\()(?<name>[a-z][A-Za-z]++)' . ARGS . '~',
57        function ($matches) {
58            // recurse
59            $matches['args'] = resolveMacros($matches['args']);
60
61            $name = $matches['name'];
62            $args = magicSplit(
63                '(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
64                $matches['args']
65            );
66
67            if ('attributes' === $name) {
68                assertArgs(0, $args, $name);
69                return '$this->getAttributes($this->tokenStartStack[#1], $this->tokenEndStack[$stackPos])';
70            }
71
72            if ('stackAttributes' === $name) {
73                assertArgs(1, $args, $name);
74                return '$this->getAttributes($this->tokenStartStack[' . $args[0] . '], '
75                       . ' $this->tokenEndStack[' . $args[0] . '])';
76            }
77
78            if ('init' === $name) {
79                return '$$ = array(' . implode(', ', $args) . ')';
80            }
81
82            if ('push' === $name) {
83                assertArgs(2, $args, $name);
84
85                return $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0];
86            }
87
88            if ('pushNormalizing' === $name) {
89                assertArgs(2, $args, $name);
90
91                return 'if (' . $args[1] . ' !== null) { ' . $args[0] . '[] = ' . $args[1] . '; } $$ = ' . $args[0] . ';';
92            }
93
94            if ('toBlock' == $name) {
95                assertArgs(1, $args, $name);
96
97                return 'if (' . $args[0] . ' instanceof Stmt\Block) { $$ = ' . $args[0] . '->stmts; } '
98                     . 'else if (' . $args[0] . ' === null) { $$ = []; } '
99                     . 'else { $$ = [' . $args[0] . ']; }';
100            }
101
102            if ('parseVar' === $name) {
103                assertArgs(1, $args, $name);
104
105                return 'substr(' . $args[0] . ', 1)';
106            }
107
108            if ('parseEncapsed' === $name) {
109                assertArgs(3, $args, $name);
110
111                return 'foreach (' . $args[0] . ' as $s) { if ($s instanceof Node\InterpolatedStringPart) {'
112                       . ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, ' . $args[1] . ', ' . $args[2] . '); } }';
113            }
114
115            if ('makeNop' === $name) {
116                assertArgs(1, $args, $name);
117
118                return $args[0] . ' = $this->maybeCreateNop($this->tokenStartStack[#1], $this->tokenEndStack[$stackPos])';
119            }
120
121            if ('makeZeroLengthNop' == $name) {
122                assertArgs(1, $args, $name);
123
124                return $args[0] . ' = $this->maybeCreateZeroLengthNop($this->tokenPos);';
125            }
126
127            return $matches[0];
128        },
129        $code
130    );
131}
132
133function assertArgs($num, $args, $name) {
134    if ($num != count($args)) {
135        die('Wrong argument count for ' . $name . '().');
136    }
137}
138
139function resolveStackAccess($code) {
140    $code = preg_replace('/\$\d+/', '$this->semStack[$0]', $code);
141    $code = preg_replace('/#(\d+)/', '$$1', $code);
142    return $code;
143}
144
145function removeTrailingWhitespace($code) {
146    $lines = explode("\n", $code);
147    $lines = array_map('rtrim', $lines);
148    return implode("\n", $lines);
149}
150
151//////////////////////////////
152/// Regex helper functions ///
153//////////////////////////////
154
155function regex($regex) {
156    return '~' . LIB . '(?:' . str_replace('~', '\~', $regex) . ')~';
157}
158
159function magicSplit($regex, $string) {
160    $pieces = preg_split(regex('(?:(?&string)|(?&comment)|(?&code))(*SKIP)(*FAIL)|' . $regex), $string);
161
162    foreach ($pieces as &$piece) {
163        $piece = trim($piece);
164    }
165
166    if ($pieces === ['']) {
167        return [];
168    }
169
170    return $pieces;
171}
172