2 namespace Consolidation\AnnotatedCommand\Parser\Internal;
4 use Consolidation\AnnotatedCommand\Parser\CommandInfo;
5 use Consolidation\AnnotatedCommand\Parser\DefaultsWithDescriptions;
8 * Given a class and method name, parse the annotations in the
9 * DocBlock comment, and provide accessor methods for all of
10 * the elements that are needed to create an annotated Command.
12 abstract class AbstractCommandDocBlockParser
17 protected $commandInfo;
20 * @var \ReflectionMethod
22 protected $reflection;
27 protected $optionParamName;
32 protected $tagProcessors = [
33 'command' => 'processCommandTag',
34 'name' => 'processCommandTag',
35 'arg' => 'processArgumentTag',
36 'param' => 'processParamTag',
37 'return' => 'processReturnTag',
38 'option' => 'processOptionTag',
39 'default' => 'processDefaultTag',
40 'aliases' => 'processAliases',
41 'usage' => 'processUsageTag',
42 'description' => 'processAlternateDescriptionTag',
43 'desc' => 'processAlternateDescriptionTag',
46 public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection)
48 $this->commandInfo = $commandInfo;
49 $this->reflection = $reflection;
52 protected function processAllTags($phpdoc)
54 // Iterate over all of the tags, and process them as necessary.
55 foreach ($phpdoc->getTags() as $tag) {
56 $processFn = [$this, 'processGenericTag'];
57 if (array_key_exists($tag->getName(), $this->tagProcessors)) {
58 $processFn = [$this, $this->tagProcessors[$tag->getName()]];
64 abstract protected function getTagContents($tag);
67 * Parse the docBlock comment for this command, and set the
68 * fields of this class with the data thereby obtained.
70 abstract public function parse();
73 * Save any tag that we do not explicitly recognize in the
74 * 'otherAnnotations' map.
76 protected function processGenericTag($tag)
78 $this->commandInfo->addAnnotation($tag->getName(), $this->getTagContents($tag));
82 * Set the name of the command from a @command or @name annotation.
84 protected function processCommandTag($tag)
86 $commandName = $this->getTagContents($tag);
87 $this->commandInfo->setName($commandName);
88 // We also store the name in the 'other annotations' so that is is
89 // possible to determine if the method had a @command annotation.
90 $this->commandInfo->addAnnotation($tag->getName(), $commandName);
94 * The @description and @desc annotations may be used in
95 * place of the synopsis (which we call 'description').
96 * This is discouraged.
100 protected function processAlternateDescriptionTag($tag)
102 $this->commandInfo->setDescription($this->getTagContents($tag));
106 * Store the data from a @arg annotation in our argument descriptions.
108 protected function processArgumentTag($tag)
110 if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
113 $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match);
117 * Store the data from an @option annotation in our option descriptions.
119 protected function processOptionTag($tag)
121 if (!$this->pregMatchOptionNameAndDescription((string)$tag->getDescription(), $match)) {
124 $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $match);
127 protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $nameAndDescription)
129 $variableName = $this->commandInfo->findMatchingOption($nameAndDescription['name']);
130 $desc = $nameAndDescription['description'];
131 $description = static::removeLineBreaks($desc);
132 $set->add($variableName, $description);
136 * Store the data from a @default annotation in our argument or option store,
139 protected function processDefaultTag($tag)
141 if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
144 $variableName = $match['name'];
145 $defaultValue = $this->interpretDefaultValue($match['description']);
146 if ($this->commandInfo->arguments()->exists($variableName)) {
147 $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
150 $variableName = $this->commandInfo->findMatchingOption($variableName);
151 if ($this->commandInfo->options()->exists($variableName)) {
152 $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
157 * Store the data from a @usage annotation in our example usage list.
159 protected function processUsageTag($tag)
161 $lines = explode("\n", $this->getTagContents($tag));
162 $usage = array_shift($lines);
163 $description = static::removeLineBreaks(implode("\n", $lines));
165 $this->commandInfo->setExampleUsage($usage, $description);
169 * Process the comma-separated list of aliases
171 protected function processAliases($tag)
173 $this->commandInfo->setAliases((string)$tag->getDescription());
176 protected function lastParameterName()
178 $params = $this->commandInfo->getParameters();
179 $param = end($params);
187 * Return the name of the last parameter if it holds the options.
189 public function optionParamName()
191 // Remember the name of the last parameter, if it holds the options.
192 // We will use this information to ignore @param annotations for the options.
193 if (!isset($this->optionParamName)) {
194 $this->optionParamName = '';
195 $options = $this->commandInfo->options();
196 if (!$options->isEmpty()) {
197 $this->optionParamName = $this->lastParameterName();
201 return $this->optionParamName;
205 * Store the data from a @param annotation in our argument descriptions.
207 protected function processParamTag($tag)
209 $variableName = $tag->getVariableName();
210 $variableName = str_replace('$', '', $variableName);
211 $description = static::removeLineBreaks((string)$tag->getDescription());
212 if ($variableName == $this->optionParamName()) {
215 $this->commandInfo->arguments()->add($variableName, $description);
219 * Store the data from a @return annotation in our argument descriptions.
221 abstract protected function processReturnTag($tag);
223 protected function interpretDefaultValue($defaultValue)
232 foreach ($defaults as $defaultName => $defaultTypedValue) {
233 if ($defaultValue == $defaultName) {
234 return $defaultTypedValue;
237 return $defaultValue;
241 * Given a docblock description in the form "$variable description",
242 * return the variable name and description via the 'match' parameter.
244 protected function pregMatchNameAndDescription($source, &$match)
246 $nameRegEx = '\\$(?P<name>[^ \t]+)[ \t]+';
247 $descriptionRegEx = '(?P<description>.*)';
248 $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
250 return preg_match($optionRegEx, $source, $match);
254 * Given a docblock description in the form "$variable description",
255 * return the variable name and description via the 'match' parameter.
257 protected function pregMatchOptionNameAndDescription($source, &$match)
259 // Strip type and $ from the text before the @option name, if present.
260 $source = preg_replace('/^[a-zA-Z]* ?\\$/', '', $source);
261 $nameRegEx = '(?P<name>[^ \t]+)[ \t]+';
262 $descriptionRegEx = '(?P<description>.*)';
263 $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
265 return preg_match($optionRegEx, $source, $match);
269 * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
270 * convert the data into the last of these forms.
272 protected static function convertListToCommaSeparated($text)
274 return preg_replace('#[ \t\n\r,]+#', ',', $text);
278 * Take a multiline description and convert it into a single
279 * long unbroken line.
281 protected static function removeLineBreaks($text)
283 return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));