Completed
Pull Request — master (#28)
by Greg
02:21
created

CommandDocBlockParser::processUsageTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 1
eloc 5
nc 1
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand\Parser;
3
4
use phpDocumentor\Reflection\DocBlock\Tag\ParamTag;
5
use phpDocumentor\Reflection\DocBlock\Tag\ReturnTag;
6
use phpDocumentor\Reflection\DocBlock;
7
8
/**
9
 * Given a class and method name, parse the annotations in the
10
 * DocBlock comment, and provide accessor methods for all of
11
 * the elements that are needed to create an annotated Command.
12
 */
13
class CommandDocBlockParser
14
{
15
    /**
16
     * @var CommandInfo
17
     */
18
    protected $commandInfo;
19
20
    /**
21
     * @var array
22
     */
23
    protected $tagProcessors = [
24
        'command' => 'processCommandTag',
25
        'name' => 'processCommandTag',
26
        'arg' => 'processArgumentTag',
27
        'param' => 'processParamTag',
28
        'return' => 'processReturnTag',
29
        'option' => 'processOptionTag',
30
        'default' => 'processDefaultTag',
31
        'aliases' => 'processAliases',
32
        'usage' => 'processUsageTag',
33
        'description' => 'processAlternateDescriptionTag',
34
        'desc' => 'processAlternateDescriptionTag',
35
    ];
36
37
    public function __construct(CommandInfo $commandInfo)
38
    {
39
        $this->commandInfo = $commandInfo;
40
    }
41
42
    /**
43
     * Parse the docBlock comment for this command, and set the
44
     * fields of this class with the data thereby obtained.
45
     */
46
    public function parse($docblock)
47
    {
48
        $phpdoc = new DocBlock($docblock);
49
50
        // First set the description (synopsis) and help.
51
        $this->commandInfo->setDescription((string)$phpdoc->getShortDescription());
52
        $this->commandInfo->setHelp((string)$phpdoc->getLongDescription());
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
    /**
65
     * Save any tag that we do not explicitly recognize in the
66
     * 'otherAnnotations' map.
67
     */
68
    protected function processGenericTag($tag)
69
    {
70
        $this->commandInfo->addOtherAnnotation($tag->getName(), $tag->getContent());
71
    }
72
73
    /**
74
     * Set the name of the command from a @command or @name annotation.
75
     */
76
    protected function processCommandTag($tag)
77
    {
78
        $this->commandInfo->setName($tag->getContent());
79
        // We also store the name in the 'other annotations' so that is is
80
        // possible to determine if the method had a @command annotation.
81
        $this->processGenericTag($tag);
82
    }
83
84
    /**
85
     * The @description and @desc annotations may be used in
86
     * place of the synopsis (which we call 'description').
87
     * This is discouraged.
88
     *
89
     * @deprecated
90
     */
91
    protected function processAlternateDescriptionTag($tag)
92
    {
93
        $this->commandInfo->setDescription($tag->getContent());
94
    }
95
96
    /**
97
     * Store the data from a @arg annotation in our argument descriptions.
98
     */
99
    protected function processArgumentTag($tag)
100
    {
101
        $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments());
102
    }
103
104
    /**
105
     * Store the data from a @param annotation in our argument descriptions.
106
     */
107
    protected function processParamTag($tag)
108
    {
109
        if (!$tag instanceof ParamTag) {
110
            return;
111
        }
112
        $variableName = $tag->getVariableName();
113
        $variableName = str_replace('$', '', $variableName);
114
        $description = static::removeLineBreaks($tag->getDescription());
115
        $this->commandInfo->arguments()->add($variableName, $description);
116
    }
117
118
    /**
119
     * Store the data from a @return annotation in our argument descriptions.
120
     */
121
    protected function processReturnTag($tag)
122
    {
123
        if (!$tag instanceof ReturnTag) {
124
            return;
125
        }
126
        $this->commandInfo->setReturnType($tag->getType());
127
    }
128
129
    /**
130
     * Given a docblock description in the form "$variable description",
131
     * return the variable name and description via the 'match' parameter.
132
     */
133
    protected function pregMatchNameAndDescription($source, &$match)
134
    {
135
        $nameRegEx = '\\$(?P<name>[^ \t]+)[ \t]+';
136
        $descriptionRegEx = '(?P<description>.*)';
137
        $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
138
139
        return preg_match($optionRegEx, $source, $match);
140
    }
141
142
    /**
143
     * Store the data from an @option annotation in our option descriptions.
144
     */
145
    protected function processOptionTag($tag)
146
    {
147
        $this->addOptionOrArgumentTag($tag, $this->commandInfo->options());
148
    }
149
150
    protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set)
151
    {
152
        if (!$this->pregMatchNameAndDescription($tag->getDescription(), $match)) {
153
            return;
154
        }
155
        $variableName = $this->commandInfo->findMatchingOption($match['name']);
156
        $desc = $match['description'];
157
        $description = static::removeLineBreaks($desc);
158
        $set->add($variableName, $description);
159
    }
160
161
    /**
162
     * Store the data from a @default annotation in our argument or option store,
163
     * as appropriate.
164
     */
165
    protected function processDefaultTag($tag)
166
    {
167
        if (!$this->pregMatchNameAndDescription($tag->getDescription(), $match)) {
168
            return;
169
        }
170
        $variableName = $match['name'];
171
        $defaultValue = $this->interpretDefaultValue($match['description']);
172
        if ($this->commandInfo->arguments()->exists($variableName)) {
173
            $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
174
            return;
175
        }
176
        $variableName = $this->commandInfo->findMatchingOption($variableName);
177
        if ($this->commandInfo->options()->exists($variableName)) {
178
            $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
179
        }
180
    }
181
182
    protected function interpretDefaultValue($defaultValue)
183
    {
184
        $defaults = [
185
            'null' => null,
186
            'true' => true,
187
            'false' => false,
188
            "''" => '',
189
            '[]' => [],
190
        ];
191
        foreach ($defaults as $defaultName => $defaultTypedValue) {
192
            if ($defaultValue == $defaultName) {
193
                return $defaultTypedValue;
194
            }
195
        }
196
        return $defaultValue;
197
    }
198
199
    /**
200
     * Process the comma-separated list of aliases
201
     */
202
    protected function processAliases($tag)
203
    {
204
        $this->commandInfo->setAliases($tag->getDescription());
205
    }
206
207
    /**
208
     * Store the data from a @usage annotation in our example usage list.
209
     */
210
    protected function processUsageTag($tag)
211
    {
212
        $lines = explode("\n", $tag->getContent());
213
        $usage = array_shift($lines);
214
        $description = static::removeLineBreaks(implode("\n", $lines));
215
216
        $this->commandInfo->setExampleUsage($usage, $description);
217
    }
218
219
    /**
220
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
221
     * convert the data into the last of these forms.
222
     */
223
    protected static function convertListToCommaSeparated($text)
224
    {
225
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
226
    }
227
228
    /**
229
     * Take a multiline description and convert it into a single
230
     * long unbroken line.
231
     */
232
    protected static function removeLineBreaks($text)
233
    {
234
        return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
235
    }
236
}
237