PluginProbe
WPIDE – File Manager & Code Editor / 3.5.9
WPIDE – File Manager & Code Editor v3.5.9
3.5.9 3.5.8 3.5.7 2.0.14 2.0.15 2.0.16 2.0.2 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1 2.2 2.3 2.3.1 2.3.2 2.4.0 2.5 2.6 3.0 3.1 3.2 3.3 All 55 releases
wpide / vendor / nikic / php-parser / lib / PhpParser / Lexer / Emulative.php

Emulative.php in WPIDE – File Manager & Code Editor 3.5.9, at vendor/nikic/php-parser/lib/PhpParser/Lexer/Emulative.php

252 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php declare(strict_types=1);
2
3 namespace PhpParser\Lexer;
4
5 use PhpParser\Error;
6 use PhpParser\ErrorHandler;
7 use PhpParser\Lexer;
8 use PhpParser\Lexer\TokenEmulator\AttributeEmulator;
9 use PhpParser\Lexer\TokenEmulator\EnumTokenEmulator;
10 use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
11 use PhpParser\Lexer\TokenEmulator\ExplicitOctalEmulator;
12 use PhpParser\Lexer\TokenEmulator\FlexibleDocStringEmulator;
13 use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
14 use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
15 use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
16 use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
17 use PhpParser\Lexer\TokenEmulator\ReadonlyFunctionTokenEmulator;
18 use PhpParser\Lexer\TokenEmulator\ReadonlyTokenEmulator;
19 use PhpParser\Lexer\TokenEmulator\ReverseEmulator;
20 use PhpParser\Lexer\TokenEmulator\TokenEmulator;
21
22 class Emulative extends Lexer
23 {
24 const PHP_7_3 = '7.3dev';
25 const PHP_7_4 = '7.4dev';
26 const PHP_8_0 = '8.0dev';
27 const PHP_8_1 = '8.1dev';
28 const PHP_8_2 = '8.2dev';
29
30 /** @var mixed[] Patches used to reverse changes introduced in the code */
31 private $patches = [];
32
33 /** @var TokenEmulator[] */
34 private $emulators = [];
35
36 /** @var string */
37 private $targetPhpVersion;
38
39 /**
40 * @param mixed[] $options Lexer options. In addition to the usual options,
41 * accepts a 'phpVersion' string that specifies the
42 * version to emulate. Defaults to newest supported.
43 */
44 public function __construct(array $options = [])
45 {
46 $this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_2;
47 unset($options['phpVersion']);
48
49 parent::__construct($options);
50
51 $emulators = [
52 new FlexibleDocStringEmulator(),
53 new FnTokenEmulator(),
54 new MatchTokenEmulator(),
55 new CoaleseEqualTokenEmulator(),
56 new NumericLiteralSeparatorEmulator(),
57 new NullsafeTokenEmulator(),
58 new AttributeEmulator(),
59 new EnumTokenEmulator(),
60 new ReadonlyTokenEmulator(),
61 new ExplicitOctalEmulator(),
62 new ReadonlyFunctionTokenEmulator(),
63 ];
64
65 // Collect emulators that are relevant for the PHP version we're running
66 // and the PHP version we're targeting for emulation.
67 foreach ($emulators as $emulator) {
68 $emulatorPhpVersion = $emulator->getPhpVersion();
69 if ($this->isForwardEmulationNeeded($emulatorPhpVersion)) {
70 $this->emulators[] = $emulator;
71 } else if ($this->isReverseEmulationNeeded($emulatorPhpVersion)) {
72 $this->emulators[] = new ReverseEmulator($emulator);
73 }
74 }
75 }
76
77 public function startLexing(string $code, ?ErrorHandler $errorHandler = null) {
78 $emulators = array_filter($this->emulators, function($emulator) use($code) {
79 return $emulator->isEmulationNeeded($code);
80 });
81
82 if (empty($emulators)) {
83 // Nothing to emulate, yay
84 parent::startLexing($code, $errorHandler);
85 return;
86 }
87
88 $this->patches = [];
89 foreach ($emulators as $emulator) {
90 $code = $emulator->preprocessCode($code, $this->patches);
91 }
92
93 $collector = new ErrorHandler\Collecting();
94 parent::startLexing($code, $collector);
95 $this->sortPatches();
96 $this->fixupTokens();
97
98 $errors = $collector->getErrors();
99 if (!empty($errors)) {
100 $this->fixupErrors($errors);
101 foreach ($errors as $error) {
102 $errorHandler->handleError($error);
103 }
104 }
105
106 foreach ($emulators as $emulator) {
107 $this->tokens = $emulator->emulate($code, $this->tokens);
108 }
109 }
110
111 private function isForwardEmulationNeeded(string $emulatorPhpVersion): bool {
112 return version_compare(\PHP_VERSION, $emulatorPhpVersion, '<')
113 && version_compare($this->targetPhpVersion, $emulatorPhpVersion, '>=');
114 }
115
116 private function isReverseEmulationNeeded(string $emulatorPhpVersion): bool {
117 return version_compare(\PHP_VERSION, $emulatorPhpVersion, '>=')
118 && version_compare($this->targetPhpVersion, $emulatorPhpVersion, '<');
119 }
120
121 private function sortPatches()
122 {
123 // Patches may be contributed by different emulators.
124 // Make sure they are sorted by increasing patch position.
125 usort($this->patches, function($p1, $p2) {
126 return $p1[0] <=> $p2[0];
127 });
128 }
129
130 private function fixupTokens()
131 {
132 if (\count($this->patches) === 0) {
133 return;
134 }
135
136 // Load first patch
137 $patchIdx = 0;
138
139 list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
140
141 // We use a manual loop over the tokens, because we modify the array on the fly
142 $pos = 0;
143 for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) {
144 $token = $this->tokens[$i];
145 if (\is_string($token)) {
146 if ($patchPos === $pos) {
147 // Only support replacement for string tokens.
148 assert($patchType === 'replace');
149 $this->tokens[$i] = $patchText;
150
151 // Fetch the next patch
152 $patchIdx++;
153 if ($patchIdx >= \count($this->patches)) {
154 // No more patches, we're done
155 return;
156 }
157 list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
158 }
159
160 $pos += \strlen($token);
161 continue;
162 }
163
164 $len = \strlen($token[1]);
165 $posDelta = 0;
166 while ($patchPos >= $pos && $patchPos < $pos + $len) {
167 $patchTextLen = \strlen($patchText);
168 if ($patchType === 'remove') {
169 if ($patchPos === $pos && $patchTextLen === $len) {
170 // Remove token entirely
171 array_splice($this->tokens, $i, 1, []);
172 $i--;
173 $c--;
174 } else {
175 // Remove from token string
176 $this->tokens[$i][1] = substr_replace(
177 $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen
178 );
179 $posDelta -= $patchTextLen;
180 }
181 } elseif ($patchType === 'add') {
182 // Insert into the token string
183 $this->tokens[$i][1] = substr_replace(
184 $token[1], $patchText, $patchPos - $pos + $posDelta, 0
185 );
186 $posDelta += $patchTextLen;
187 } else if ($patchType === 'replace') {
188 // Replace inside the token string
189 $this->tokens[$i][1] = substr_replace(
190 $token[1], $patchText, $patchPos - $pos + $posDelta, $patchTextLen
191 );
192 } else {
193 assert(false);
194 }
195
196 // Fetch the next patch
197 $patchIdx++;
198 if ($patchIdx >= \count($this->patches)) {
199 // No more patches, we're done
200 return;
201 }
202
203 list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
204
205 // Multiple patches may apply to the same token. Reload the current one to check
206 // If the new patch applies
207 $token = $this->tokens[$i];
208 }
209
210 $pos += $len;
211 }
212
213 // A patch did not apply
214 assert(false);
215 }
216
217 /**
218 * Fixup line and position information in errors.
219 *
220 * @param Error[] $errors
221 */
222 private function fixupErrors(array $errors) {
223 foreach ($errors as $error) {
224 $attrs = $error->getAttributes();
225
226 $posDelta = 0;
227 $lineDelta = 0;
228 foreach ($this->patches as $patch) {
229 list($patchPos, $patchType, $patchText) = $patch;
230 if ($patchPos >= $attrs['startFilePos']) {
231 // No longer relevant
232 break;
233 }
234
235 if ($patchType === 'add') {
236 $posDelta += strlen($patchText);
237 $lineDelta += substr_count($patchText, "\n");
238 } else if ($patchType === 'remove') {
239 $posDelta -= strlen($patchText);
240 $lineDelta -= substr_count($patchText, "\n");
241 }
242 }
243
244 $attrs['startFilePos'] += $posDelta;
245 $attrs['endFilePos'] += $posDelta;
246 $attrs['startLine'] += $lineDelta;
247 $attrs['endLine'] += $lineDelta;
248 $error->setAttributes($attrs);
249 }
250 }
251 }
252