Completed
Push — develop ( 84bfed...2b68dc )
by Paul
02:42
created

DocumentationNodeParser::parse()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 38
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 33
nc 12
nop 2
dl 0
loc 38
rs 5.1612
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpUnitGen\Parser\NodeParser;
4
5
use PhpParser\Comment\Doc;
6
use PhpUnitGen\Annotation\AbstractAnnotation;
7
use PhpUnitGen\Annotation\AnnotationFactory;
8
use PhpUnitGen\Annotation\Lexer;
9
use PhpUnitGen\Exception\AnnotationParseException;
10
use PhpUnitGen\Exception\ParseException;
11
use PhpUnitGen\Model\ModelInterface\FunctionModelInterface;
12
13
/**
14
 * Class DocumentationNodeParser.
15
 *
16
 * @author     Paul Thébaud <[email protected]>.
17
 * @copyright  2017-2018 Paul Thébaud <[email protected]>.
18
 * @license    https://opensource.org/licenses/MIT The MIT license.
19
 * @link       https://github.com/paul-thebaud/phpunit-generator
20
 * @since      Class available since Release 2.0.0.
21
 */
22
class DocumentationNodeParser
23
{
24
    /**
25
     * @var Lexer $lexer The lexer to use.
26
     */
27
    private $lexer;
28
29
    /**
30
     * @var AnnotationFactory $annotationFactory The annotation factory to use.
31
     */
32
    private $annotationFactory;
33
34
    /**
35
     * @var AbstractAnnotation[] $parsedAnnotations The parsed annotation list.
36
     */
37
    private $parsedAnnotations;
38
39
    /**
40
     * @var int $currentLine The current line number.
41
     */
42
    private $currentLine;
43
44
    /**
45
     * @var AbstractAnnotation $currentAnnotation The current parsed annotation, null if none.
46
     */
47
    private $currentAnnotation;
48
49
    /**
50
     * @var string|null $currentAnnotationContent The current parsed annotation content, null if none.
51
     */
52
    private $currentAnnotationContent;
53
54
    /**
55
     * @var int|null $openedStringToken The opened string opening token identifier, null if no string opened.
56
     */
57
    private $openedStringToken;
58
59
    /**
60
     * @var bool $currentlyEscaping Tells if last parsed token was an escape token.
61
     */
62
    private $currentlyEscaping;
63
64
    /**
65
     * @var int $openedParenthesis The opened parenthesis number for an annotation content.
66
     */
67
    private $openedParenthesis;
68
69
    /**
70
     * DocumentationNodeParser constructor.
71
     *
72
     * @param Lexer             $lexer             The lexer to use.
73
     * @param AnnotationFactory $annotationFactory The annotation factory to use.
74
     */
75
    public function __construct(Lexer $lexer, AnnotationFactory $annotationFactory)
76
    {
77
        $this->lexer             = $lexer;
78
        $this->annotationFactory = $annotationFactory;
79
    }
80
81
    /**
82
     * Parse a node to update the parent node model.
83
     *
84
     * @param Doc                    $node   The node to parse.
85
     * @param FunctionModelInterface $parent The parent node.
86
     *
87
     * @return FunctionModelInterface The updated parent.
88
     *
89
     * @throws AnnotationParseException If an annotation is invalid.
90
     */
91
    public function invoke(Doc $node, FunctionModelInterface $parent): FunctionModelInterface
92
    {
93
        $documentation = $node->getText();
94
        $parent->setDocumentation($documentation);
95
96
        try {
97
            $this->initialize();
98
99
            $this->lexer->setInput($documentation);
100
            $this->lexer->moveNext();
101
            $this->lexer->moveNext();
102
103
            while ($this->lexer->token) {
104
                $this->parse(
105
                    $this->lexer->token['type'],
106
                    $this->lexer->token['value']
107
                );
108
                $this->lexer->moveNext();
109
                $this->lexer->moveNext();
110
            }
111
112
            if ($this->currentAnnotation !== null) {
113
                if ($this->currentAnnotationContent === null) {
114
                    $this->parsedAnnotations[] = $this->currentAnnotation;
115
                } else {
116
                    throw new AnnotationParseException(
117
                        'An annotation content is not closed (you probably forget to close a parenthesis or a quote)'
118
                    );
119
                }
120
            }
121
122
            foreach ($this->parsedAnnotations as $annotation) {
123
                $annotation->compile();
124
                /** @todo add to parent */
125
            }
126
        } catch (AnnotationParseException $exception) {
127
            throw new AnnotationParseException(
128
                sprintf('On function "%s": %s', $parent->getName(), $exception->getMessage())
129
            );
130
        }
131
132
        return $parent;
133
    }
134
135
    /**
136
     * Initialize the documentation parsing process.
137
     */
138
    private function initialize(): void
139
    {
140
        $this->parsedAnnotations = [];
141
        $this->currentLine = 0;
142
        $this->reset();
143
    }
144
145
    /**
146
     * Reset an annotation parsing.
147
     */
148
    private function reset(): void
149
    {
150
        $this->currentAnnotation        = null;
151
        $this->currentAnnotationContent = null;
152
        $this->openedStringToken        = null;
153
        $this->currentlyEscaping        = false;
154
155
        $this->openedParenthesis = 0;
156
    }
157
158
    /**
159
     * Parse a token with a value and type, and consume it depending on type.
160
     *
161
     * @param int    $type  The token type (an integer from the Lexer class constant).
162
     * @param string $value The token value.
163
     *
164
     * @throws AnnotationParseException If the token type is invalid.
165
     */
166
    private function parse(int $type, string $value): void
167
    {
168
        switch ($type) {
169
            case Lexer::T_ANNOTATION:
170
                $this->consumeAnnotationToken($value);
171
                break;
172
            case Lexer::T_O_PARENTHESIS:
173
                $this->addTokenToContent($value);
174
                $this->consumeOpeningParenthesisToken();
175
                break;
176
            case Lexer::T_C_PARENTHESIS:
177
                $this->consumeClosingParenthesisToken();
178
                $this->addTokenToContent($value);
179
                break;
180
            case Lexer::T_SINGLE_QUOTE:
181
            case Lexer::T_DOUBLE_QUOTE:
182
                $this->addTokenToContent($value);
183
                $this->consumeQuoteToken($type);
184
                break;
185
            case Lexer::T_ASTERISK:
186
                if ($this->openedStringToken !== null) {
187
                    // We are in a string, save this token value.
188
                    $this->addTokenToContent($value);
189
                }
190
                break;
191
            case Lexer::T_LINE_BREAK:
192
                $this->addTokenToContent($value);
193
                $this->currentLine++;
194
                break;
195
            case Lexer::T_BACKSLASH:
196
            case Lexer::T_WHITESPACE:
197
            case Lexer::T_OTHER:
198
                $this->addTokenToContent($value);
199
                break;
200
            default:
201
                throw new AnnotationParseException(sprintf('A token of value "%s" has an invalid type', $value));
202
        }
203
        $this->afterConsume($type);
204
    }
205
206
    private function addTokenToContent(string $value)
207
    {
208
        if ($this->currentAnnotationContent !== null) {
209
            $this->currentAnnotationContent .= $value;
210
        }
211
    }
212
213
    private function consumeAnnotationToken(string $value): void
214
    {
215
        if ($this->currentAnnotation === null) {
216
            // We are not in an annotation, build a new one.
217
            $this->currentAnnotation = $this->annotationFactory
218
                ->invoke($value, $this->currentLine);
219
        } else {
220
            if ($this->currentAnnotationContent === null) {
221
                // It is an annotation without content, save it and create the new one.
222
                $this->parsedAnnotations[] = $this->currentAnnotation;
223
                $this->reset();
224
                $this->currentAnnotation = $this->annotationFactory
225
                    ->invoke($value, $this->currentLine);
226
            } else {
227
                // An annotation content parsing is not finished.
228
                throw new AnnotationParseException(
229
                    'An annotation content is not closed (you probably forget to close a parenthesis or a quote)'
230
                );
231
            }
232
        }
233
    }
234
235
    private function consumeOpeningParenthesisToken(): void
236
    {
237
        if ($this->currentAnnotation !== null && $this->openedStringToken === null) {
238
            // We are in an annotation but not in a string, lets do something.
239
            if ($this->currentAnnotationContent === null) {
240
                if ($this->currentAnnotation->getLine() === $this->currentLine) {
241
                    // Begin content parsing only if it is on the same line.
242
                    $this->currentAnnotationContent = '';
243
                    $this->openedParenthesis++;
244
                }
245
            }
246
        }
247
    }
248
249
    private function consumeClosingParenthesisToken(): void
250
    {
251
        if ($this->currentAnnotationContent !== null && $this->openedStringToken === null) {
252
            // We are in an annotation content and not in a string.
253
            if ($this->openedParenthesis > 0) {
254
                $this->openedParenthesis--;
255
                if ($this->openedParenthesis === 0) {
256
                    // Annotation content is finished.
257
                    $this->currentAnnotation->setStringContent($this->currentAnnotationContent);
258
                    $this->parsedAnnotations[] = $this->currentAnnotation;
259
                    $this->reset();
260
                }
261
            }
262
        }
263
    }
264
265
    private function consumeQuoteToken(int $type): void
266
    {
267
        if ($this->currentAnnotationContent !== null) {
268
            if ($this->openedStringToken === null) {
269
                // It is a string opening.
270
                $this->openedStringToken = $type;
271
            } else {
272
                if (! $this->currentlyEscaping && $type === $this->openedStringToken) {
273
                    // We are in a string, the token is not escaped and is the same as the string opening token.
274
                    // Close the string.
275
                    $this->openedStringToken = null;
276
                }
277
            }
278
        }
279
    }
280
281
    /**
282
     * A method that is executed after consuming a token.
283
     *
284
     * @param int    $type  The token type (an integer from the Lexer class constant).
285
     */
286
    private function afterConsume(int $type): void
287
    {
288
        // Put in escaping mode if it were not escaping and there is a backslash token.
289
        if (! $this->currentlyEscaping && $type === Lexer::T_BACKSLASH) {
290
            $this->currentlyEscaping = true;
291
        } else {
292
            $this->currentlyEscaping = false;
293
        }
294
    }
295
}
296