Chris@13
|
1 <?php declare(strict_types=1);
|
Chris@0
|
2
|
Chris@0
|
3 namespace PhpParser\Lexer;
|
Chris@0
|
4
|
Chris@17
|
5 use PhpParser\Error;
|
Chris@17
|
6 use PhpParser\ErrorHandler;
|
Chris@17
|
7 use PhpParser\Parser;
|
Chris@17
|
8
|
Chris@0
|
9 class Emulative extends \PhpParser\Lexer
|
Chris@0
|
10 {
|
Chris@17
|
11 const PHP_7_3 = '7.3.0dev';
|
Chris@17
|
12 const PHP_7_4 = '7.4.0dev';
|
Chris@17
|
13
|
Chris@17
|
14 const FLEXIBLE_DOC_STRING_REGEX = <<<'REGEX'
|
Chris@17
|
15 /<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n
|
Chris@17
|
16 (?:.*\r?\n)*?
|
Chris@17
|
17 (?<indentation>\h*)\2(?![a-zA-Z_\x80-\xff])(?<separator>(?:;?[\r\n])?)/x
|
Chris@17
|
18 REGEX;
|
Chris@17
|
19
|
Chris@17
|
20 const T_COALESCE_EQUAL = 1007;
|
Chris@17
|
21
|
Chris@17
|
22 /**
|
Chris@17
|
23 * @var mixed[] Patches used to reverse changes introduced in the code
|
Chris@17
|
24 */
|
Chris@17
|
25 private $patches = [];
|
Chris@17
|
26
|
Chris@17
|
27 /**
|
Chris@17
|
28 * @param mixed[] $options
|
Chris@17
|
29 */
|
Chris@17
|
30 public function __construct(array $options = [])
|
Chris@17
|
31 {
|
Chris@17
|
32 parent::__construct($options);
|
Chris@17
|
33
|
Chris@17
|
34 // add emulated tokens here
|
Chris@17
|
35 $this->tokenMap[self::T_COALESCE_EQUAL] = Parser\Tokens::T_COALESCE_EQUAL;
|
Chris@17
|
36 }
|
Chris@17
|
37
|
Chris@17
|
38 public function startLexing(string $code, ErrorHandler $errorHandler = null) {
|
Chris@17
|
39 $this->patches = [];
|
Chris@17
|
40
|
Chris@17
|
41 if ($this->isEmulationNeeded($code) === false) {
|
Chris@17
|
42 // Nothing to emulate, yay
|
Chris@17
|
43 parent::startLexing($code, $errorHandler);
|
Chris@17
|
44 return;
|
Chris@17
|
45 }
|
Chris@17
|
46
|
Chris@17
|
47 $collector = new ErrorHandler\Collecting();
|
Chris@17
|
48
|
Chris@17
|
49 // 1. emulation of heredoc and nowdoc new syntax
|
Chris@17
|
50 $preparedCode = $this->processHeredocNowdoc($code);
|
Chris@17
|
51 parent::startLexing($preparedCode, $collector);
|
Chris@17
|
52
|
Chris@17
|
53 // 2. emulation of ??= token
|
Chris@17
|
54 $this->processCoaleseEqual($code);
|
Chris@17
|
55 $this->fixupTokens();
|
Chris@17
|
56
|
Chris@17
|
57 $errors = $collector->getErrors();
|
Chris@17
|
58 if (!empty($errors)) {
|
Chris@17
|
59 $this->fixupErrors($errors);
|
Chris@17
|
60 foreach ($errors as $error) {
|
Chris@17
|
61 $errorHandler->handleError($error);
|
Chris@17
|
62 }
|
Chris@17
|
63 }
|
Chris@17
|
64 }
|
Chris@17
|
65
|
Chris@17
|
66 private function isCoalesceEqualEmulationNeeded(string $code): bool
|
Chris@17
|
67 {
|
Chris@17
|
68 // skip version where this works without emulation
|
Chris@17
|
69 if (version_compare(\PHP_VERSION, self::PHP_7_4, '>=')) {
|
Chris@17
|
70 return false;
|
Chris@17
|
71 }
|
Chris@17
|
72
|
Chris@17
|
73 return strpos($code, '??=') !== false;
|
Chris@17
|
74 }
|
Chris@17
|
75
|
Chris@17
|
76 private function processCoaleseEqual(string $code)
|
Chris@17
|
77 {
|
Chris@17
|
78 if ($this->isCoalesceEqualEmulationNeeded($code) === false) {
|
Chris@17
|
79 return;
|
Chris@17
|
80 }
|
Chris@17
|
81
|
Chris@17
|
82 // We need to manually iterate and manage a count because we'll change
|
Chris@17
|
83 // the tokens array on the way
|
Chris@17
|
84 $line = 1;
|
Chris@17
|
85 for ($i = 0, $c = count($this->tokens); $i < $c; ++$i) {
|
Chris@17
|
86 if (isset($this->tokens[$i + 1])) {
|
Chris@17
|
87 if ($this->tokens[$i][0] === T_COALESCE && $this->tokens[$i + 1] === '=') {
|
Chris@17
|
88 array_splice($this->tokens, $i, 2, [
|
Chris@17
|
89 [self::T_COALESCE_EQUAL, '??=', $line]
|
Chris@17
|
90 ]);
|
Chris@17
|
91 $c--;
|
Chris@17
|
92 continue;
|
Chris@17
|
93 }
|
Chris@17
|
94 }
|
Chris@17
|
95 if (\is_array($this->tokens[$i])) {
|
Chris@17
|
96 $line += substr_count($this->tokens[$i][1], "\n");
|
Chris@17
|
97 }
|
Chris@17
|
98 }
|
Chris@17
|
99 }
|
Chris@17
|
100
|
Chris@17
|
101 private function isHeredocNowdocEmulationNeeded(string $code): bool
|
Chris@17
|
102 {
|
Chris@17
|
103 // skip version where this works without emulation
|
Chris@17
|
104 if (version_compare(\PHP_VERSION, self::PHP_7_3, '>=')) {
|
Chris@17
|
105 return false;
|
Chris@17
|
106 }
|
Chris@17
|
107
|
Chris@17
|
108 return strpos($code, '<<<') !== false;
|
Chris@17
|
109 }
|
Chris@17
|
110
|
Chris@17
|
111 private function processHeredocNowdoc(string $code): string
|
Chris@17
|
112 {
|
Chris@17
|
113 if ($this->isHeredocNowdocEmulationNeeded($code) === false) {
|
Chris@17
|
114 return $code;
|
Chris@17
|
115 }
|
Chris@17
|
116
|
Chris@17
|
117 if (!preg_match_all(self::FLEXIBLE_DOC_STRING_REGEX, $code, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
|
Chris@17
|
118 // No heredoc/nowdoc found
|
Chris@17
|
119 return $code;
|
Chris@17
|
120 }
|
Chris@17
|
121
|
Chris@17
|
122 // Keep track of how much we need to adjust string offsets due to the modifications we
|
Chris@17
|
123 // already made
|
Chris@17
|
124 $posDelta = 0;
|
Chris@17
|
125 foreach ($matches as $match) {
|
Chris@17
|
126 $indentation = $match['indentation'][0];
|
Chris@17
|
127 $indentationStart = $match['indentation'][1];
|
Chris@17
|
128
|
Chris@17
|
129 $separator = $match['separator'][0];
|
Chris@17
|
130 $separatorStart = $match['separator'][1];
|
Chris@17
|
131
|
Chris@17
|
132 if ($indentation === '' && $separator !== '') {
|
Chris@17
|
133 // Ordinary heredoc/nowdoc
|
Chris@17
|
134 continue;
|
Chris@17
|
135 }
|
Chris@17
|
136
|
Chris@17
|
137 if ($indentation !== '') {
|
Chris@17
|
138 // Remove indentation
|
Chris@17
|
139 $indentationLen = strlen($indentation);
|
Chris@17
|
140 $code = substr_replace($code, '', $indentationStart + $posDelta, $indentationLen);
|
Chris@17
|
141 $this->patches[] = [$indentationStart + $posDelta, 'add', $indentation];
|
Chris@17
|
142 $posDelta -= $indentationLen;
|
Chris@17
|
143 }
|
Chris@17
|
144
|
Chris@17
|
145 if ($separator === '') {
|
Chris@17
|
146 // Insert newline as separator
|
Chris@17
|
147 $code = substr_replace($code, "\n", $separatorStart + $posDelta, 0);
|
Chris@17
|
148 $this->patches[] = [$separatorStart + $posDelta, 'remove', "\n"];
|
Chris@17
|
149 $posDelta += 1;
|
Chris@17
|
150 }
|
Chris@17
|
151 }
|
Chris@17
|
152
|
Chris@17
|
153 return $code;
|
Chris@17
|
154 }
|
Chris@17
|
155
|
Chris@17
|
156 private function isEmulationNeeded(string $code): bool
|
Chris@17
|
157 {
|
Chris@17
|
158 if ($this->isHeredocNowdocEmulationNeeded($code)) {
|
Chris@17
|
159 return true;
|
Chris@17
|
160 }
|
Chris@17
|
161
|
Chris@17
|
162 if ($this->isCoalesceEqualEmulationNeeded($code)) {
|
Chris@17
|
163 return true;
|
Chris@17
|
164 }
|
Chris@17
|
165
|
Chris@17
|
166 return false;
|
Chris@17
|
167 }
|
Chris@17
|
168
|
Chris@17
|
169 private function fixupTokens()
|
Chris@17
|
170 {
|
Chris@17
|
171 if (\count($this->patches) === 0) {
|
Chris@17
|
172 return;
|
Chris@17
|
173 }
|
Chris@17
|
174
|
Chris@17
|
175 // Load first patch
|
Chris@17
|
176 $patchIdx = 0;
|
Chris@17
|
177
|
Chris@17
|
178 list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
|
Chris@17
|
179
|
Chris@17
|
180 // We use a manual loop over the tokens, because we modify the array on the fly
|
Chris@17
|
181 $pos = 0;
|
Chris@17
|
182 for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) {
|
Chris@17
|
183 $token = $this->tokens[$i];
|
Chris@17
|
184 if (\is_string($token)) {
|
Chris@17
|
185 // We assume that patches don't apply to string tokens
|
Chris@17
|
186 $pos += \strlen($token);
|
Chris@17
|
187 continue;
|
Chris@17
|
188 }
|
Chris@17
|
189
|
Chris@17
|
190 $len = \strlen($token[1]);
|
Chris@17
|
191 $posDelta = 0;
|
Chris@17
|
192 while ($patchPos >= $pos && $patchPos < $pos + $len) {
|
Chris@17
|
193 $patchTextLen = \strlen($patchText);
|
Chris@17
|
194 if ($patchType === 'remove') {
|
Chris@17
|
195 if ($patchPos === $pos && $patchTextLen === $len) {
|
Chris@17
|
196 // Remove token entirely
|
Chris@17
|
197 array_splice($this->tokens, $i, 1, []);
|
Chris@17
|
198 $i--;
|
Chris@17
|
199 $c--;
|
Chris@17
|
200 } else {
|
Chris@17
|
201 // Remove from token string
|
Chris@17
|
202 $this->tokens[$i][1] = substr_replace(
|
Chris@17
|
203 $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen
|
Chris@17
|
204 );
|
Chris@17
|
205 $posDelta -= $patchTextLen;
|
Chris@17
|
206 }
|
Chris@17
|
207 } elseif ($patchType === 'add') {
|
Chris@17
|
208 // Insert into the token string
|
Chris@17
|
209 $this->tokens[$i][1] = substr_replace(
|
Chris@17
|
210 $token[1], $patchText, $patchPos - $pos + $posDelta, 0
|
Chris@17
|
211 );
|
Chris@17
|
212 $posDelta += $patchTextLen;
|
Chris@17
|
213 } else {
|
Chris@17
|
214 assert(false);
|
Chris@17
|
215 }
|
Chris@17
|
216
|
Chris@17
|
217 // Fetch the next patch
|
Chris@17
|
218 $patchIdx++;
|
Chris@17
|
219 if ($patchIdx >= \count($this->patches)) {
|
Chris@17
|
220 // No more patches, we're done
|
Chris@17
|
221 return;
|
Chris@17
|
222 }
|
Chris@17
|
223
|
Chris@17
|
224 list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
|
Chris@17
|
225
|
Chris@17
|
226 // Multiple patches may apply to the same token. Reload the current one to check
|
Chris@17
|
227 // If the new patch applies
|
Chris@17
|
228 $token = $this->tokens[$i];
|
Chris@17
|
229 }
|
Chris@17
|
230
|
Chris@17
|
231 $pos += $len;
|
Chris@17
|
232 }
|
Chris@17
|
233
|
Chris@17
|
234 // A patch did not apply
|
Chris@17
|
235 assert(false);
|
Chris@17
|
236 }
|
Chris@17
|
237
|
Chris@17
|
238 /**
|
Chris@17
|
239 * Fixup line and position information in errors.
|
Chris@17
|
240 *
|
Chris@17
|
241 * @param Error[] $errors
|
Chris@17
|
242 */
|
Chris@17
|
243 private function fixupErrors(array $errors) {
|
Chris@17
|
244 foreach ($errors as $error) {
|
Chris@17
|
245 $attrs = $error->getAttributes();
|
Chris@17
|
246
|
Chris@17
|
247 $posDelta = 0;
|
Chris@17
|
248 $lineDelta = 0;
|
Chris@17
|
249 foreach ($this->patches as $patch) {
|
Chris@17
|
250 list($patchPos, $patchType, $patchText) = $patch;
|
Chris@17
|
251 if ($patchPos >= $attrs['startFilePos']) {
|
Chris@17
|
252 // No longer relevant
|
Chris@17
|
253 break;
|
Chris@17
|
254 }
|
Chris@17
|
255
|
Chris@17
|
256 if ($patchType === 'add') {
|
Chris@17
|
257 $posDelta += strlen($patchText);
|
Chris@17
|
258 $lineDelta += substr_count($patchText, "\n");
|
Chris@17
|
259 } else {
|
Chris@17
|
260 $posDelta -= strlen($patchText);
|
Chris@17
|
261 $lineDelta -= substr_count($patchText, "\n");
|
Chris@17
|
262 }
|
Chris@17
|
263 }
|
Chris@17
|
264
|
Chris@17
|
265 $attrs['startFilePos'] += $posDelta;
|
Chris@17
|
266 $attrs['endFilePos'] += $posDelta;
|
Chris@17
|
267 $attrs['startLine'] += $lineDelta;
|
Chris@17
|
268 $attrs['endLine'] += $lineDelta;
|
Chris@17
|
269 $error->setAttributes($attrs);
|
Chris@17
|
270 }
|
Chris@17
|
271 }
|
Chris@0
|
272 }
|