* DocBlock comment, and provide accessor methods for all of
* the elements that are needed to create an annotated Command.
*/
-abstract class AbstractCommandDocBlockParser
+class BespokeDocBlockParser
{
- /**
- * @var CommandInfo
- */
- protected $commandInfo;
-
- /**
- * @var \ReflectionMethod
- */
- protected $reflection;
-
- /**
- * @var string
- */
- protected $optionParamName;
+ protected $fqcnCache;
/**
* @var array
'command' => 'processCommandTag',
'name' => 'processCommandTag',
'arg' => 'processArgumentTag',
- 'param' => 'processParamTag',
+ 'param' => 'processArgumentTag',
'return' => 'processReturnTag',
'option' => 'processOptionTag',
'default' => 'processDefaultTag',
'desc' => 'processAlternateDescriptionTag',
];
- public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection)
+ public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection, $fqcnCache = null)
{
$this->commandInfo = $commandInfo;
$this->reflection = $reflection;
+ $this->fqcnCache = $fqcnCache ?: new FullyQualifiedClassCache();
}
- protected function processAllTags($phpdoc)
- {
- // Iterate over all of the tags, and process them as necessary.
- foreach ($phpdoc->getTags() as $tag) {
- $processFn = [$this, 'processGenericTag'];
- if (array_key_exists($tag->getName(), $this->tagProcessors)) {
- $processFn = [$this, $this->tagProcessors[$tag->getName()]];
- }
- $processFn($tag);
- }
- }
-
- abstract protected function getTagContents($tag);
-
/**
* Parse the docBlock comment for this command, and set the
* fields of this class with the data thereby obtained.
*/
- abstract public function parse();
+ public function parse()
+ {
+ $doc = $this->reflection->getDocComment();
+ $this->parseDocBlock($doc);
+ }
/**
* Save any tag that we do not explicitly recognize in the
*/
protected function processGenericTag($tag)
{
- $this->commandInfo->addAnnotation($tag->getName(), $this->getTagContents($tag));
+ $this->commandInfo->addAnnotation($tag->getTag(), $tag->getContent());
}
/**
*/
protected function processCommandTag($tag)
{
- $commandName = $this->getTagContents($tag);
+ if (!$tag->hasWordAndDescription($matches)) {
+ throw new \Exception('Could not determine command name from tag ' . (string)$tag);
+ }
+ $commandName = $matches['word'];
$this->commandInfo->setName($commandName);
// We also store the name in the 'other annotations' so that is is
// possible to determine if the method had a @command annotation.
- $this->commandInfo->addAnnotation($tag->getName(), $commandName);
+ $this->commandInfo->addAnnotation($tag->getTag(), $commandName);
}
/**
*/
protected function processAlternateDescriptionTag($tag)
{
- $this->commandInfo->setDescription($this->getTagContents($tag));
+ $this->commandInfo->setDescription($tag->getContent());
}
/**
*/
protected function processArgumentTag($tag)
{
- if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
+ if (!$tag->hasVariable($matches)) {
+ throw new \Exception('Could not determine argument name from tag ' . (string)$tag);
+ }
+ if ($matches['variable'] == $this->optionParamName()) {
return;
}
- $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match);
+ $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $matches['variable'], $matches['description']);
}
/**
*/
protected function processOptionTag($tag)
{
- if (!$this->pregMatchOptionNameAndDescription((string)$tag->getDescription(), $match)) {
- return;
+ if (!$tag->hasVariable($matches)) {
+ throw new \Exception('Could not determine option name from tag ' . (string)$tag);
}
- $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $match);
+ $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $matches['variable'], $matches['description']);
}
- protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $nameAndDescription)
+ protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $name, $description)
{
- $variableName = $this->commandInfo->findMatchingOption($nameAndDescription['name']);
- $desc = $nameAndDescription['description'];
- $description = static::removeLineBreaks($desc);
+ $variableName = $this->commandInfo->findMatchingOption($name);
+ $description = static::removeLineBreaks($description);
$set->add($variableName, $description);
}
*/
protected function processDefaultTag($tag)
{
- if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
- return;
+ if (!$tag->hasVariable($matches)) {
+ throw new \Exception('Could not determine parameter name for default value from tag ' . (string)$tag);
}
- $variableName = $match['name'];
- $defaultValue = $this->interpretDefaultValue($match['description']);
+ $variableName = $matches['variable'];
+ $defaultValue = $this->interpretDefaultValue($matches['description']);
if ($this->commandInfo->arguments()->exists($variableName)) {
$this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
return;
*/
protected function processUsageTag($tag)
{
- $lines = explode("\n", $this->getTagContents($tag));
- $usage = array_shift($lines);
- $description = static::removeLineBreaks(implode("\n", $lines));
+ $lines = explode("\n", $tag->getContent());
+ $usage = trim(array_shift($lines));
+ $description = static::removeLineBreaks(implode("\n", array_map(function ($line) {
+ return trim($line);
+ }, $lines)));
$this->commandInfo->setExampleUsage($usage, $description);
}
*/
protected function processAliases($tag)
{
- $this->commandInfo->setAliases((string)$tag->getDescription());
+ $this->commandInfo->setAliases((string)$tag->getContent());
+ }
+
+ /**
+ * Store the data from a @return annotation in our argument descriptions.
+ */
+ protected function processReturnTag($tag)
+ {
+ // The return type might be a variable -- '$this'. It will
+ // usually be a type, like RowsOfFields, or \Namespace\RowsOfFields.
+ if (!$tag->hasVariableAndDescription($matches)) {
+ throw new \Exception('Could not determine return type from tag ' . (string)$tag);
+ }
+ // Look at namespace and `use` statments to make returnType a fqdn
+ $returnType = $matches['variable'];
+ $returnType = $this->findFullyQualifiedClass($returnType);
+ $this->commandInfo->setReturnType($returnType);
+ }
+
+ protected function findFullyQualifiedClass($className)
+ {
+ if (strpos($className, '\\') !== false) {
+ return $className;
+ }
+
+ return $this->fqcnCache->qualify($this->reflection->getFileName(), $className);
+ }
+
+ private function parseDocBlock($doc)
+ {
+ // Remove the leading /** and the trailing */
+ $doc = preg_replace('#^\s*/\*+\s*#', '', $doc);
+ $doc = preg_replace('#\s*\*+/\s*#', '', $doc);
+
+ // Nothing left? Exit.
+ if (empty($doc)) {
+ return;
+ }
+
+ $tagFactory = new TagFactory();
+ $lines = [];
+
+ foreach (explode("\n", $doc) as $row) {
+ // Remove trailing whitespace and leading space + '*'s
+ $row = rtrim($row);
+ $row = preg_replace('#^[ \t]*\**#', '', $row);
+
+ if (!$tagFactory->parseLine($row)) {
+ $lines[] = $row;
+ }
+ }
+
+ $this->processDescriptionAndHelp($lines);
+ $this->processAllTags($tagFactory->getTags());
+ }
+
+ protected function processDescriptionAndHelp($lines)
+ {
+ // Trim all of the lines individually.
+ $lines =
+ array_map(
+ function ($line) {
+ return trim($line);
+ },
+ $lines
+ );
+
+ // Everything up to the first blank line goes in the description.
+ $description = array_shift($lines);
+ while ($this->nextLineIsNotEmpty($lines)) {
+ $description .= ' ' . array_shift($lines);
+ }
+
+ // Everything else goes in the help.
+ $help = trim(implode("\n", $lines));
+
+ $this->commandInfo->setDescription($description);
+ $this->commandInfo->setHelp($help);
+ }
+
+ protected function nextLineIsNotEmpty($lines)
+ {
+ if (empty($lines)) {
+ return false;
+ }
+
+ $nextLine = trim($lines[0]);
+ return !empty($nextLine);
+ }
+
+ protected function processAllTags($tags)
+ {
+ // Iterate over all of the tags, and process them as necessary.
+ foreach ($tags as $tag) {
+ $processFn = [$this, 'processGenericTag'];
+ if (array_key_exists($tag->getTag(), $this->tagProcessors)) {
+ $processFn = [$this, $this->tagProcessors[$tag->getTag()]];
+ }
+ $processFn($tag);
+ }
}
protected function lastParameterName()
return $this->optionParamName;
}
- /**
- * Store the data from a @param annotation in our argument descriptions.
- */
- protected function processParamTag($tag)
- {
- $variableName = $tag->getVariableName();
- $variableName = str_replace('$', '', $variableName);
- $description = static::removeLineBreaks((string)$tag->getDescription());
- if ($variableName == $this->optionParamName()) {
- return;
- }
- $this->commandInfo->arguments()->add($variableName, $description);
- }
-
- /**
- * Store the data from a @return annotation in our argument descriptions.
- */
- abstract protected function processReturnTag($tag);
-
protected function interpretDefaultValue($defaultValue)
{
$defaults = [
return $defaultValue;
}
- /**
- * Given a docblock description in the form "$variable description",
- * return the variable name and description via the 'match' parameter.
- */
- protected function pregMatchNameAndDescription($source, &$match)
- {
- $nameRegEx = '\\$(?P<name>[^ \t]+)[ \t]+';
- $descriptionRegEx = '(?P<description>.*)';
- $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
-
- return preg_match($optionRegEx, $source, $match);
- }
-
- /**
- * Given a docblock description in the form "$variable description",
- * return the variable name and description via the 'match' parameter.
- */
- protected function pregMatchOptionNameAndDescription($source, &$match)
- {
- // Strip type and $ from the text before the @option name, if present.
- $source = preg_replace('/^[a-zA-Z]* ?\\$/', '', $source);
- $nameRegEx = '(?P<name>[^ \t]+)[ \t]+';
- $descriptionRegEx = '(?P<description>.*)';
- $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
-
- return preg_match($optionRegEx, $source, $match);
- }
-
/**
* Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
* convert the data into the last of these forms.