Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | 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;
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
    /** @var mixed[] Patches used to reverse changes introduced in the code */
29
    private $patches = [];
30
 
31
    /** @var TokenEmulator[] */
32
    private $emulators = [];
33
 
34
    /** @var string */
35
    private $targetPhpVersion;
36
 
37
    /**
38
     * @param mixed[] $options Lexer options. In addition to the usual options,
39
     *                         accepts a 'phpVersion' string that specifies the
40
     *                         version to emulate. Defaults to newest supported.
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
        // Collect emulators that are relevant for the PHP version we're running
63
        // and the PHP version we're targeting for emulation.
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
            // Nothing to emulate, yay
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
        // Patches may be contributed by different emulators.
121
        // Make sure they are sorted by increasing patch position.
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
        // Load first patch
134
        $patchIdx = 0;
135
 
136
        list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
137
 
138
        // We use a manual loop over the tokens, because we modify the array on the fly
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
                    // Only support replacement for string tokens.
145
                    assert($patchType === 'replace');
146
                    $this->tokens[$i] = $patchText;
147
 
148
                    // Fetch the next patch
149
                    $patchIdx++;
150
                    if ($patchIdx >= \count($this->patches)) {
151
                        // No more patches, we're done
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
                        // Remove token entirely
168
                        array_splice($this->tokens, $i, 1, []);
169
                        $i--;
170
                        $c--;
171
                    } else {
172
                        // Remove from token string
173
                        $this->tokens[$i][1] = substr_replace(
174
                            $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen
175
                        );
176
                        $posDelta -= $patchTextLen;
177
                    }
178
                } elseif ($patchType === 'add') {
179
                    // Insert into the token string
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
                    // Replace inside the token string
186
                    $this->tokens[$i][1] = substr_replace(
187
                        $token[1], $patchText, $patchPos - $pos + $posDelta, $patchTextLen
188
                    );
189
                } else {
190
                    assert(false);
191
                }
192
 
193
                // Fetch the next patch
194
                $patchIdx++;
195
                if ($patchIdx >= \count($this->patches)) {
196
                    // No more patches, we're done
197
                    return;
198
                }
199
 
200
                list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
201
 
202
                // Multiple patches may apply to the same token. Reload the current one to check
203
                // If the new patch applies
204
                $token = $this->tokens[$i];
205
            }
206
 
207
            $pos += $len;
208
        }
209
 
210
        // A patch did not apply
211
        assert(false);
212
    }
213
 
214
    /**
215
     * Fixup line and position information in errors.
216
     *
217
     * @param Error[] $errors
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
                    // No longer relevant
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
}