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