Completed
Pull Request — master (#46)
by Greg
02:15
created

AbstractCommandDocBlockParser   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 0
Metric Value
wmc 27
lcom 2
cbo 2
dl 0
loc 241
rs 10
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A processAllTags() 0 11 3
getTagContents() 0 1 ?
parse() 0 1 ?
A processGenericTag() 0 4 1
A processCommandTag() 0 8 1
A processAlternateDescriptionTag() 0 4 1
A processArgumentTag() 0 7 2
A processOptionTag() 0 7 2
A addOptionOrArgumentTag() 0 7 1
A processDefaultTag() 0 16 4
A processUsageTag() 0 8 1
A processAliases() 0 4 1
A processParamTag() 0 10 2
processReturnTag() 0 1 ?
A interpretDefaultValue() 0 16 3
A pregMatchNameAndDescription() 0 8 1
A pregMatchOptionNameAndDescription() 0 10 1
A convertListToCommaSeparated() 0 4 1
A removeLineBreaks() 0 4 1
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 array
26
     */
27
    protected $tagProcessors = [
28
        'command' => 'processCommandTag',
29
        'name' => 'processCommandTag',
30
        'arg' => 'processArgumentTag',
31
        'param' => 'processParamTag',
32
        'return' => 'processReturnTag',
33
        'option' => 'processOptionTag',
34
        'default' => 'processDefaultTag',
35
        'aliases' => 'processAliases',
36
        'usage' => 'processUsageTag',
37
        'description' => 'processAlternateDescriptionTag',
38
        'desc' => 'processAlternateDescriptionTag',
39
    ];
40
41
    public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection)
42
    {
43
        $this->commandInfo = $commandInfo;
44
        $this->reflection = $reflection;
45
    }
46
47
    protected function processAllTags($phpdoc)
48
    {
49
        // Iterate over all of the tags, and process them as necessary.
50
        foreach ($phpdoc->getTags() as $tag) {
51
            $processFn = [$this, 'processGenericTag'];
52
            if (array_key_exists($tag->getName(), $this->tagProcessors)) {
53
                $processFn = [$this, $this->tagProcessors[$tag->getName()]];
54
            }
55
            $processFn($tag);
56
        }
57
    }
58
59
    abstract protected function getTagContents($tag);
60
61
    /**
62
     * Parse the docBlock comment for this command, and set the
63
     * fields of this class with the data thereby obtained.
64
     */
65
    abstract public function parse();
66
67
    /**
68
     * Save any tag that we do not explicitly recognize in the
69
     * 'otherAnnotations' map.
70
     */
71
    protected function processGenericTag($tag)
72
    {
73
        $this->commandInfo->addOtherAnnotation($tag->getName(), $this->getTagContents($tag));
74
    }
75
76
    /**
77
     * Set the name of the command from a @command or @name annotation.
78
     */
79
    protected function processCommandTag($tag)
80
    {
81
        $commandName = $this->getTagContents($tag);
82
        $this->commandInfo->setName($commandName);
83
        // We also store the name in the 'other annotations' so that is is
84
        // possible to determine if the method had a @command annotation.
85
        $this->commandInfo->addOtherAnnotation($tag->getName(), $commandName);
86
    }
87
88
    /**
89
     * The @description and @desc annotations may be used in
90
     * place of the synopsis (which we call 'description').
91
     * This is discouraged.
92
     *
93
     * @deprecated
94
     */
95
    protected function processAlternateDescriptionTag($tag)
96
    {
97
        $this->commandInfo->setDescription($this->getTagContents($tag));
98
    }
99
100
    /**
101
     * Store the data from a @arg annotation in our argument descriptions.
102
     */
103
    protected function processArgumentTag($tag)
104
    {
105
        if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
106
            return;
107
        }
108
        $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match);
109
    }
110
111
    /**
112
     * Store the data from an @option annotation in our option descriptions.
113
     */
114
    protected function processOptionTag($tag)
115
    {
116
        if (!$this->pregMatchOptionNameAndDescription((string)$tag->getDescription(), $match)) {
117
            return;
118
        }
119
        $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $match);
120
    }
121
122
    protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $nameAndDescription)
