Viewing file: Docblock.php (6.56 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
/* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */
namespace Psy\Util;
/** * A docblock representation. * * Based on PHP-DocBlock-Parser by Paul Scott: * * {@link http://www.github.com/icio/PHP-DocBlock-Parser} * * @author Paul Scott <paul@duedil.com> * @author Justin Hileman <justin@justinhileman.info> */ class Docblock { /** * Tags in the docblock that have a whitespace-delimited number of parameters * (such as `@param type var desc` and `@return type desc`) and the names of * those parameters. * * @var array */ public static $vectors = [ 'throws' => ['type', 'desc'], 'param' => ['type', 'var', 'desc'], 'return' => ['type', 'desc'], ];
protected $reflector;
/** * The description of the symbol. * * @var string */ public $desc;
/** * The tags defined in the docblock. * * The array has keys which are the tag names (excluding the @) and values * that are arrays, each of which is an entry for the tag. * * In the case where the tag name is defined in {@see DocBlock::$vectors} the * value within the tag-value array is an array in itself with keys as * described by {@see DocBlock::$vectors}. * * @var array */ public $tags;
/** * The entire DocBlock comment that was parsed. * * @var string */ public $comment;
/** * Docblock constructor. * * @param \Reflector $reflector */ public function __construct(\Reflector $reflector) { $this->reflector = $reflector;
if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionClassConstant || $reflector instanceof \ReflectionFunctionAbstract || $reflector instanceof \ReflectionProperty) { $this->setComment($reflector->getDocComment()); } }
/** * Set and parse the docblock comment. * * @param string $comment The docblock */ protected function setComment(string $comment) { $this->desc = ''; $this->tags = []; $this->comment = $comment;
$this->parseComment($comment); }
/** * Find the length of the docblock prefix. * * @param array $lines * * @return int Prefix length */ protected static function prefixLength(array $lines): int { // find only lines with interesting things $lines = \array_filter($lines, function ($line) { return \substr($line, \strspn($line, "* \t\n\r\0\x0B")); });
// if we sort the lines, we only have to compare two items \sort($lines);
$first = \reset($lines); $last = \end($lines);
// Special case for single-line comments if (\count($lines) === 1) { return \strspn($first, "* \t\n\r\0\x0B"); }
// find the longest common substring $count = \min(\strlen($first), \strlen($last)); for ($i = 0; $i < $count; $i++) { if ($first[$i] !== $last[$i]) { return $i; } }
return $count; }
/** * Parse the comment into the component parts and set the state of the object. * * @param string $comment The docblock */ protected function parseComment(string $comment) { // Strip the opening and closing tags of the docblock $comment = \substr($comment, 3, -2);
// Split into arrays of lines $comment = \array_filter(\preg_split('/\r?\n\r?/', $comment));
// Trim asterisks and whitespace from the beginning and whitespace from the end of lines $prefixLength = self::prefixLength($comment); $comment = \array_map(function ($line) use ($prefixLength) { return \rtrim(\substr($line, $prefixLength)); }, $comment);
// Group the lines together by @tags $blocks = []; $b = -1; foreach ($comment as $line) { if (self::isTagged($line)) { $b++; $blocks[] = []; } elseif ($b === -1) { $b = 0; $blocks[] = []; } $blocks[$b][] = $line; }
// Parse the blocks foreach ($blocks as $block => $body) { $body = \trim(\implode("\n", $body));
if ($block === 0 && !self::isTagged($body)) { // This is the description block $this->desc = $body; } else { // This block is tagged $tag = \substr(self::strTag($body), 1); $body = \ltrim(\substr($body, \strlen($tag) + 2));
if (isset(self::$vectors[$tag])) { // The tagged block is a vector $count = \count(self::$vectors[$tag]); if ($body) { $parts = \preg_split('/\s+/', $body, $count); } else { $parts = []; }
// Default the trailing values $parts = \array_pad($parts, $count, null);
// Store as a mapped array $this->tags[$tag][] = \array_combine(self::$vectors[$tag], $parts); } else { // The tagged block is only text $this->tags[$tag][] = $body; } } } }
/** * Whether or not a docblock contains a given @tag. * * @param string $tag The name of the @tag to check for */ public function hasTag(string $tag): bool { return \is_array($this->tags) && \array_key_exists($tag, $this->tags); }
/** * The value of a tag. * * @param string $tag * * @return array|null */ public function tag(string $tag): ?array { return $this->hasTag($tag) ? $this->tags[$tag] : null; }
/** * Whether or not a string begins with a @tag. * * @param string $str */ public static function isTagged(string $str): bool { return isset($str[1]) && $str[0] === '@' && !\preg_match('/[^A-Za-z]/', $str[1]); }
/** * The tag at the beginning of a string. * * @param string $str * * @return string|null */ public static function strTag(string $str) { if (\preg_match('/^@[a-z0-9_]+/', $str, $matches)) { return $matches[0]; } } }
|