mirror of
https://github.com/barryvdh/ReflectionDocBlock.git
synced 2026-08-19 02:23:11 +00:00
The array of tags in the docblock constructor should only contain tags. Some of the factory methods of the tags are returning a null when the tag could not be created. This causes issues during parsing. By filtering these null values in the factory this use is resolved.
279 lines
9.2 KiB
PHP
279 lines
9.2 KiB
PHP
<?php
|
|
/**
|
|
* This file is part of phpDocumentor.
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*
|
|
* @copyright 2010-2015 Mike van Riel<[email protected]>
|
|
* @license http://www.opensource.org/licenses/mit-license.php MIT
|
|
* @link http://phpdoc.org
|
|
*/
|
|
|
|
namespace phpDocumentor\Reflection;
|
|
|
|
use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
|
|
use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
|
|
use phpDocumentor\Reflection\DocBlock\Tag;
|
|
use phpDocumentor\Reflection\DocBlock\TagFactory;
|
|
use phpDocumentor\Reflection\Types\Context;
|
|
use Webmozart\Assert\Assert;
|
|
|
|
final class DocBlockFactory implements DocBlockFactoryInterface
|
|
{
|
|
/** @var DocBlock\DescriptionFactory */
|
|
private $descriptionFactory;
|
|
|
|
/** @var DocBlock\TagFactory */
|
|
private $tagFactory;
|
|
|
|
/**
|
|
* Initializes this factory with the required subcontractors.
|
|
*
|
|
* @param DescriptionFactory $descriptionFactory
|
|
* @param TagFactory $tagFactory
|
|
*/
|
|
public function __construct(DescriptionFactory $descriptionFactory, TagFactory $tagFactory)
|
|
{
|
|
$this->descriptionFactory = $descriptionFactory;
|
|
$this->tagFactory = $tagFactory;
|
|
}
|
|
|
|
/**
|
|
* Factory method for easy instantiation.
|
|
*
|
|
* @param string[] $additionalTags
|
|
*
|
|
* @return DocBlockFactory
|
|
*/
|
|
public static function createInstance(array $additionalTags = [])
|
|
{
|
|
$fqsenResolver = new FqsenResolver();
|
|
$tagFactory = new StandardTagFactory($fqsenResolver);
|
|
$descriptionFactory = new DescriptionFactory($tagFactory);
|
|
|
|
$tagFactory->addService($descriptionFactory);
|
|
$tagFactory->addService(new TypeResolver($fqsenResolver));
|
|
|
|
$docBlockFactory = new self($descriptionFactory, $tagFactory);
|
|
foreach ($additionalTags as $tagName => $tagHandler) {
|
|
$docBlockFactory->registerTagHandler($tagName, $tagHandler);
|
|
}
|
|
|
|
return $docBlockFactory;
|
|
}
|
|
|
|
/**
|
|
* @param object|string $docblock A string containing the DocBlock to parse or an object supporting the
|
|
* getDocComment method (such as a ReflectionClass object).
|
|
* @param Types\Context $context
|
|
* @param Location $location
|
|
*
|
|
* @return DocBlock
|
|
*/
|
|
public function create($docblock, Types\Context $context = null, Location $location = null)
|
|
{
|
|
if (is_object($docblock)) {
|
|
if (!method_exists($docblock, 'getDocComment')) {
|
|
$exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method';
|
|
throw new \InvalidArgumentException($exceptionMessage);
|
|
}
|
|
|
|
$docblock = $docblock->getDocComment();
|
|
}
|
|
|
|
Assert::stringNotEmpty($docblock);
|
|
|
|
if ($context === null) {
|
|
$context = new Context('');
|
|
}
|
|
|
|
$parts = $this->splitDocBlock($this->stripDocComment($docblock));
|
|
list($templateMarker, $summary, $description, $tags) = $parts;
|
|
|
|
return new DocBlock(
|
|
$summary,
|
|
$description ? $this->descriptionFactory->create($description, $context) : null,
|
|
array_filter($this->parseTagBlock($tags, $context), function($tag) {
|
|
return $tag instanceof Tag;
|
|
}),
|
|
$context,
|
|
$location,
|
|
$templateMarker === '#@+',
|
|
$templateMarker === '#@-'
|
|
);
|
|
}
|
|
|
|
public function registerTagHandler($tagName, $handler)
|
|
{
|
|
$this->tagFactory->registerTagHandler($tagName, $handler);
|
|
}
|
|
|
|
/**
|
|
* Strips the asterisks from the DocBlock comment.
|
|
*
|
|
* @param string $comment String containing the comment text.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function stripDocComment($comment)
|
|
{
|
|
$comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment));
|
|
|
|
// reg ex above is not able to remove */ from a single line docblock
|
|
if (substr($comment, -2) == '*/') {
|
|
$comment = trim(substr($comment, 0, -2));
|
|
}
|
|
|
|
return str_replace(array("\r\n", "\r"), "\n", $comment);
|
|
}
|
|
|
|
/**
|
|
* Splits the DocBlock into a template marker, summary, description and block of tags.
|
|
*
|
|
* @param string $comment Comment to split into the sub-parts.
|
|
*
|
|
* @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split.
|
|
* @author Mike van Riel <[email protected]> for extending the regex with template marker support.
|
|
*
|
|
* @return string[] containing the template marker (if any), summary, description and a string containing the tags.
|
|
*/
|
|
private function splitDocBlock($comment)
|
|
{
|
|
// Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This
|
|
// method does not split tags so we return this verbatim as the fourth result (tags). This saves us the
|
|
// performance impact of running a regular expression
|
|
if (strpos($comment, '@') === 0) {
|
|
return array('', '', '', $comment);
|
|
}
|
|
|
|
// clears all extra horizontal whitespace from the line endings to prevent parsing issues
|
|
$comment = preg_replace('/\h*$/Sum', '', $comment);
|
|
|
|
/*
|
|
* Splits the docblock into a template marker, summary, description and tags section.
|
|
*
|
|
* - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may
|
|
* occur after it and will be stripped).
|
|
* - The short description is started from the first character until a dot is encountered followed by a
|
|
* newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing
|
|
* errors). This is optional.
|
|
* - The long description, any character until a new line is encountered followed by an @ and word
|
|
* characters (a tag). This is optional.
|
|
* - Tags; the remaining characters
|
|
*
|
|
* Big thanks to RichardJ for contributing this Regular Expression
|
|
*/
|
|
preg_match(
|
|
'/
|
|
\A
|
|
# 1. Extract the template marker
|
|
(?:(\#\@\+|\#\@\-)\n?)?
|
|
|
|
# 2. Extract the summary
|
|
(?:
|
|
(?! @\pL ) # The summary may not start with an @
|
|
(
|
|
[^\n.]+
|
|
(?:
|
|
(?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines
|
|
[\n.] (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line
|
|
[^\n.]+ # Include anything else
|
|
)*
|
|
\.?
|
|
)?
|
|
)
|
|
|
|
# 3. Extract the description
|
|
(?:
|
|
\s* # Some form of whitespace _must_ precede a description because a summary must be there
|
|
(?! @\pL ) # The description may not start with an @
|
|
(
|
|
[^\n]+
|
|
(?: \n+
|
|
(?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line
|
|
[^\n]+ # Include anything else
|
|
)*
|
|
)
|
|
)?
|
|
|
|
# 4. Extract the tags (anything that follows)
|
|
(\s+ [\s\S]*)? # everything that follows
|
|
/ux',
|
|
$comment,
|
|
$matches
|
|
);
|
|
array_shift($matches);
|
|
|
|
while (count($matches) < 4) {
|
|
$matches[] = '';
|
|
}
|
|
|
|
return $matches;
|
|
}
|
|
|
|
/**
|
|
* Creates the tag objects.
|
|
*
|
|
* @param string $tags Tag block to parse.
|
|
* @param Context $context Context of the parsed Tag
|
|
*
|
|
* @return DocBlock\Tag[]
|
|
*/
|
|
private function parseTagBlock($tags, Context $context)
|
|
{
|
|
$tags = $this->filterTagBlock($tags);
|
|
if (!$tags) {
|
|
return [];
|
|
}
|
|
|
|
$result = $this->splitTagBlockIntoTagLines($tags);
|
|
foreach ($result as $key => $tagLine) {
|
|
$result[$key] = $this->tagFactory->create(trim($tagLine), $context);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param string $tags
|
|
*
|
|
* @return string[]
|
|
*/
|
|
private function splitTagBlockIntoTagLines($tags)
|
|
{
|
|
$result = array();
|
|
foreach (explode("\n", $tags) as $tag_line) {
|
|
if (isset($tag_line[0]) && ($tag_line[0] === '@')) {
|
|
$result[] = $tag_line;
|
|
} else {
|
|
$result[count($result) - 1] .= "\n" . $tag_line;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param $tags
|
|
* @return string
|
|
*/
|
|
private function filterTagBlock($tags)
|
|
{
|
|
$tags = trim($tags);
|
|
if (!$tags) {
|
|
return null;
|
|
}
|
|
|
|
if ('@' !== $tags[0]) {
|
|
// @codeCoverageIgnoreStart
|
|
// Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that
|
|
// we didn't foresee.
|
|
throw new \LogicException('A tag block started with text instead of an at-sign(@): ' . $tags);
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
return $tags;
|
|
}
|
|
}
|