Subversion-Projekte lars-tiefland.laravel_shop

Revision

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

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of Psy Shell.
5
 *
200 lars 6
 * (c) 2012-2023 Justin Hileman
148 lars 7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Psy\Formatter;
13
 
14
use Psy\Exception\RuntimeException;
15
use Symfony\Component\Console\Formatter\OutputFormatter;
16
 
17
/**
18
 * A pretty-printer for code.
19
 */
20
class CodeFormatter implements ReflectorFormatter
21
{
22
    const LINE_MARKER = '  <urgent>></urgent> ';
23
    const NO_LINE_MARKER = '    ';
24
 
25
    const HIGHLIGHT_DEFAULT = 'default';
26
    const HIGHLIGHT_KEYWORD = 'keyword';
27
 
28
    const HIGHLIGHT_PUBLIC = 'public';
29
    const HIGHLIGHT_PROTECTED = 'protected';
30
    const HIGHLIGHT_PRIVATE = 'private';
31
 
32
    const HIGHLIGHT_CONST = 'const';
33
    const HIGHLIGHT_NUMBER = 'number';
34
    const HIGHLIGHT_STRING = 'string';
35
    const HIGHLIGHT_COMMENT = 'comment';
36
    const HIGHLIGHT_INLINE_HTML = 'inline_html';
37
 
38
    private static $tokenMap = [
39
        // Not highlighted
40
        \T_OPEN_TAG           => self::HIGHLIGHT_DEFAULT,
41
        \T_OPEN_TAG_WITH_ECHO => self::HIGHLIGHT_DEFAULT,
42
        \T_CLOSE_TAG          => self::HIGHLIGHT_DEFAULT,
43
        \T_STRING             => self::HIGHLIGHT_DEFAULT,
44
        \T_VARIABLE           => self::HIGHLIGHT_DEFAULT,
45
        \T_NS_SEPARATOR       => self::HIGHLIGHT_DEFAULT,
46
 
47
        // Visibility
48
        \T_PUBLIC    => self::HIGHLIGHT_PUBLIC,
49
        \T_PROTECTED => self::HIGHLIGHT_PROTECTED,
50
        \T_PRIVATE   => self::HIGHLIGHT_PRIVATE,
51
 
52
        // Constants
53
        \T_DIR      => self::HIGHLIGHT_CONST,
54
        \T_FILE     => self::HIGHLIGHT_CONST,
55
        \T_METHOD_C => self::HIGHLIGHT_CONST,
56
        \T_NS_C     => self::HIGHLIGHT_CONST,
57
        \T_LINE     => self::HIGHLIGHT_CONST,
58
        \T_CLASS_C  => self::HIGHLIGHT_CONST,
59
        \T_FUNC_C   => self::HIGHLIGHT_CONST,
60
        \T_TRAIT_C  => self::HIGHLIGHT_CONST,
61
 
62
        // Types
63
        \T_DNUMBER                  => self::HIGHLIGHT_NUMBER,
64
        \T_LNUMBER                  => self::HIGHLIGHT_NUMBER,
65
        \T_ENCAPSED_AND_WHITESPACE  => self::HIGHLIGHT_STRING,
66
        \T_CONSTANT_ENCAPSED_STRING => self::HIGHLIGHT_STRING,
67
 
68
        // Comments
69
        \T_COMMENT     => self::HIGHLIGHT_COMMENT,
70
        \T_DOC_COMMENT => self::HIGHLIGHT_COMMENT,
71
 
72
        // @todo something better here?
73
        \T_INLINE_HTML => self::HIGHLIGHT_INLINE_HTML,
74
    ];
75
 
76
    /**
77
     * Format the code represented by $reflector for shell output.
78
     *
79
     * @param \Reflector  $reflector
80
     * @param string|null $colorMode (deprecated and ignored)
81
     *
82
     * @return string formatted code
83
     */
84
    public static function format(\Reflector $reflector, string $colorMode = null): string
85
    {
86
        if (self::isReflectable($reflector)) {
87
            if ($code = @\file_get_contents($reflector->getFileName())) {
88
                return self::formatCode($code, self::getStartLine($reflector), $reflector->getEndLine());
89
            }
90
        }
91
 
92
        throw new RuntimeException('Source code unavailable');
93
    }
94
 
95
    /**
96
     * Format code for shell output.
97
     *
98
     * Optionally, restrict by $startLine and $endLine line numbers, or pass $markLine to add a line marker.
99
     *
100
     * @param string   $code
101
     * @param int      $startLine
102
     * @param int|null $endLine
103
     * @param int|null $markLine
104
     *
105
     * @return string formatted code
106
     */
107
    public static function formatCode(string $code, int $startLine = 1, int $endLine = null, int $markLine = null): string
108
    {
109
        $spans = self::tokenizeSpans($code);
110
        $lines = self::splitLines($spans, $startLine, $endLine);
111
        $lines = self::formatLines($lines);
112
        $lines = self::numberLines($lines, $markLine);
113
 
114
        return \implode('', \iterator_to_array($lines));
115
    }
116
 
117
    /**
118
     * Get the start line for a given Reflector.
119
     *
120
     * Tries to incorporate doc comments if possible.
121
     *
122
     * This is typehinted as \Reflector but we've narrowed the input via self::isReflectable already.
123
     *
124
     * @param \ReflectionClass|\ReflectionFunctionAbstract $reflector
125
     */
126
    private static function getStartLine(\Reflector $reflector): int
127
    {
128
        $startLine = $reflector->getStartLine();
129
 
130
        if ($docComment = $reflector->getDocComment()) {
131
            $startLine -= \preg_match_all('/(\r\n?|\n)/', $docComment) + 1;
132
        }
133
 
134
        return \max($startLine, 1);
135
    }
136
 
137
    /**
138
     * Split code into highlight spans.
139
     *
140
     * Tokenize via \token_get_all, then map these tokens to internal highlight types, combining
141
     * adjacent spans of the same highlight type.
142
     *
143
     * @todo consider switching \token_get_all() out for PHP-Parser-based formatting at some point.
144
     *
145
     * @param string $code
146
     *
147
     * @return \Generator [$spanType, $spanText] highlight spans
148
     */
149
    private static function tokenizeSpans(string $code): \Generator
150
    {
151
        $spanType = null;
152
        $buffer = '';
153
 
154
        foreach (\token_get_all($code) as $token) {
155
            $nextType = self::nextHighlightType($token, $spanType);
156
            $spanType = $spanType ?: $nextType;
157
 
158
            if ($spanType !== $nextType) {
159
                yield [$spanType, $buffer];
160
                $spanType = $nextType;
161
                $buffer = '';
162
            }
163
 
164
            $buffer .= \is_array($token) ? $token[1] : $token;
165
        }
166
 
167
        if ($spanType !== null && $buffer !== '') {
168
            yield [$spanType, $buffer];
169
        }
170
    }
171
 
172
    /**
173
     * Given a token and the current highlight span type, compute the next type.
174
     *
175
     * @param array|string $token       \token_get_all token
176
     * @param string|null  $currentType
177
     *
178
     * @return string|null
179
     */
180
    private static function nextHighlightType($token, $currentType)
181
    {
182
        if ($token === '"') {
183
            return self::HIGHLIGHT_STRING;
184
        }
185
 
186
        if (\is_array($token)) {
187
            if ($token[0] === \T_WHITESPACE) {
188
                return $currentType;
189
            }
190
 
191
            if (\array_key_exists($token[0], self::$tokenMap)) {
192
                return self::$tokenMap[$token[0]];
193
            }
194
        }
195
 
196
        return self::HIGHLIGHT_KEYWORD;
197
    }
198
 
199
    /**
200
     * Group highlight spans into an array of lines.
201
     *
202
     * Optionally, restrict by start and end line numbers.
203
     *
204
     * @param \Generator $spans     as [$spanType, $spanText] pairs
205
     * @param int        $startLine
206
     * @param int|null   $endLine
207
     *
208
     * @return \Generator lines, each an array of [$spanType, $spanText] pairs
209
     */
210
    private static function splitLines(\Generator $spans, int $startLine = 1, int $endLine = null): \Generator
211
    {
212
        $lineNum = 1;
213
        $buffer = [];
214
 
215
        foreach ($spans as list($spanType, $spanText)) {
216
            foreach (\preg_split('/(\r\n?|\n)/', $spanText) as $index => $spanLine) {
217
                if ($index > 0) {
218
                    if ($lineNum >= $startLine) {
219
                        yield $lineNum => $buffer;
220
                    }
221
 
222
                    $lineNum++;
223
                    $buffer = [];
224
 
225
                    if ($endLine !== null && $lineNum > $endLine) {
226
                        return;
227
                    }
228
                }
229
 
230
                if ($spanLine !== '') {
231
                    $buffer[] = [$spanType, $spanLine];
232
                }
233
            }
234
        }
235
 
236
        if (!empty($buffer)) {
237
            yield $lineNum => $buffer;
238
        }
239
    }
240
 
241
    /**
242
     * Format lines of highlight spans for shell output.
243
     *
244
     * @param \Generator $spanLines lines, each an array of [$spanType, $spanText] pairs
245
     *
246
     * @return \Generator Formatted lines
247
     */
248
    private static function formatLines(\Generator $spanLines): \Generator
249
    {
250
        foreach ($spanLines as $lineNum => $spanLine) {
251
            $line = '';
252
 
253
            foreach ($spanLine as list($spanType, $spanText)) {
254
                if ($spanType === self::HIGHLIGHT_DEFAULT) {
255
                    $line .= OutputFormatter::escape($spanText);
256
                } else {
257
                    $line .= \sprintf('<%s>%s</%s>', $spanType, OutputFormatter::escape($spanText), $spanType);
258
                }
259
            }
260
 
261
            yield $lineNum => $line.\PHP_EOL;
262
        }
263
    }
264
 
265
    /**
266
     * Prepend line numbers to formatted lines.
267
     *
268
     * Lines must be in an associative array with the correct keys in order to be numbered properly.
269
     *
270
     * Optionally, pass $markLine to add a line marker.
271
     *
272
     * @param \Generator $lines    Formatted lines
273
     * @param int|null   $markLine
274
     *
275
     * @return \Generator Numbered, formatted lines
276
     */
277
    private static function numberLines(\Generator $lines, int $markLine = null): \Generator
278
    {
279
        $lines = \iterator_to_array($lines);
280
 
281
        // Figure out how much space to reserve for line numbers.
282
        \end($lines);
283
        $pad = \strlen(\key($lines));
284
 
285
        // If $markLine is before or after our line range, don't bother reserving space for the marker.
286
        if ($markLine !== null) {
287
            if ($markLine > \key($lines)) {
288
                $markLine = null;
289
            }
290
 
291
            \reset($lines);
292
            if ($markLine < \key($lines)) {
293
                $markLine = null;
294
            }
295
        }
296
 
297
        foreach ($lines as $lineNum => $line) {
298
            $mark = '';
299
            if ($markLine !== null) {
300
                $mark = ($markLine === $lineNum) ? self::LINE_MARKER : self::NO_LINE_MARKER;
301
            }
302
 
303
            yield \sprintf("%s<aside>%{$pad}s</aside>: %s", $mark, $lineNum, $line);
304
        }
305
    }
306
 
307
    /**
308
     * Check whether a Reflector instance is reflectable by this formatter.
309
     *
200 lars 310
     * @phpstan-assert-if-true \ReflectionClass|\ReflectionFunctionAbstract $reflector
311
     *
148 lars 312
     * @param \Reflector $reflector
313
     */
314
    private static function isReflectable(\Reflector $reflector): bool
315
    {
316
        return ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) && \is_file($reflector->getFileName());
317
    }
318
}