More tidying.
[yaffs-website] / vendor / consolidation / annotated-command / src / Parser / Internal / AbstractCommandDocBlockParser.php
1 <?php
2 namespace Consolidation\AnnotatedCommand\Parser\Internal;
3
4 use Consolidation\AnnotatedCommand\Parser\CommandInfo;
5 use Consolidation\AnnotatedCommand\Parser\DefaultsWithDescriptions;
6
7 /**
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.
11  */
12 abstract class AbstractCommandDocBlockParser
13 {
14     /**
15      * @var CommandInfo
16      */
17     protected $commandInfo;
18
19     /**
20      * @var \ReflectionMethod
21      */
22     protected $reflection;
23
24     /**
25      * @var string
26      */
27     protected $optionParamName;
28
29     /**
30      * @var array
31      */
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',
44     ];
45
46     public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection)
47     {
48         $this->commandInfo = $commandInfo;
49         $this->reflection = $reflection;
50     }
51
52     protected function processAllTags($phpdoc)
53     {
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()]];
59             }
60             $processFn($tag);
61         }
62     }
63
64     abstract protected function getTagContents($tag);
65
66     /**
67      * Parse the docBlock comment for this command, and set the
68      * fields of this class with the data thereby obtained.
69      */
70     abstract public function parse();
71
72     /**
73      * Save any tag that we do not explicitly recognize in the
74      * 'otherAnnotations' map.
75      */
76     protected function processGenericTag($tag)
77     {
78         $this->commandInfo->addAnnotation($tag->getName(), $this->getTagContents($tag));
79     }
80
81     /**
82      * Set the name of the command from a @command or @name annotation.
83      */
84     protected function processCommandTag($tag)
85     {
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);
91     }
92
93     /**
94      * The @description and @desc annotations may be used in
95      * place of the synopsis (which we call 'description').
96      * This is discouraged.
97      *
98      * @deprecated
99      */
100     protected function processAlternateDescriptionTag($tag)
101     {
102         $this->commandInfo->setDescription($this->getTagContents($tag));
103     }
104
105     /**
106      * Store the data from a @arg annotation in our argument descriptions.
107      */
108     protected function processArgumentTag($tag)
109     {
110         if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
111             return;
112         }
113         $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match);
114     }
115
116     /**
117      * Store the data from an @option annotation in our option descriptions.
118      */
119     protected function processOptionTag($tag)
120     {
121         if (!$this->pregMatchOptionNameAndDescription((string)$tag->getDescription(), $match)) {
122             return;
123         }
124         $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $match);
125     }
126
127     protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $nameAndDescription)
128     {
129         $variableName = $this->commandInfo->findMatchingOption($nameAndDescription['name']);
130         $desc = $nameAndDescription['description'];
131         $description = static::removeLineBreaks($desc);
132         $set->add($variableName, $description);
133     }
134
135     /**
136      * Store the data from a @default annotation in our argument or option store,
137      * as appropriate.
138      */
139     protected function processDefaultTag($tag)
140     {
141         if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
142             return;
143         }
144         $variableName = $match['name'];
145         $defaultValue = $this->interpretDefaultValue($match['description']);
146         if ($this->commandInfo->arguments()->exists($variableName)) {
147             $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
148             return;
149         }
150         $variableName = $this->commandInfo->findMatchingOption($variableName);
151         if ($this->commandInfo->options()->exists($variableName)) {
152             $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
153         }
154     }
155
156     /**
157      * Store the data from a @usage annotation in our example usage list.
158      */
159     protected function processUsageTag($tag)
160     {
161         $lines = explode("\n", $this->getTagContents($tag));
162         $usage = array_shift($lines);
163         $description = static::removeLineBreaks(implode("\n", $lines));
164
165         $this->commandInfo->setExampleUsage($usage, $description);
166     }
167
168     /**
169      * Process the comma-separated list of aliases
170      */
171     protected function processAliases($tag)
172     {
173         $this->commandInfo->setAliases((string)$tag->getDescription());
174     }
175
176     protected function lastParameterName()
177     {
178         $params = $this->commandInfo->getParameters();
179         $param = end($params);
180         if (!$param) {
181             return '';
182         }
183         return $param->name;
184     }
185
186     /**
187      * Return the name of the last parameter if it holds the options.
188      */
189     public function optionParamName()
190     {
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();
198             }
199         }
200
201         return $this->optionParamName;
202     }
203
204     /**
205      * Store the data from a @param annotation in our argument descriptions.
206      */
207     protected function processParamTag($tag)
208     {
209         $variableName = $tag->getVariableName();
210         $variableName = str_replace('$', '', $variableName);
211         $description = static::removeLineBreaks((string)$tag->getDescription());
212         if ($variableName == $this->optionParamName()) {
213             return;
214         }
215         $this->commandInfo->arguments()->add($variableName, $description);
216     }
217
218     /**
219      * Store the data from a @return annotation in our argument descriptions.
220      */
221     abstract protected function processReturnTag($tag);
222
223     protected function interpretDefaultValue($defaultValue)
224     {
225         $defaults = [
226             'null' => null,
227             'true' => true,
228             'false' => false,
229             "''" => '',
230             '[]' => [],
231         ];
232         foreach ($defaults as $defaultName => $defaultTypedValue) {
233             if ($defaultValue == $defaultName) {
234                 return $defaultTypedValue;
235             }
236         }
237         return $defaultValue;
238     }
239
240     /**
241      * Given a docblock description in the form "$variable description",
242      * return the variable name and description via the 'match' parameter.
243      */
244     protected function pregMatchNameAndDescription($source, &$match)
245     {
246         $nameRegEx = '\\$(?P<name>[^ \t]+)[ \t]+';
247         $descriptionRegEx = '(?P<description>.*)';
248         $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
249
250         return preg_match($optionRegEx, $source, $match);
251     }
252
253     /**
254      * Given a docblock description in the form "$variable description",
255      * return the variable name and description via the 'match' parameter.
256      */
257     protected function pregMatchOptionNameAndDescription($source, &$match)
258     {
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";
264
265         return preg_match($optionRegEx, $source, $match);
266     }
267
268     /**
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.
271      */
272     protected static function convertListToCommaSeparated($text)
273     {
274         return preg_replace('#[ \t\n\r,]+#', ',', $text);
275     }
276
277     /**
278      * Take a multiline description and convert it into a single
279      * long unbroken line.
280      */
281     protected static function removeLineBreaks($text)
282     {
283         return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
284     }
285 }