Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / vendor / consolidation / annotated-command / src / Parser / Internal / BespokeDocBlockParser.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 class BespokeDocBlockParser
13 {
14     protected $fqcnCache;
15
16     /**
17      * @var array
18      */
19     protected $tagProcessors = [
20         'command' => 'processCommandTag',
21         'name' => 'processCommandTag',
22         'arg' => 'processArgumentTag',
23         'param' => 'processArgumentTag',
24         'return' => 'processReturnTag',
25         'option' => 'processOptionTag',
26         'default' => 'processDefaultTag',
27         'aliases' => 'processAliases',
28         'usage' => 'processUsageTag',
29         'description' => 'processAlternateDescriptionTag',
30         'desc' => 'processAlternateDescriptionTag',
31     ];
32
33     public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection, $fqcnCache = null)
34     {
35         $this->commandInfo = $commandInfo;
36         $this->reflection = $reflection;
37         $this->fqcnCache = $fqcnCache ?: new FullyQualifiedClassCache();
38     }
39
40     /**
41      * Parse the docBlock comment for this command, and set the
42      * fields of this class with the data thereby obtained.
43      */
44     public function parse()
45     {
46         $doc = $this->reflection->getDocComment();
47         $this->parseDocBlock($doc);
48     }
49
50     /**
51      * Save any tag that we do not explicitly recognize in the
52      * 'otherAnnotations' map.
53      */
54     protected function processGenericTag($tag)
55     {
56         $this->commandInfo->addAnnotation($tag->getTag(), $tag->getContent());
57     }
58
59     /**
60      * Set the name of the command from a @command or @name annotation.
61      */
62     protected function processCommandTag($tag)
63     {
64         if (!$tag->hasWordAndDescription($matches)) {
65             throw new \Exception('Could not determine command name from tag ' . (string)$tag);
66         }
67         $commandName = $matches['word'];
68         $this->commandInfo->setName($commandName);
69         // We also store the name in the 'other annotations' so that is is
70         // possible to determine if the method had a @command annotation.
71         $this->commandInfo->addAnnotation($tag->getTag(), $commandName);
72     }
73
74     /**
75      * The @description and @desc annotations may be used in
76      * place of the synopsis (which we call 'description').
77      * This is discouraged.
78      *
79      * @deprecated
80      */
81     protected function processAlternateDescriptionTag($tag)
82     {
83         $this->commandInfo->setDescription($tag->getContent());
84     }
85
86     /**
87      * Store the data from a @arg annotation in our argument descriptions.
88      */
89     protected function processArgumentTag($tag)
90     {
91         if (!$tag->hasVariable($matches)) {
92             throw new \Exception('Could not determine argument name from tag ' . (string)$tag);
93         }
94         if ($matches['variable'] == $this->optionParamName()) {
95             return;
96         }
97         $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $matches['variable'], $matches['description']);
98     }
99
100     /**
101      * Store the data from an @option annotation in our option descriptions.
102      */
103     protected function processOptionTag($tag)
104     {
105         if (!$tag->hasVariable($matches)) {
106             throw new \Exception('Could not determine option name from tag ' . (string)$tag);
107         }
108         $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $matches['variable'], $matches['description']);
109     }
110
111     protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $name, $description)
112     {
113         $variableName = $this->commandInfo->findMatchingOption($name);
114         $description = static::removeLineBreaks($description);
115         $set->add($variableName, $description);
116     }
117
118     /**
119      * Store the data from a @default annotation in our argument or option store,
120      * as appropriate.
121      */
122     protected function processDefaultTag($tag)
123     {
124         if (!$tag->hasVariable($matches)) {
125             throw new \Exception('Could not determine parameter name for default value from tag ' . (string)$tag);
126         }
127         $variableName = $matches['variable'];
128         $defaultValue = $this->interpretDefaultValue($matches['description']);
129         if ($this->commandInfo->arguments()->exists($variableName)) {
130             $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
131             return;
132         }
133         $variableName = $this->commandInfo->findMatchingOption($variableName);
134         if ($this->commandInfo->options()->exists($variableName)) {
135             $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
136         }
137     }
138
139     /**
140      * Store the data from a @usage annotation in our example usage list.
141      */
142     protected function processUsageTag($tag)
143     {
144         $lines = explode("\n", $tag->getContent());
145         $usage = trim(array_shift($lines));
146         $description = static::removeLineBreaks(implode("\n", array_map(function ($line) {
147             return trim($line);
148         }, $lines)));
149
150         $this->commandInfo->setExampleUsage($usage, $description);
151     }
152
153     /**
154      * Process the comma-separated list of aliases
155      */
156     protected function processAliases($tag)
157     {
158         $this->commandInfo->setAliases((string)$tag->getContent());
159     }
160
161     /**
162      * Store the data from a @return annotation in our argument descriptions.
163      */
164     protected function processReturnTag($tag)
165     {
166         // The return type might be a variable -- '$this'. It will
167         // usually be a type, like RowsOfFields, or \Namespace\RowsOfFields.
168         if (!$tag->hasVariableAndDescription($matches)) {
169             throw new \Exception('Could not determine return type from tag ' . (string)$tag);
170         }
171         // Look at namespace and `use` statments to make returnType a fqdn
172         $returnType = $matches['variable'];
173         $returnType = $this->findFullyQualifiedClass($returnType);
174         $this->commandInfo->setReturnType($returnType);
175     }
176
177     protected function findFullyQualifiedClass($className)
178     {
179         if (strpos($className, '\\') !== false) {
180             return $className;
181         }
182
183         return $this->fqcnCache->qualify($this->reflection->getFileName(), $className);
184     }
185
186     private function parseDocBlock($doc)
187     {
188         // Remove the leading /** and the trailing */
189         $doc = preg_replace('#^\s*/\*+\s*#', '', $doc);
190         $doc = preg_replace('#\s*\*+/\s*#', '', $doc);
191
192         // Nothing left? Exit.
193         if (empty($doc)) {
194             return;
195         }
196
197         $tagFactory = new TagFactory();
198         $lines = [];
199
200         foreach (explode("\n", $doc) as $row) {
201             // Remove trailing whitespace and leading space + '*'s
202             $row = rtrim($row);
203             $row = preg_replace('#^[ \t]*\**#', '', $row);
204
205             if (!$tagFactory->parseLine($row)) {
206                 $lines[] = $row;
207             }
208         }
209
210         $this->processDescriptionAndHelp($lines);
211         $this->processAllTags($tagFactory->getTags());
212     }
213
214     protected function processDescriptionAndHelp($lines)
215     {
216         // Trim all of the lines individually.
217         $lines =
218             array_map(
219                 function ($line) {
220                     return trim($line);
221                 },
222                 $lines
223             );
224
225         // Everything up to the first blank line goes in the description.
226         $description = array_shift($lines);
227         while ($this->nextLineIsNotEmpty($lines)) {
228             $description .= ' ' . array_shift($lines);
229         }
230
231         // Everything else goes in the help.
232         $help = trim(implode("\n", $lines));
233
234         $this->commandInfo->setDescription($description);
235         $this->commandInfo->setHelp($help);
236     }
237
238     protected function nextLineIsNotEmpty($lines)
239     {
240         if (empty($lines)) {
241             return false;
242         }
243
244         $nextLine = trim($lines[0]);
245         return !empty($nextLine);
246     }
247
248     protected function processAllTags($tags)
249     {
250         // Iterate over all of the tags, and process them as necessary.
251         foreach ($tags as $tag) {
252             $processFn = [$this, 'processGenericTag'];
253             if (array_key_exists($tag->getTag(), $this->tagProcessors)) {
254                 $processFn = [$this, $this->tagProcessors[$tag->getTag()]];
255             }
256             $processFn($tag);
257         }
258     }
259
260     protected function lastParameterName()
261     {
262         $params = $this->commandInfo->getParameters();
263         $param = end($params);
264         if (!$param) {
265             return '';
266         }
267         return $param->name;
268     }
269
270     /**
271      * Return the name of the last parameter if it holds the options.
272      */
273     public function optionParamName()
274     {
275         // Remember the name of the last parameter, if it holds the options.
276         // We will use this information to ignore @param annotations for the options.
277         if (!isset($this->optionParamName)) {
278             $this->optionParamName = '';
279             $options = $this->commandInfo->options();
280             if (!$options->isEmpty()) {
281                 $this->optionParamName = $this->lastParameterName();
282             }
283         }
284
285         return $this->optionParamName;
286     }
287
288     protected function interpretDefaultValue($defaultValue)
289     {
290         $defaults = [
291             'null' => null,
292             'true' => true,
293             'false' => false,
294             "''" => '',
295             '[]' => [],
296         ];
297         foreach ($defaults as $defaultName => $defaultTypedValue) {
298             if ($defaultValue == $defaultName) {
299                 return $defaultTypedValue;
300             }
301         }
302         return $defaultValue;
303     }
304
305     /**
306      * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
307      * convert the data into the last of these forms.
308      */
309     protected static function convertListToCommaSeparated($text)
310     {
311         return preg_replace('#[ \t\n\r,]+#', ',', $text);
312     }
313
314     /**
315      * Take a multiline description and convert it into a single
316      * long unbroken line.
317      */
318     protected static function removeLineBreaks($text)
319     {
320         return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
321     }
322 }