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
2
 
3
/*
4
 * This file is part of Psy Shell.
5
 *
6
 * (c) 2012-2022 Justin Hileman
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\Util;
13
 
14
/**
15
 * A docblock representation.
16
 *
17
 * Based on PHP-DocBlock-Parser by Paul Scott:
18
 *
19
 * {@link http://www.github.com/icio/PHP-DocBlock-Parser}
20
 *
21
 * @author Paul Scott <paul@duedil.com>
22
 * @author Justin Hileman <justin@justinhileman.info>
23
 */
24
class Docblock
25
{
26
    /**
27
     * Tags in the docblock that have a whitespace-delimited number of parameters
28
     * (such as `@param type var desc` and `@return type desc`) and the names of
29
     * those parameters.
30
     *
31
     * @var array
32
     */
33
    public static $vectors = [
34
        'throws' => ['type', 'desc'],
35
        'param'  => ['type', 'var', 'desc'],
36
        'return' => ['type', 'desc'],
37
    ];
38
 
39
    protected $reflector;
40
 
41
    /**
42
     * The description of the symbol.
43
     *
44
     * @var string
45
     */
46
    public $desc;
47
 
48
    /**
49
     * The tags defined in the docblock.
50
     *
51
     * The array has keys which are the tag names (excluding the @) and values
52
     * that are arrays, each of which is an entry for the tag.
53
     *
54
     * In the case where the tag name is defined in {@see DocBlock::$vectors} the
55
     * value within the tag-value array is an array in itself with keys as
56
     * described by {@see DocBlock::$vectors}.
57
     *
58
     * @var array
59
     */
60
    public $tags;
61
 
62
    /**
63
     * The entire DocBlock comment that was parsed.
64
     *
65
     * @var string
66
     */
67
    public $comment;
68
 
69
    /**
70
     * Docblock constructor.
71
     *
72
     * @param \Reflector $reflector
73
     */
74
    public function __construct(\Reflector $reflector)
75
    {
76
        $this->reflector = $reflector;
77
 
78
        if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionClassConstant || $reflector instanceof \ReflectionFunctionAbstract || $reflector instanceof \ReflectionProperty) {
79
            $this->setComment($reflector->getDocComment());
80
        }
81
    }
82
 
83
    /**
84
     * Set and parse the docblock comment.
85
     *
86
     * @param string $comment The docblock
87
     */
88
    protected function setComment(string $comment)
89
    {
90
        $this->desc = '';
91
        $this->tags = [];
92
        $this->comment = $comment;
93
 
94
        $this->parseComment($comment);
95
    }
96
 
97
    /**
98
     * Find the length of the docblock prefix.
99
     *
100
     * @param array $lines
101
     *
102
     * @return int Prefix length
103
     */
104
    protected static function prefixLength(array $lines): int
105
    {
106
        // find only lines with interesting things
107
        $lines = \array_filter($lines, function ($line) {
108
            return \substr($line, \strspn($line, "* \t\n\r\0\x0B"));
109
        });
110
 
111
        // if we sort the lines, we only have to compare two items
112
        \sort($lines);
113
 
114
        $first = \reset($lines);
115
        $last = \end($lines);
116
 
117
        // Special case for single-line comments
118
        if (\count($lines) === 1) {
119
            return \strspn($first, "* \t\n\r\0\x0B");
120
        }
121
 
122
        // find the longest common substring
123
        $count = \min(\strlen($first), \strlen($last));
124
        for ($i = 0; $i < $count; $i++) {
125
            if ($first[$i] !== $last[$i]) {
126
                return $i;
127
            }
128
        }
129
 
130
        return $count;
131
    }
132
 
133
    /**
134
     * Parse the comment into the component parts and set the state of the object.
135
     *
136
     * @param string $comment The docblock
137
     */
138
    protected function parseComment(string $comment)
139
    {
140
        // Strip the opening and closing tags of the docblock
141
        $comment = \substr($comment, 3, -2);
142
 
143
        // Split into arrays of lines
144
        $comment = \array_filter(\preg_split('/\r?\n\r?/', $comment));
145
 
146
        // Trim asterisks and whitespace from the beginning and whitespace from the end of lines
147
        $prefixLength = self::prefixLength($comment);
148
        $comment = \array_map(function ($line) use ($prefixLength) {
149
            return \rtrim(\substr($line, $prefixLength));
150
        }, $comment);
151
 
152
        // Group the lines together by @tags
153
        $blocks = [];
154
        $b = -1;
155
        foreach ($comment as $line) {
156
            if (self::isTagged($line)) {
157
                $b++;
158
                $blocks[] = [];
159
            } elseif ($b === -1) {
160
                $b = 0;
161
                $blocks[] = [];
162
            }
163
            $blocks[$b][] = $line;
164
        }
165
 
166
        // Parse the blocks
167
        foreach ($blocks as $block => $body) {
168
            $body = \trim(\implode("\n", $body));
169
 
170
            if ($block === 0 && !self::isTagged($body)) {
171
                // This is the description block
172
                $this->desc = $body;
173
            } else {
174
                // This block is tagged
175
                $tag = \substr(self::strTag($body), 1);
176
                $body = \ltrim(\substr($body, \strlen($tag) + 2));
177
 
178
                if (isset(self::$vectors[$tag])) {
179
                    // The tagged block is a vector
180
                    $count = \count(self::$vectors[$tag]);
181
                    if ($body) {
182
                        $parts = \preg_split('/\s+/', $body, $count);
183
                    } else {
184
                        $parts = [];
185
                    }
186
 
187
                    // Default the trailing values
188
                    $parts = \array_pad($parts, $count, null);
189
 
190
                    // Store as a mapped array
191
                    $this->tags[$tag][] = \array_combine(self::$vectors[$tag], $parts);
192
                } else {
193
                    // The tagged block is only text
194
                    $this->tags[$tag][] = $body;
195
                }
196
            }
197
        }
198
    }
199
 
200
    /**
201
     * Whether or not a docblock contains a given @tag.
202
     *
203
     * @param string $tag The name of the @tag to check for
204
     *
205
     * @return bool
206
     */
207
    public function hasTag(string $tag): bool
208
    {
209
        return \is_array($this->tags) && \array_key_exists($tag, $this->tags);
210
    }
211
 
212
    /**
213
     * The value of a tag.
214
     *
215
     * @param string $tag
216
     *
217
     * @return array
218
     */
219
    public function tag(string $tag): array
220
    {
221
        return $this->hasTag($tag) ? $this->tags[$tag] : null;
222
    }
223
 
224
    /**
225
     * Whether or not a string begins with a @tag.
226
     *
227
     * @param string $str
228
     *
229
     * @return bool
230
     */
231
    public static function isTagged(string $str): bool
232
    {
233
        return isset($str[1]) && $str[0] === '@' && !\preg_match('/[^A-Za-z]/', $str[1]);
234
    }
235
 
236
    /**
237
     * The tag at the beginning of a string.
238
     *
239
     * @param string $str
240
     *
241
     * @return string|null
242
     */
243
    public static function strTag(string $str)
244
    {
245
        if (\preg_match('/^@[a-z0-9_]+/', $str, $matches)) {
246
            return $matches[0];
247
        }
248
    }
249
}