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