annotate vendor/psy/psysh/src/Util/Docblock.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
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 }