annotate vendor/consolidation/annotated-command/src/Parser/Internal/BespokeDocBlockParser.php @ 0:4c8ae668cc8c

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