Security update for Core, with self-updated composer
[yaffs-website] / vendor / consolidation / annotated-command / src / Parser / CommandInfo.php
1 <?php
2 namespace Consolidation\AnnotatedCommand\Parser;
3
4 use Symfony\Component\Console\Input\InputOption;
5 use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParser;
6 use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParserFactory;
7 use Consolidation\AnnotatedCommand\AnnotationData;
8
9 /**
10  * Given a class and method name, parse the annotations in the
11  * DocBlock comment, and provide accessor methods for all of
12  * the elements that are needed to create a Symfony Console Command.
13  *
14  * Note that the name of this class is now somewhat of a misnomer,
15  * as we now use it to hold annotation data for hooks as well as commands.
16  * It would probably be better to rename this to MethodInfo at some point.
17  */
18 class CommandInfo
19 {
20     /**
21      * Serialization schema version. Incremented every time the serialization schema changes.
22      */
23     const SERIALIZATION_SCHEMA_VERSION = 3;
24
25     /**
26      * @var \ReflectionMethod
27      */
28     protected $reflection;
29
30     /**
31      * @var boolean
32      * @var string
33     */
34     protected $docBlockIsParsed = false;
35
36     /**
37      * @var string
38      */
39     protected $name;
40
41     /**
42      * @var string
43      */
44     protected $description = '';
45
46     /**
47      * @var string
48      */
49     protected $help = '';
50
51     /**
52      * @var DefaultsWithDescriptions
53      */
54     protected $options;
55
56     /**
57      * @var DefaultsWithDescriptions
58      */
59     protected $arguments;
60
61     /**
62      * @var array
63      */
64     protected $exampleUsage = [];
65
66     /**
67      * @var AnnotationData
68      */
69     protected $otherAnnotations;
70
71     /**
72      * @var array
73      */
74     protected $aliases = [];
75
76     /**
77      * @var InputOption[]
78      */
79     protected $inputOptions;
80
81     /**
82      * @var string
83      */
84     protected $methodName;
85
86     /**
87      * @var string
88      */
89     protected $returnType;
90
91     /**
92      * Create a new CommandInfo class for a particular method of a class.
93      *
94      * @param string|mixed $classNameOrInstance The name of a class, or an
95      *   instance of it, or an array of cached data.
96      * @param string $methodName The name of the method to get info about.
97      * @param array $cache Cached data
98      * @deprecated Use CommandInfo::create() or CommandInfo::deserialize()
99      *   instead. In the future, this constructor will be protected.
100      */
101     public function __construct($classNameOrInstance, $methodName, $cache = [])
102     {
103         $this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
104         $this->methodName = $methodName;
105         $this->arguments = new DefaultsWithDescriptions();
106         $this->options = new DefaultsWithDescriptions();
107
108         // If the cache came from a newer version, ignore it and
109         // regenerate the cached information.
110         if (!empty($cache) && CommandInfoDeserializer::isValidSerializedData($cache) && !$this->cachedFileIsModified($cache)) {
111             $deserializer = new CommandInfoDeserializer();
112             $deserializer->constructFromCache($this, $cache);
113             $this->docBlockIsParsed = true;
114         } else {
115             $this->constructFromClassAndMethod($classNameOrInstance, $methodName);
116         }
117     }
118
119     public static function create($classNameOrInstance, $methodName)
120     {
121         return new self($classNameOrInstance, $methodName);
122     }
123
124     public static function deserialize($cache)
125     {
126         $cache = (array)$cache;
127         return new self($cache['class'], $cache['method_name'], $cache);
128     }
129
130     public function cachedFileIsModified($cache)
131     {
132         $path = $this->reflection->getFileName();
133         return filemtime($path) != $cache['mtime'];
134     }
135
136     protected function constructFromClassAndMethod($classNameOrInstance, $methodName)
137     {
138         $this->otherAnnotations = new AnnotationData();
139         // Set up a default name for the command from the method name.
140         // This can be overridden via @command or @name annotations.
141         $this->name = $this->convertName($methodName);
142         $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
143         $this->arguments = $this->determineAgumentClassifications();
144     }
145
146     /**
147      * Recover the method name provided to the constructor.
148      *
149      * @return string
150      */
151     public function getMethodName()
152     {
153         return $this->methodName;
154     }
155
156     /**
157      * Return the primary name for this command.
158      *
159      * @return string
160      */
161     public function getName()
162     {
163         $this->parseDocBlock();
164         return $this->name;
165     }
166
167     /**
168      * Set the primary name for this command.
169      *
170      * @param string $name
171      */
172     public function setName($name)
173     {
174         $this->name = $name;
175         return $this;
176     }
177
178     /**
179      * Return whether or not this method represents a valid command
180      * or hook.
181      */
182     public function valid()
183     {
184         return !empty($this->name);
185     }
186
187     /**
188      * If higher-level code decides that this CommandInfo is not interesting
189      * or useful (if it is not a command method or a hook method), then
190      * we will mark it as invalid to prevent it from being created as a command.
191      * We still cache a placeholder record for invalid methods, so that we
192      * do not need to re-parse the method again later simply to determine that
193      * it is invalid.
194      */
195     public function invalidate()
196     {
197         $this->name = '';
198     }
199
200     public function getReturnType()
201     {
202         $this->parseDocBlock();
203         return $this->returnType;
204     }
205
206     public function setReturnType($returnType)
207     {
208         $this->returnType = $returnType;
209         return $this;
210     }
211
212     /**
213      * Get any annotations included in the docblock comment for the
214      * implementation method of this command that are not already
215      * handled by the primary methods of this class.
216      *
217      * @return AnnotationData
218      */
219     public function getRawAnnotations()
220     {
221         $this->parseDocBlock();
222         return $this->otherAnnotations;
223     }
224
225     /**
226      * Replace the annotation data.
227      */
228     public function replaceRawAnnotations($annotationData)
229     {
230         $this->otherAnnotations = new AnnotationData((array) $annotationData);
231         return $this;
232     }
233
234     /**
235      * Get any annotations included in the docblock comment,
236      * also including default values such as @command.  We add
237      * in the default @command annotation late, and only in a
238      * copy of the annotation data because we use the existance
239      * of a @command to indicate that this CommandInfo is
240      * a command, and not a hook or anything else.
241      *
242      * @return AnnotationData
243      */
244     public function getAnnotations()
245     {
246         // Also provide the path to the commandfile that these annotations
247         // were pulled from and the classname of that file.
248         $path = $this->reflection->getFileName();
249         $className = $this->reflection->getDeclaringClass()->getName();
250         return new AnnotationData(
251             $this->getRawAnnotations()->getArrayCopy() +
252             [
253                 'command' => $this->getName(),
254                 '_path' => $path,
255                 '_classname' => $className,
256             ]
257         );
258     }
259
260     /**
261      * Return a specific named annotation for this command as a list.
262      *
263      * @param string $name The name of the annotation.
264      * @return array|null
265      */
266     public function getAnnotationList($name)
267     {
268         // hasAnnotation parses the docblock
269         if (!$this->hasAnnotation($name)) {
270             return null;
271         }
272         return $this->otherAnnotations->getList($name);
273         ;
274     }
275
276     /**
277      * Return a specific named annotation for this command as a string.
278      *
279      * @param string $name The name of the annotation.
280      * @return string|null
281      */
282     public function getAnnotation($name)
283     {
284         // hasAnnotation parses the docblock
285         if (!$this->hasAnnotation($name)) {
286             return null;
287         }
288         return $this->otherAnnotations->get($name);
289     }
290
291     /**
292      * Check to see if the specified annotation exists for this command.
293      *
294      * @param string $annotation The name of the annotation.
295      * @return boolean
296      */
297     public function hasAnnotation($annotation)
298     {
299         $this->parseDocBlock();
300         return isset($this->otherAnnotations[$annotation]);
301     }
302
303     /**
304      * Save any tag that we do not explicitly recognize in the
305      * 'otherAnnotations' map.
306      */
307     public function addAnnotation($name, $content)
308     {
309         // Convert to an array and merge if there are multiple
310         // instances of the same annotation defined.
311         if (isset($this->otherAnnotations[$name])) {
312             $content = array_merge((array) $this->otherAnnotations[$name], (array)$content);
313         }
314         $this->otherAnnotations[$name] = $content;
315     }
316
317     /**
318      * Remove an annotation that was previoudly set.
319      */
320     public function removeAnnotation($name)
321     {
322         unset($this->otherAnnotations[$name]);
323     }
324
325     /**
326      * Get the synopsis of the command (~first line).
327      *
328      * @return string
329      */
330     public function getDescription()
331     {
332         $this->parseDocBlock();
333         return $this->description;
334     }
335
336     /**
337      * Set the command description.
338      *
339      * @param string $description The description to set.
340      */
341     public function setDescription($description)
342     {
343         $this->description = str_replace("\n", ' ', $description);
344         return $this;
345     }
346
347     /**
348      * Get the help text of the command (the description)
349      */
350     public function getHelp()
351     {
352         $this->parseDocBlock();
353         return $this->help;
354     }
355     /**
356      * Set the help text for this command.
357      *
358      * @param string $help The help text.
359      */
360     public function setHelp($help)
361     {
362         $this->help = $help;
363         return $this;
364     }
365
366     /**
367      * Return the list of aliases for this command.
368      * @return string[]
369      */
370     public function getAliases()
371     {
372         $this->parseDocBlock();
373         return $this->aliases;
374     }
375
376     /**
377      * Set aliases that can be used in place of the command's primary name.
378      *
379      * @param string|string[] $aliases
380      */
381     public function setAliases($aliases)
382     {
383         if (is_string($aliases)) {
384             $aliases = explode(',', static::convertListToCommaSeparated($aliases));
385         }
386         $this->aliases = array_filter($aliases);
387         return $this;
388     }
389
390     /**
391      * Get hidden status for the command.
392      * @return bool
393      */
394     public function getHidden()
395     {
396         $this->parseDocBlock();
397         return $this->hasAnnotation('hidden');
398     }
399
400     /**
401      * Set hidden status. List command omits hidden commands.
402      *
403      * @param bool $hidden
404      */
405     public function setHidden($hidden)
406     {
407         $this->hidden = $hidden;
408         return $this;
409     }
410
411     /**
412      * Return the examples for this command. This is @usage instead of
413      * @example because the later is defined by the phpdoc standard to
414      * be example method calls.
415      *
416      * @return string[]
417      */
418     public function getExampleUsages()
419     {
420         $this->parseDocBlock();
421         return $this->exampleUsage;
422     }
423
424     /**
425      * Add an example usage for this command.
426      *
427      * @param string $usage An example of the command, including the command
428      *   name and all of its example arguments and options.
429      * @param string $description An explanation of what the example does.
430      */
431     public function setExampleUsage($usage, $description)
432     {
433         $this->exampleUsage[$usage] = $description;
434         return $this;
435     }
436
437     /**
438      * Overwrite all example usages
439      */
440     public function replaceExampleUsages($usages)
441     {
442         $this->exampleUsage = $usages;
443         return $this;
444     }
445
446     /**
447      * Return the topics for this command.
448      *
449      * @return string[]
450      */
451     public function getTopics()
452     {
453         if (!$this->hasAnnotation('topics')) {
454             return [];
455         }
456         $topics = $this->getAnnotation('topics');
457         return explode(',', trim($topics));
458     }
459
460     /**
461      * Return the list of refleaction parameters.
462      *
463      * @return ReflectionParameter[]
464      */
465     public function getParameters()
466     {
467         return $this->reflection->getParameters();
468     }
469
470     /**
471      * Descriptions of commandline arguements for this command.
472      *
473      * @return DefaultsWithDescriptions
474      */
475     public function arguments()
476     {
477         return $this->arguments;
478     }
479
480     /**
481      * Descriptions of commandline options for this command.
482      *
483      * @return DefaultsWithDescriptions
484      */
485     public function options()
486     {
487         return $this->options;
488     }
489
490     /**
491      * Get the inputOptions for the options associated with this CommandInfo
492      * object, e.g. via @option annotations, or from
493      * $options = ['someoption' => 'defaultvalue'] in the command method
494      * parameter list.
495      *
496      * @return InputOption[]
497      */
498     public function inputOptions()
499     {
500         if (!isset($this->inputOptions)) {
501             $this->inputOptions = $this->createInputOptions();
502         }
503         return $this->inputOptions;
504     }
505
506     protected function addImplicitNoOptions()
507     {
508         $opts = $this->options()->getValues();
509         foreach ($opts as $name => $defaultValue) {
510             if ($defaultValue === true) {
511                 $key = 'no-' . $name;
512                 if (!array_key_exists($key, $opts)) {
513                     $description = "Negate --$name option.";
514                     $this->options()->add($key, $description, false);
515                 }
516             }
517         }
518     }
519
520     protected function createInputOptions()
521     {
522         $explicitOptions = [];
523         $this->addImplicitNoOptions();
524
525         $opts = $this->options()->getValues();
526         foreach ($opts as $name => $defaultValue) {
527             $description = $this->options()->getDescription($name);
528
529             $fullName = $name;
530             $shortcut = '';
531             if (strpos($name, '|')) {
532                 list($fullName, $shortcut) = explode('|', $name, 2);
533             }
534
535             // Treat the following two cases identically:
536             //   - 'foo' => InputOption::VALUE_OPTIONAL
537             //   - 'foo' => null
538             // The first form is preferred, but we will convert the value
539             // to 'null' for storage as the option default value.
540             if ($defaultValue === InputOption::VALUE_OPTIONAL) {
541                 $defaultValue = null;
542             }
543
544             if ($defaultValue === false) {
545                 $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
546             } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
547                 $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
548             } elseif (is_array($defaultValue)) {
549                 $optionality = count($defaultValue) ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
550                 $explicitOptions[$fullName] = new InputOption(
551                     $fullName,
552                     $shortcut,
553                     InputOption::VALUE_IS_ARRAY | $optionality,
554                     $description,
555                     count($defaultValue) ? $defaultValue : null
556                 );
557             } else {
558                 $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
559             }
560         }
561
562         return $explicitOptions;
563     }
564
565     /**
566      * An option might have a name such as 'silent|s'. In this
567      * instance, we will allow the @option or @default tag to
568      * reference the option only by name (e.g. 'silent' or 's'
569      * instead of 'silent|s').
570      *
571      * @param string $optionName
572      * @return string
573      */
574     public function findMatchingOption($optionName)
575     {
576         // Exit fast if there's an exact match
577         if ($this->options->exists($optionName)) {
578             return $optionName;
579         }
580         $existingOptionName = $this->findExistingOption($optionName);
581         if (isset($existingOptionName)) {
582             return $existingOptionName;
583         }
584         return $this->findOptionAmongAlternatives($optionName);
585     }
586
587     /**
588      * @param string $optionName
589      * @return string
590      */
591     protected function findOptionAmongAlternatives($optionName)
592     {
593         // Check the other direction: if the annotation contains @silent|s
594         // and the options array has 'silent|s'.
595         $checkMatching = explode('|', $optionName);
596         if (count($checkMatching) > 1) {
597             foreach ($checkMatching as $checkName) {
598                 if ($this->options->exists($checkName)) {
599                     $this->options->rename($checkName, $optionName);
600                     return $optionName;
601                 }
602             }
603         }
604         return $optionName;
605     }
606
607     /**
608      * @param string $optionName
609      * @return string|null
610      */
611     protected function findExistingOption($optionName)
612     {
613         // Check to see if we can find the option name in an existing option,
614         // e.g. if the options array has 'silent|s' => false, and the annotation
615         // is @silent.
616         foreach ($this->options()->getValues() as $name => $default) {
617             if (in_array($optionName, explode('|', $name))) {
618                 return $name;
619             }
620         }
621     }
622
623     /**
624      * Examine the parameters of the method for this command, and
625      * build a list of commandline arguements for them.
626      *
627      * @return array
628      */
629     protected function determineAgumentClassifications()
630     {
631         $result = new DefaultsWithDescriptions();
632         $params = $this->reflection->getParameters();
633         $optionsFromParameters = $this->determineOptionsFromParameters();
634         if ($this->lastParameterIsOptionsArray()) {
635             array_pop($params);
636         }
637         foreach ($params as $param) {
638             $this->addParameterToResult($result, $param);
639         }
640         return $result;
641     }
642
643     /**
644      * Examine the provided parameter, and determine whether it
645      * is a parameter that will be filled in with a positional
646      * commandline argument.
647      */
648     protected function addParameterToResult($result, $param)
649     {
650         // Commandline arguments must be strings, so ignore any
651         // parameter that is typehinted to any non-primative class.
652         if ($param->getClass() != null) {
653             return;
654         }
655         $result->add($param->name);
656         if ($param->isDefaultValueAvailable()) {
657             $defaultValue = $param->getDefaultValue();
658             if (!$this->isAssoc($defaultValue)) {
659                 $result->setDefaultValue($param->name, $defaultValue);
660             }
661         } elseif ($param->isArray()) {
662             $result->setDefaultValue($param->name, []);
663         }
664     }
665
666     /**
667      * Examine the parameters of the method for this command, and determine
668      * the disposition of the options from them.
669      *
670      * @return array
671      */
672     protected function determineOptionsFromParameters()
673     {
674         $params = $this->reflection->getParameters();
675         if (empty($params)) {
676             return [];
677         }
678         $param = end($params);
679         if (!$param->isDefaultValueAvailable()) {
680             return [];
681         }
682         if (!$this->isAssoc($param->getDefaultValue())) {
683             return [];
684         }
685         return $param->getDefaultValue();
686     }
687
688     /**
689      * Determine if the last argument contains $options.
690      *
691      * Two forms indicate options:
692      * - $options = []
693      * - $options = ['flag' => 'default-value']
694      *
695      * Any other form, including `array $foo`, is not options.
696      */
697     protected function lastParameterIsOptionsArray()
698     {
699         $params = $this->reflection->getParameters();
700         if (empty($params)) {
701             return [];
702         }
703         $param = end($params);
704         if (!$param->isDefaultValueAvailable()) {
705             return [];
706         }
707         return is_array($param->getDefaultValue());
708     }
709
710     /**
711      * Helper; determine if an array is associative or not. An array
712      * is not associative if its keys are numeric, and numbered sequentially
713      * from zero. All other arrays are considered to be associative.
714      *
715      * @param array $arr The array
716      * @return boolean
717      */
718     protected function isAssoc($arr)
719     {
720         if (!is_array($arr)) {
721             return false;
722         }
723         return array_keys($arr) !== range(0, count($arr) - 1);
724     }
725
726     /**
727      * Convert from a method name to the corresponding command name. A
728      * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
729      * become 'foo:bar-baz-boz'.
730      *
731      * @param string $camel method name.
732      * @return string
733      */
734     protected function convertName($camel)
735     {
736         $splitter="-";
737         $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
738         $camel = preg_replace("/$splitter/", ':', $camel, 1);
739         return strtolower($camel);
740     }
741
742     /**
743      * Parse the docBlock comment for this command, and set the
744      * fields of this class with the data thereby obtained.
745      */
746     protected function parseDocBlock()
747     {
748         if (!$this->docBlockIsParsed) {
749             // The parse function will insert data from the provided method
750             // into this object, using our accessors.
751             CommandDocBlockParserFactory::parse($this, $this->reflection);
752             $this->docBlockIsParsed = true;
753         }
754     }
755
756     /**
757      * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
758      * convert the data into the last of these forms.
759      */
760     protected static function convertListToCommaSeparated($text)
761     {
762         return preg_replace('#[ \t\n\r,]+#', ',', $text);
763     }
764 }