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: | |
31: | private $patches = []; |
32: | |
33: | |
34: | private $emulators = []; |
35: | |
36: | |
37: | private $targetPhpVersion; |
38: | |
39: | |
40: | |
41: | |
42: | |
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: | |
66: | |
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: | |
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: | |
124: | |
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: | |
137: | $patchIdx = 0; |
138: | |
139: | list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; |
140: | |
141: | |
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: | |
148: | assert($patchType === 'replace'); |
149: | $this->tokens[$i] = $patchText; |
150: | |
151: | |
152: | $patchIdx++; |
153: | if ($patchIdx >= \count($this->patches)) { |
154: | |
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: | |
171: | array_splice($this->tokens, $i, 1, []); |
172: | $i--; |
173: | $c--; |
174: | } else { |
175: | |
176: | $this->tokens[$i][1] = substr_replace( |
177: | $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen |
178: | ); |
179: | $posDelta -= $patchTextLen; |
180: | } |
181: | } elseif ($patchType === 'add') { |
182: | |
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: | |
189: | $this->tokens[$i][1] = substr_replace( |
190: | $token[1], $patchText, $patchPos - $pos + $posDelta, $patchTextLen |
191: | ); |
192: | } else { |
193: | assert(false); |
194: | } |
195: | |
196: | |
197: | $patchIdx++; |
198: | if ($patchIdx >= \count($this->patches)) { |
199: | |
200: | return; |
201: | } |
202: | |
203: | list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; |
204: | |
205: | |
206: | |
207: | $token = $this->tokens[$i]; |
208: | } |
209: | |
210: | $pos += $len; |
211: | } |
212: | |
213: | |
214: | assert(false); |
215: | } |
216: | |
217: | |
218: | |
219: | |
220: | |
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: | |
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: | |