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\ReadonlyTokenEmulator; |
18: | use PhpParser\Lexer\TokenEmulator\ReverseEmulator; |
19: | use PhpParser\Lexer\TokenEmulator\TokenEmulator; |
20: | |
21: | class Emulative extends Lexer |
22: | { |
23: | const PHP_7_3 = '7.3dev'; |
24: | const PHP_7_4 = '7.4dev'; |
25: | const PHP_8_0 = '8.0dev'; |
26: | const PHP_8_1 = '8.1dev'; |
27: | |
28: | |
29: | private $patches = []; |
30: | |
31: | |
32: | private $emulators = []; |
33: | |
34: | |
35: | private $targetPhpVersion; |
36: | |
37: | |
38: | |
39: | |
40: | |
41: | |
42: | public function __construct(array $options = []) |
43: | { |
44: | $this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_1; |
45: | unset($options['phpVersion']); |
46: | |
47: | parent::__construct($options); |
48: | |
49: | $emulators = [ |
50: | new FlexibleDocStringEmulator(), |
51: | new FnTokenEmulator(), |
52: | new MatchTokenEmulator(), |
53: | new CoaleseEqualTokenEmulator(), |
54: | new NumericLiteralSeparatorEmulator(), |
55: | new NullsafeTokenEmulator(), |
56: | new AttributeEmulator(), |
57: | new EnumTokenEmulator(), |
58: | new ReadonlyTokenEmulator(), |
59: | new ExplicitOctalEmulator(), |
60: | ]; |
61: | |
62: | |
63: | |
64: | foreach ($emulators as $emulator) { |
65: | $emulatorPhpVersion = $emulator->getPhpVersion(); |
66: | if ($this->isForwardEmulationNeeded($emulatorPhpVersion)) { |
67: | $this->emulators[] = $emulator; |
68: | } else if ($this->isReverseEmulationNeeded($emulatorPhpVersion)) { |
69: | $this->emulators[] = new ReverseEmulator($emulator); |
70: | } |
71: | } |
72: | } |
73: | |
74: | public function startLexing(string $code, ErrorHandler $errorHandler = null) { |
75: | $emulators = array_filter($this->emulators, function($emulator) use($code) { |
76: | return $emulator->isEmulationNeeded($code); |
77: | }); |
78: | |
79: | if (empty($emulators)) { |
80: | |
81: | parent::startLexing($code, $errorHandler); |
82: | return; |
83: | } |
84: | |
85: | $this->patches = []; |
86: | foreach ($emulators as $emulator) { |
87: | $code = $emulator->preprocessCode($code, $this->patches); |
88: | } |
89: | |
90: | $collector = new ErrorHandler\Collecting(); |
91: | parent::startLexing($code, $collector); |
92: | $this->sortPatches(); |
93: | $this->fixupTokens(); |
94: | |
95: | $errors = $collector->getErrors(); |
96: | if (!empty($errors)) { |
97: | $this->fixupErrors($errors); |
98: | foreach ($errors as $error) { |
99: | $errorHandler->handleError($error); |
100: | } |
101: | } |
102: | |
103: | foreach ($emulators as $emulator) { |
104: | $this->tokens = $emulator->emulate($code, $this->tokens); |
105: | } |
106: | } |
107: | |
108: | private function isForwardEmulationNeeded(string $emulatorPhpVersion): bool { |
109: | return version_compare(\PHP_VERSION, $emulatorPhpVersion, '<') |
110: | && version_compare($this->targetPhpVersion, $emulatorPhpVersion, '>='); |
111: | } |
112: | |
113: | private function isReverseEmulationNeeded(string $emulatorPhpVersion): bool { |
114: | return version_compare(\PHP_VERSION, $emulatorPhpVersion, '>=') |
115: | && version_compare($this->targetPhpVersion, $emulatorPhpVersion, '<'); |
116: | } |
117: | |
118: | private function sortPatches() |
119: | { |
120: | |
121: | |
122: | usort($this->patches, function($p1, $p2) { |
123: | return $p1[0] <=> $p2[0]; |
124: | }); |
125: | } |
126: | |
127: | private function fixupTokens() |
128: | { |
129: | if (\count($this->patches) === 0) { |
130: | return; |
131: | } |
132: | |
133: | |
134: | $patchIdx = 0; |
135: | |
136: | list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; |
137: | |
138: | |
139: | $pos = 0; |
140: | for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) { |
141: | $token = $this->tokens[$i]; |
142: | if (\is_string($token)) { |
143: | if ($patchPos === $pos) { |
144: | |
145: | assert($patchType === 'replace'); |
146: | $this->tokens[$i] = $patchText; |
147: | |
148: | |
149: | $patchIdx++; |
150: | if ($patchIdx >= \count($this->patches)) { |
151: | |
152: | return; |
153: | } |
154: | list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; |
155: | } |
156: | |
157: | $pos += \strlen($token); |
158: | continue; |
159: | } |
160: | |
161: | $len = \strlen($token[1]); |
162: | $posDelta = 0; |
163: | while ($patchPos >= $pos && $patchPos < $pos + $len) { |
164: | $patchTextLen = \strlen($patchText); |
165: | if ($patchType === 'remove') { |
166: | if ($patchPos === $pos && $patchTextLen === $len) { |
167: | |
168: | array_splice($this->tokens, $i, 1, []); |
169: | $i--; |
170: | $c--; |
171: | } else { |
172: | |
173: | $this->tokens[$i][1] = substr_replace( |
174: | $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen |
175: | ); |
176: | $posDelta -= $patchTextLen; |
177: | } |
178: | } elseif ($patchType === 'add') { |
179: | |
180: | $this->tokens[$i][1] = substr_replace( |
181: | $token[1], $patchText, $patchPos - $pos + $posDelta, 0 |
182: | ); |
183: | $posDelta += $patchTextLen; |
184: | } else if ($patchType === 'replace') { |
185: | |
186: | $this->tokens[$i][1] = substr_replace( |
187: | $token[1], $patchText, $patchPos - $pos + $posDelta, $patchTextLen |
188: | ); |
189: | } else { |
190: | assert(false); |
191: | } |
192: | |
193: | |
194: | $patchIdx++; |
195: | if ($patchIdx >= \count($this->patches)) { |
196: | |
197: | return; |
198: | } |
199: | |
200: | list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; |
201: | |
202: | |
203: | |
204: | $token = $this->tokens[$i]; |
205: | } |
206: | |
207: | $pos += $len; |
208: | } |
209: | |
210: | |
211: | assert(false); |
212: | } |
213: | |
214: | |
215: | |
216: | |
217: | |
218: | |
219: | private function fixupErrors(array $errors) { |
220: | foreach ($errors as $error) { |
221: | $attrs = $error->getAttributes(); |
222: | |
223: | $posDelta = 0; |
224: | $lineDelta = 0; |
225: | foreach ($this->patches as $patch) { |
226: | list($patchPos, $patchType, $patchText) = $patch; |
227: | if ($patchPos >= $attrs['startFilePos']) { |
228: | |
229: | break; |
230: | } |
231: | |
232: | if ($patchType === 'add') { |
233: | $posDelta += strlen($patchText); |
234: | $lineDelta += substr_count($patchText, "\n"); |
235: | } else if ($patchType === 'remove') { |
236: | $posDelta -= strlen($patchText); |
237: | $lineDelta -= substr_count($patchText, "\n"); |
238: | } |
239: | } |
240: | |
241: | $attrs['startFilePos'] += $posDelta; |
242: | $attrs['endFilePos'] += $posDelta; |
243: | $attrs['startLine'] += $lineDelta; |
244: | $attrs['endLine'] += $lineDelta; |
245: | $error->setAttributes($attrs); |
246: | } |
247: | } |
248: | } |
249: | |