comparison vendor/nikic/php-parser/lib/PhpParser/Lexer/Emulative.php @ 17:129ea1e6d783

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