annotate vendor/phpdocumentor/reflection-docblock/src/DocBlockFactory.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 7a779792577d
children
rev   line source
Chris@12 1 <?php
Chris@12 2 /**
Chris@12 3 * This file is part of phpDocumentor.
Chris@12 4 *
Chris@12 5 * For the full copyright and license information, please view the LICENSE
Chris@12 6 * file that was distributed with this source code.
Chris@12 7 *
Chris@12 8 * @copyright 2010-2015 Mike van Riel<mike@phpdoc.org>
Chris@12 9 * @license http://www.opensource.org/licenses/mit-license.php MIT
Chris@12 10 * @link http://phpdoc.org
Chris@12 11 */
Chris@12 12
Chris@12 13 namespace phpDocumentor\Reflection;
Chris@12 14
Chris@12 15 use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
Chris@12 16 use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
Chris@12 17 use phpDocumentor\Reflection\DocBlock\Tag;
Chris@12 18 use phpDocumentor\Reflection\DocBlock\TagFactory;
Chris@12 19 use Webmozart\Assert\Assert;
Chris@12 20
Chris@12 21 final class DocBlockFactory implements DocBlockFactoryInterface
Chris@12 22 {
Chris@12 23 /** @var DocBlock\DescriptionFactory */
Chris@12 24 private $descriptionFactory;
Chris@12 25
Chris@12 26 /** @var DocBlock\TagFactory */
Chris@12 27 private $tagFactory;
Chris@12 28
Chris@12 29 /**
Chris@12 30 * Initializes this factory with the required subcontractors.
Chris@12 31 *
Chris@12 32 * @param DescriptionFactory $descriptionFactory
Chris@12 33 * @param TagFactory $tagFactory
Chris@12 34 */
Chris@12 35 public function __construct(DescriptionFactory $descriptionFactory, TagFactory $tagFactory)
Chris@12 36 {
Chris@12 37 $this->descriptionFactory = $descriptionFactory;
Chris@12 38 $this->tagFactory = $tagFactory;
Chris@12 39 }
Chris@12 40
Chris@12 41 /**
Chris@12 42 * Factory method for easy instantiation.
Chris@12 43 *
Chris@12 44 * @param string[] $additionalTags
Chris@12 45 *
Chris@12 46 * @return DocBlockFactory
Chris@12 47 */
Chris@12 48 public static function createInstance(array $additionalTags = [])
Chris@12 49 {
Chris@12 50 $fqsenResolver = new FqsenResolver();
Chris@12 51 $tagFactory = new StandardTagFactory($fqsenResolver);
Chris@12 52 $descriptionFactory = new DescriptionFactory($tagFactory);
Chris@12 53
Chris@12 54 $tagFactory->addService($descriptionFactory);
Chris@12 55 $tagFactory->addService(new TypeResolver($fqsenResolver));
Chris@12 56
Chris@12 57 $docBlockFactory = new self($descriptionFactory, $tagFactory);
Chris@12 58 foreach ($additionalTags as $tagName => $tagHandler) {
Chris@12 59 $docBlockFactory->registerTagHandler($tagName, $tagHandler);
Chris@12 60 }
Chris@12 61
Chris@12 62 return $docBlockFactory;
Chris@12 63 }
Chris@12 64
Chris@12 65 /**
Chris@12 66 * @param object|string $docblock A string containing the DocBlock to parse or an object supporting the
Chris@12 67 * getDocComment method (such as a ReflectionClass object).
Chris@12 68 * @param Types\Context $context
Chris@12 69 * @param Location $location
Chris@12 70 *
Chris@12 71 * @return DocBlock
Chris@12 72 */
Chris@12 73 public function create($docblock, Types\Context $context = null, Location $location = null)
Chris@12 74 {
Chris@12 75 if (is_object($docblock)) {
Chris@12 76 if (!method_exists($docblock, 'getDocComment')) {
Chris@12 77 $exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method';
Chris@12 78 throw new \InvalidArgumentException($exceptionMessage);
Chris@12 79 }
Chris@12 80
Chris@12 81 $docblock = $docblock->getDocComment();
Chris@12 82 }
Chris@12 83
Chris@12 84 Assert::stringNotEmpty($docblock);
Chris@12 85
Chris@12 86 if ($context === null) {
Chris@12 87 $context = new Types\Context('');
Chris@12 88 }
Chris@12 89
Chris@12 90 $parts = $this->splitDocBlock($this->stripDocComment($docblock));
Chris@12 91 list($templateMarker, $summary, $description, $tags) = $parts;
Chris@12 92
Chris@12 93 return new DocBlock(
Chris@12 94 $summary,
Chris@12 95 $description ? $this->descriptionFactory->create($description, $context) : null,
Chris@12 96 array_filter($this->parseTagBlock($tags, $context), function ($tag) {
Chris@12 97 return $tag instanceof Tag;
Chris@12 98 }),
Chris@12 99 $context,
Chris@12 100 $location,
Chris@12 101 $templateMarker === '#@+',
Chris@12 102 $templateMarker === '#@-'
Chris@12 103 );
Chris@12 104 }
Chris@12 105
Chris@12 106 public function registerTagHandler($tagName, $handler)
Chris@12 107 {
Chris@12 108 $this->tagFactory->registerTagHandler($tagName, $handler);
Chris@12 109 }
Chris@12 110
Chris@12 111 /**
Chris@12 112 * Strips the asterisks from the DocBlock comment.
Chris@12 113 *
Chris@12 114 * @param string $comment String containing the comment text.
Chris@12 115 *
Chris@12 116 * @return string
Chris@12 117 */
Chris@12 118 private function stripDocComment($comment)
Chris@12 119 {
Chris@12 120 $comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment));
Chris@12 121
Chris@12 122 // reg ex above is not able to remove */ from a single line docblock
Chris@12 123 if (substr($comment, -2) === '*/') {
Chris@12 124 $comment = trim(substr($comment, 0, -2));
Chris@12 125 }
Chris@12 126
Chris@12 127 return str_replace(["\r\n", "\r"], "\n", $comment);
Chris@12 128 }
Chris@12 129
Chris@12 130 /**
Chris@12 131 * Splits the DocBlock into a template marker, summary, description and block of tags.
Chris@12 132 *
Chris@12 133 * @param string $comment Comment to split into the sub-parts.
Chris@12 134 *
Chris@12 135 * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split.
Chris@12 136 * @author Mike van Riel <me@mikevanriel.com> for extending the regex with template marker support.
Chris@12 137 *
Chris@12 138 * @return string[] containing the template marker (if any), summary, description and a string containing the tags.
Chris@12 139 */
Chris@12 140 private function splitDocBlock($comment)
Chris@12 141 {
Chris@12 142 // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This
Chris@12 143 // method does not split tags so we return this verbatim as the fourth result (tags). This saves us the
Chris@12 144 // performance impact of running a regular expression
Chris@12 145 if (strpos($comment, '@') === 0) {
Chris@12 146 return ['', '', '', $comment];
Chris@12 147 }
Chris@12 148
Chris@12 149 // clears all extra horizontal whitespace from the line endings to prevent parsing issues
Chris@12 150 $comment = preg_replace('/\h*$/Sum', '', $comment);
Chris@12 151
Chris@12 152 /*
Chris@12 153 * Splits the docblock into a template marker, summary, description and tags section.
Chris@12 154 *
Chris@12 155 * - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may
Chris@12 156 * occur after it and will be stripped).
Chris@12 157 * - The short description is started from the first character until a dot is encountered followed by a
Chris@12 158 * newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing
Chris@12 159 * errors). This is optional.
Chris@12 160 * - The long description, any character until a new line is encountered followed by an @ and word
Chris@12 161 * characters (a tag). This is optional.
Chris@12 162 * - Tags; the remaining characters
Chris@12 163 *
Chris@12 164 * Big thanks to RichardJ for contributing this Regular Expression
Chris@12 165 */
Chris@12 166 preg_match(
Chris@12 167 '/
Chris@12 168 \A
Chris@12 169 # 1. Extract the template marker
Chris@12 170 (?:(\#\@\+|\#\@\-)\n?)?
Chris@12 171
Chris@12 172 # 2. Extract the summary
Chris@12 173 (?:
Chris@12 174 (?! @\pL ) # The summary may not start with an @
Chris@12 175 (
Chris@12 176 [^\n.]+
Chris@12 177 (?:
Chris@12 178 (?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines
Chris@12 179 [\n.] (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line
Chris@12 180 [^\n.]+ # Include anything else
Chris@12 181 )*
Chris@12 182 \.?
Chris@12 183 )?
Chris@12 184 )
Chris@12 185
Chris@12 186 # 3. Extract the description
Chris@12 187 (?:
Chris@12 188 \s* # Some form of whitespace _must_ precede a description because a summary must be there
Chris@12 189 (?! @\pL ) # The description may not start with an @
Chris@12 190 (
Chris@12 191 [^\n]+
Chris@12 192 (?: \n+
Chris@12 193 (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line
Chris@12 194 [^\n]+ # Include anything else
Chris@12 195 )*
Chris@12 196 )
Chris@12 197 )?
Chris@12 198
Chris@12 199 # 4. Extract the tags (anything that follows)
Chris@12 200 (\s+ [\s\S]*)? # everything that follows
Chris@12 201 /ux',
Chris@12 202 $comment,
Chris@12 203 $matches
Chris@12 204 );
Chris@12 205 array_shift($matches);
Chris@12 206
Chris@12 207 while (count($matches) < 4) {
Chris@12 208 $matches[] = '';
Chris@12 209 }
Chris@12 210
Chris@12 211 return $matches;
Chris@12 212 }
Chris@12 213
Chris@12 214 /**
Chris@12 215 * Creates the tag objects.
Chris@12 216 *
Chris@12 217 * @param string $tags Tag block to parse.
Chris@12 218 * @param Types\Context $context Context of the parsed Tag
Chris@12 219 *
Chris@12 220 * @return DocBlock\Tag[]
Chris@12 221 */
Chris@12 222 private function parseTagBlock($tags, Types\Context $context)
Chris@12 223 {
Chris@12 224 $tags = $this->filterTagBlock($tags);
Chris@12 225 if (!$tags) {
Chris@12 226 return [];
Chris@12 227 }
Chris@12 228
Chris@12 229 $result = $this->splitTagBlockIntoTagLines($tags);
Chris@12 230 foreach ($result as $key => $tagLine) {
Chris@12 231 $result[$key] = $this->tagFactory->create(trim($tagLine), $context);
Chris@12 232 }
Chris@12 233
Chris@12 234 return $result;
Chris@12 235 }
Chris@12 236
Chris@12 237 /**
Chris@12 238 * @param string $tags
Chris@12 239 *
Chris@12 240 * @return string[]
Chris@12 241 */
Chris@12 242 private function splitTagBlockIntoTagLines($tags)
Chris@12 243 {
Chris@12 244 $result = [];
Chris@12 245 foreach (explode("\n", $tags) as $tag_line) {
Chris@12 246 if (isset($tag_line[0]) && ($tag_line[0] === '@')) {
Chris@12 247 $result[] = $tag_line;
Chris@12 248 } else {
Chris@12 249 $result[count($result) - 1] .= "\n" . $tag_line;
Chris@12 250 }
Chris@12 251 }
Chris@12 252
Chris@12 253 return $result;
Chris@12 254 }
Chris@12 255
Chris@12 256 /**
Chris@12 257 * @param $tags
Chris@12 258 * @return string
Chris@12 259 */
Chris@12 260 private function filterTagBlock($tags)
Chris@12 261 {
Chris@12 262 $tags = trim($tags);
Chris@12 263 if (!$tags) {
Chris@12 264 return null;
Chris@12 265 }
Chris@12 266
Chris@12 267 if ('@' !== $tags[0]) {
Chris@12 268 // @codeCoverageIgnoreStart
Chris@12 269 // Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that
Chris@12 270 // we didn't foresee.
Chris@12 271 throw new \LogicException('A tag block started with text instead of an at-sign(@): ' . $tags);
Chris@12 272 // @codeCoverageIgnoreEnd
Chris@12 273 }
Chris@12 274
Chris@12 275 return $tags;
Chris@12 276 }
Chris@12 277 }