0 ignored issues
show
Unused Code introduced by
The parameter $tag is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
123
    {
124
        $variableName = $this->commandInfo->findMatchingOption($nameAndDescription['name']);
125
        $desc = $nameAndDescription['description'];
126
        $description = static::removeLineBreaks($desc);
127
        $set->add($variableName, $description);
128
    }
129
130
    /**
131
     * Store the data from a @default annotation in our argument or option store,
132
     * as appropriate.
133
     */
134
    protected function processDefaultTag($tag)
135
    {
136
        if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) {
137
            return;
138
        }
139
        $variableName = $match['name'];
140
        $defaultValue = $this->interpretDefaultValue($match['description']);
141
        if ($this->commandInfo->arguments()->exists($variableName)) {
142
            $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue);
143
            return;
144
        }
145
        $variableName = $this->commandInfo->findMatchingOption($variableName);
146
        if ($this->commandInfo->options()->exists($variableName)) {
147
            $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue);
148
        }
149
    }
150
151
    /**
152
     * Store the data from a @usage annotation in our example usage list.
153
     */
154
    protected function processUsageTag($tag)
155
    {
156
        $lines = explode("\n", $this->getTagContents($tag));
157
        $usage = array_shift($lines);
158
        $description = static::removeLineBreaks(implode("\n", $lines));
159
160
        $this->commandInfo->setExampleUsage($usage, $description);
161
    }
162
163
    /**
164
     * Process the comma-separated list of aliases
165
     */
166
    protected function processAliases($tag)
167
    {
168
        $this->commandInfo->setAliases((string)$tag->getDescription());
169
    }
170
171
    /**
172
     * Store the data from a @param annotation in our argument descriptions.
173
     */
174
    protected function processParamTag($tag)
175
    {
176
        $variableName = $tag->getVariableName();
177
        $variableName = str_replace('$', '', $variableName);
178
        $description = static::removeLineBreaks((string)$tag->getDescription());
179
        if ($variableName == $this->commandInfo->optionParamName()) {
180
            return;
181
        }
182
        $this->commandInfo->arguments()->add($variableName, $description);
183
    }
184
185
    /**
186
     * Store the data from a @return annotation in our argument descriptions.
187
     */
188
    abstract protected function processReturnTag($tag);
189
190
    protected function interpretDefaultValue($defaultValue)
191
    {
192
        $defaults = [
193
            'null' => null,
194
            'true' => true,
195
            'false' => false,
196
            "''" => '',
197
            '[]' => [],
198
        ];
199
        foreach ($defaults as $defaultName => $defaultTypedValue) {
200
            if ($defaultValue == $defaultName) {
201
                return $defaultTypedValue;
202
            }
203
        }
204
        return $defaultValue;
205
    }
206
207
    /**
208
     * Given a docblock description in the form "$variable description",
209
     * return the variable name and description via the 'match' parameter.
210
     */
211
    protected function pregMatchNameAndDescription($source, &$match)
212
    {
213
        $nameRegEx = '\\$(?P<name>[^ \t]+)[ \t]+';
214
        $descriptionRegEx = '(?P<description>.*)';
215
        $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
216
217
        return preg_match($optionRegEx, $source, $match);
218
    }
219
220
    /**
221
     * Given a docblock description in the form "$variable description",
222
     * return the variable name and description via the 'match' parameter.
223
     */
224
    protected function pregMatchOptionNameAndDescription($source, &$match)
225
    {
226
        // Strip type and $ from the text before the @option name, if present.
227
        $source = preg_replace('/^[a-zA-Z]* ?\\$/', '', $source);
228
        $nameRegEx = '(?P<name>[^ \t]+)[ \t]+';
229
        $descriptionRegEx = '(?P<description>.*)';
230
        $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s";
231
232
        return preg_match($optionRegEx, $source, $match);
233
    }
234
235
    /**
236
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
237
     * convert the data into the last of these forms.
238
     */
239
    protected static function convertListToCommaSeparated($text)
240
    {
241
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
242
    }
243
244
    /**
245
     * Take a multiline description and convert it into a single
246
     * long unbroken line.
247
     */
248
    protected static function removeLineBreaks($text)
249
    {
250
        return trim(preg_replace('#[ \t\n\r]+#', ' ', $text));
251
    }
252
}
253