Yaffs site version 1.1
[yaffs-website] / vendor / psy / psysh / src / Psy / Util / Docblock.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2017 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 = array(
34         'throws' => array('type', 'desc'),
35         'param'  => array('type', 'var', 'desc'),
36         'return' => array('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         $this->setComment($reflector->getDocComment());
78     }
79
80     /**
81      * Set and parse the docblock comment.
82      *
83      * @param string $comment The docblock
84      */
85     protected function setComment($comment)
86     {
87         $this->desc    = '';
88         $this->tags    = array();
89         $this->comment = $comment;
90
91         $this->parseComment($comment);
92     }
93
94     /**
95      * Find the length of the docblock prefix.
96      *
97      * @param array $lines
98      *
99      * @return int Prefix length
100      */
101     protected static function prefixLength(array $lines)
102     {
103         // find only lines with interesting things
104         $lines = array_filter($lines, function ($line) {
105             return substr($line, strspn($line, "* \t\n\r\0\x0B"));
106         });
107
108         // if we sort the lines, we only have to compare two items
109         sort($lines);
110
111         $first = reset($lines);
112         $last  = end($lines);
113
114         // find the longest common substring
115         $count = min(strlen($first), strlen($last));
116         for ($i = 0; $i < $count; $i++) {
117             if ($first[$i] !== $last[$i]) {
118                 return $i;
119             }
120         }
121
122         return $count;
123     }
124
125     /**
126      * Parse the comment into the component parts and set the state of the object.
127      *
128      * @param string $comment The docblock
129      */
130     protected function parseComment($comment)
131     {
132         // Strip the opening and closing tags of the docblock
133         $comment = substr($comment, 3, -2);
134
135         // Split into arrays of lines
136         $comment = array_filter(preg_split('/\r?\n\r?/', $comment));
137
138         // Trim asterisks and whitespace from the beginning and whitespace from the end of lines
139         $prefixLength = self::prefixLength($comment);
140         $comment = array_map(function ($line) use ($prefixLength) {
141             return rtrim(substr($line, $prefixLength));
142         }, $comment);
143
144         // Group the lines together by @tags
145         $blocks = array();
146         $b = -1;
147         foreach ($comment as $line) {
148             if (self::isTagged($line)) {
149                 $b++;
150                 $blocks[] = array();
151             } elseif ($b === -1) {
152                 $b = 0;
153                 $blocks[] = array();
154             }
155             $blocks[$b][] = $line;
156         }
157
158         // Parse the blocks
159         foreach ($blocks as $block => $body) {
160             $body = trim(implode("\n", $body));
161
162             if ($block === 0 && !self::isTagged($body)) {
163                 // This is the description block
164                 $this->desc = $body;
165             } else {
166                 // This block is tagged
167                 $tag  = substr(self::strTag($body), 1);
168                 $body = ltrim(substr($body, strlen($tag) + 2));
169
170                 if (isset(self::$vectors[$tag])) {
171                     // The tagged block is a vector
172                     $count = count(self::$vectors[$tag]);
173                     if ($body) {
174                         $parts = preg_split('/\s+/', $body, $count);
175                     } else {
176                         $parts = array();
177                     }
178
179                     // Default the trailing values
180                     $parts = array_pad($parts, $count, null);
181
182                     // Store as a mapped array
183                     $this->tags[$tag][] = array_combine(self::$vectors[$tag], $parts);
184                 } else {
185                     // The tagged block is only text
186                     $this->tags[$tag][] = $body;
187                 }
188             }
189         }
190     }
191
192     /**
193      * Whether or not a docblock contains a given @tag.
194      *
195      * @param string $tag The name of the @tag to check for
196      *
197      * @return bool
198      */
199     public function hasTag($tag)
200     {
201         return is_array($this->tags) && array_key_exists($tag, $this->tags);
202     }
203
204     /**
205      * The value of a tag.
206      *
207      * @param string $tag
208      *
209      * @return array
210      */
211     public function tag($tag)
212     {
213         return $this->hasTag($tag) ? $this->tags[$tag] : null;
214     }
215
216     /**
217      * Whether or not a string begins with a @tag.
218      *
219      * @param string $str
220      *
221      * @return bool
222      */
223     public static function isTagged($str)
224     {
225         return isset($str[1]) && $str[0] === '@' && ctype_alpha($str[1]);
226     }
227
228     /**
229      * The tag at the beginning of a string.
230      *
231      * @param string $str
232      *
233      * @return string|null
234      */
235     public static function strTag($str)
236     {
237         if (preg_match('/^@[a-z0-9_]+/', $str, $matches)) {
238             return $matches[0];
239         }
240     }
241 }