Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 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;
150 lars 17
use PhpParser\Lexer\TokenEmulator\ReadonlyFunctionTokenEmulator;
148 lars 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';
150 lars 28
    const PHP_8_2 = '8.2dev';
148 lars 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
    {
150 lars 46
        $this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_2;
148 lars 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(),
150 lars 62
            new ReadonlyFunctionTokenEmulator(),
148 lars 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
}