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