Passed
Push — develop ( 4c7678...74dbd7 )
by Paul
02:25
created

DocumentationNodeParser::saveFunctionAnnotation()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 2
nop 2
dl 0
loc 14
rs 8.8571
c 0
b 0
f 0
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\AnnotationInterface\AnnotationInterface;
9
use PhpUnitGen\Annotation\Lexer;
10
use PhpUnitGen\Exception\AnnotationParseException;
11
use PhpUnitGen\Model\ModelInterface\FunctionModelInterface;
12
use PhpUnitGen\Model\PropertyInterface\ClassLikeInterface;
13
use PhpUnitGen\Model\PropertyInterface\DocumentationInterface;
14
15
/**
16
 * Class DocumentationNodeParser.
17
 *
18
 * @author     Paul Thébaud <[email protected]>.
19
 * @copyright  2017-2018 Paul Thébaud <[email protected]>.
20
 * @license    https://opensource.org/licenses/MIT The MIT license.
21
 * @link       https://github.com/paul-thebaud/phpunit-generator
22
 * @since      Class available since Release 2.0.0.
23
 */
24
class DocumentationNodeParser
25
{
26
    /**
27
     * @var Lexer $lexer The lexer to use.
28
     */
29
    private $lexer;
30
31
    /**
32
     * @var AnnotationFactory $annotationFactory The annotation factory to use.
33
     */
34
    private $annotationFactory;
35
36
    /**
37
     * @var AbstractAnnotation[] $parsedAnnotations The parsed annotation list.
38
     */
39
    private $parsedAnnotations;
40
41
    /**
42
     * @var int $currentLine The current line number.
43
     */
44
    private $currentLine;
45
46
    /**
47
     * @var AbstractAnnotation $currentAnnotation The current parsed annotation, null if none.
48
     */
49
    private $currentAnnotation;
50
51
    /**
52
     * @var string|null $currentAnnotationContent The current parsed annotation content, null if none.
53
     */
54
    private $currentAnnotationContent;
55
56
    /**
57
     * @var int|null $openedStringToken The opened string opening token identifier, null if no string opened.
58
     */
59
    private $openedStringToken;
60
61
    /**
62
     * @var bool $currentlyEscaping Tells if last parsed token was an escape token.
63
     */
64
    private $currentlyEscaping;
65
66
    /**
67
     * @var int $openedParenthesis The opened parenthesis number for an annotation content.
68
     */
69
    private $openedParenthesis;
70
71
    /**
72
     * DocumentationNodeParser constructor.
73
     *
74
     * @param Lexer             $lexer             The lexer to use.
75
     * @param AnnotationFactory $annotationFactory The annotation factory to use.
76
     */
77
    public function __construct(Lexer $lexer, AnnotationFactory $annotationFactory)
78
    {
79
        $this->lexer             = $lexer;
80
        $this->annotationFactory = $annotationFactory;
81
    }
82
83
    /**
84
     * Parse a node to update the parent node model.
85
     *
86
     * @param Doc                    $node   The node to parse.
87
     * @param DocumentationInterface $parent The parent node.
88
     *
89
     * @return DocumentationInterface The updated parent.
90
     *
91
     * @throws AnnotationParseException If an annotation is invalid.
92
     */
93
    public function invoke(Doc $node, DocumentationInterface $parent): DocumentationInterface
94
    {
95
        $documentation = $node->getText();
96
        $parent->setDocumentation($documentation);
97
98
        try {
99
            $this->initialize();
100
101
            $this->lexer->setInput($documentation);
102
            $this->lexer->moveNext();
103
            $this->lexer->moveNext();
104
105
            while ($this->lexer->token) {
106
                $this->parse(
107
                    $this->lexer->token['type'],
108
                    $this->lexer->token['value']
109
                );
110
                $this->lexer->moveNext();
111
                $this->lexer->moveNext();
112
            }
113
114
            if ($this->currentAnnotation !== null) {
0 ignored issues
show
introduced by
The condition $this->currentAnnotation !== null can never be true.
Loading history...
115
                if ($this->currentAnnotationContent === null) {
116
                    $this->parsedAnnotations[] = $this->currentAnnotation;
117
                } else {
118
                    throw new AnnotationParseException(
119
                        'An annotation content is not closed (you probably forget to close a parenthesis or a quote)'
120
                    );
121
                }
122
            }
123
124
            $parent = $this->saveAnnotations($parent);
125
        } catch (AnnotationParseException $exception) {
126
            throw new AnnotationParseException($exception->getMessage());
127
        }
128
129
        return $parent;
130
    }
131
132
    /**
133
     * Initialize the documentation parsing process.
134
     */
135
    private function initialize(): void
136
    {
137
        $this->parsedAnnotations = [];
138
        $this->currentLine       = 0;
139
        $this->reset();
140
    }
141
142
    /**
143
     * Reset an annotation parsing.
144
     */
145
    private function reset(): void
146
    {
147
        $this->currentAnnotation        = null;
148
        $this->currentAnnotationContent = null;
149
        $this->openedStringToken        = null;
150
        $this->currentlyEscaping        = false;
151
152
        $this->openedParenthesis = 0;
153
    }
154
155
    /**
156
     * Parse a token with a value and type, and consume it depending on type.
157
     *
158
     * @param int    $type  The token type (an integer from the Lexer class constant).
159
     * @param string $value The token value.
160
     *
161
     * @throws AnnotationParseException If the token type is invalid.
162
     */
163
    private function parse(int $type, string $value): void
164
    {
165
        switch ($type) {
166
            case Lexer::T_ANNOTATION:
167
                $this->consumeAnnotationToken($value);
168
                break;
169
            case Lexer::T_O_PARENTHESIS:
170
                $this->consumeOpeningParenthesisToken();
171
                $this->addTokenToContent($value);
172
                break;
173
            case Lexer::T_C_PARENTHESIS:
174
                $this->addTokenToContent($value);
175
                $this->consumeClosingParenthesisToken();
176
                break;
177
            case Lexer::T_SINGLE_QUOTE:
178
            case Lexer::T_DOUBLE_QUOTE:
179
                $this->addTokenToContent($value);
180
                $this->consumeQuoteToken($type);
181
                break;
182
            case Lexer::T_ASTERISK:
183
                if ($this->openedStringToken !== null) {
184
                    // We are in a string, save this token value.
185
                    $this->addTokenToContent($value);
186
                }
187
                break;
188
            case Lexer::T_LINE_BREAK:
189
                $this->addTokenToContent($value);
190
                $this->currentLine++;
191
                break;
192
            case Lexer::T_BACKSLASH:
193
            case Lexer::T_WHITESPACE:
194
            case Lexer::T_OTHER:
195
                $this->addTokenToContent($value);
196
                break;
197
            default:
198
                throw new AnnotationParseException(sprintf('A token of value "%s" has an invalid type', $value));
199
        }
200
        $this->afterConsume($type);
201
    }
202
203
    /**
204
     * Add a token content to the current annotation content.
205
     *
206
     * @param string $value The token value to add.
207
     */
208
    private function addTokenToContent(string $value)
209
    {
210
        if ($this->currentAnnotationContent !== null) {
211
            $this->currentAnnotationContent .= $value;
212
        }
213
    }
214
215
    /**
216
     * Consume an annotation token ("@PhpUnitGen" for example).
217
     *
218
     * @param string $value The annotation token value.
219
     *
220
     * @throws AnnotationParseException If the annotation is unknown.
221
     */
222
    private function consumeAnnotationToken(string $value): void
223
    {
224
        if ($this->currentAnnotation === null) {
225
            // We are not in an annotation, build a new one.
226
            $this->currentAnnotation = $this->annotationFactory
227
                ->invoke($value, $this->currentLine);
228
        } else {
229
            if ($this->currentAnnotationContent === null) {
230
                // It is an annotation without content, save it and create the new one.
231
                $this->parsedAnnotations[] = $this->currentAnnotation;
232
                $this->reset();
233
                $this->currentAnnotation = $this->annotationFactory
234
                    ->invoke($value, $this->currentLine);
235
            } else {
236
                // An annotation content parsing is not finished.
237
                throw new AnnotationParseException(
238
                    'An annotation content is not closed (you probably forget to close a parenthesis or a quote)'
239
                );
240
            }
241
        }
242
    }
243
244
    /**
245
     * Consume an opening parenthesis token.
246
     */
247
    private function consumeOpeningParenthesisToken(): void
248
    {
249
        if ($this->currentAnnotation !== null && $this->openedStringToken === null) {
250
            // We are in an annotation but not in a string, lets do something.
251
            if ($this->currentAnnotationContent === null) {
252
                if ($this->currentAnnotation->getLine() === $this->currentLine) {
253
                    // Begin content parsing only if it is on the same line.
254
                    $this->currentAnnotationContent = '';
255
                    $this->openedParenthesis++;
256
                }
257
            } else {
258
                $this->openedParenthesis++;
259
            }
260
        }
261
    }
262
263
    /**
264
     * Consume an closing parenthesis token.
265
     */
266
    private function consumeClosingParenthesisToken(): void
267
    {
268
        if ($this->currentAnnotationContent !== null && $this->openedStringToken === null) {
269
            // We are in an annotation content and not in a string.
270
            if ($this->openedParenthesis > 0) {
271
                $this->openedParenthesis--;
272
                if ($this->openedParenthesis === 0) {
273
                    // Annotation content is finished.
274
                    $this->currentAnnotation->setStringContent(substr(
275
                        $this->currentAnnotationContent,
276
                        1,
277
                        strlen($this->currentAnnotationContent) - 2
278
                    ));
279
                    $this->parsedAnnotations[] = $this->currentAnnotation;
280
                    $this->reset();
281
                }
282
            }
283
        }
284
    }
285
286
    /**
287
     * Consume a quote token (" or ').
288
     */
289
    private function consumeQuoteToken(int $type): void
290
    {
291
        if ($this->currentAnnotationContent !== null) {
292
            if ($this->openedStringToken === null) {
293
                // It is a string opening.
294
                $this->openedStringToken = $type;
295
            } else {
296
                if (! $this->currentlyEscaping && $type === $this->openedStringToken) {
297
                    // We are in a string, the token is not escaped and is the same as the string opening token.
298
                    // Close the string.
299
                    $this->openedStringToken = null;
300
                }
301
            }
302
        }
303
    }
304
305
    /**
306
     * A method that is executed after consuming a token.
307
     *
308
     * @param int $type The token type (an integer from the Lexer class constant).
309
     */
310
    private function afterConsume(int $type): void
311
    {
312
        // Put in escaping mode if it were not escaping and there is a backslash token.
313
        if (! $this->currentlyEscaping && $type === Lexer::T_BACKSLASH) {
314
            $this->currentlyEscaping = true;
315
        } else {
316
            $this->currentlyEscaping = false;
317
        }
318
    }
319
320
    /**
321
     * Save each annotations in the parent depending on type.
322
     *
323
     * @param DocumentationInterface $parent The parent node.
324
     *
325
     * @return DocumentationInterface The updated parent.
326
     */
327
    private function saveAnnotations(DocumentationInterface $parent): DocumentationInterface
328
    {
329
        foreach ($this->parsedAnnotations as $annotation) {
330
            if ($parent instanceof FunctionModelInterface) {
331
                // Parent is a function, save assert and mock annotations.
332
                $parent = $this->saveFunctionAnnotation($parent, $annotation);
333
            }
334
            if ($parent instanceof ClassLikeInterface) {
335
                // Parent is a class like, save mock and constructor annotations.
336
                $parent = $this->saveClassLikeAnnotation($parent, $annotation);
337
            }
338
        }
339
340
        return $parent;
341
    }
342
343
    private function saveFunctionAnnotation(
344
        FunctionModelInterface $function,
345
        AnnotationInterface $annotation
346
    ): FunctionModelInterface {
347
        if ($annotation->getType() === AnnotationInterface::TYPE_ASSERT
348
            || $annotation->getType() === AnnotationInterface::TYPE_GETTER
349
            || $annotation->getType() === AnnotationInterface::TYPE_SETTER
350
            || $annotation->getType() === AnnotationInterface::TYPE_MOCK
351
        ) {
352
            $annotation->setParentNode($function);
353
            $function->addAnnotation($annotation);
354
            $annotation->compile();
355
        }
356
        return $function;
357
    }
358
359
    private function saveClassLikeAnnotation(
360
        ClassLikeInterface $classLike,
361
        AnnotationInterface $annotation
362
    ): ClassLikeInterface {
363
        if ($annotation->getType() === AnnotationInterface::TYPE_MOCK) {
364
            $parent = $classLike->getParentNode();
365
            $annotation->setParentNode($parent);
366
            $parent->addAnnotation($annotation);
0 ignored issues
show
Bug introduced by
The method addAnnotation() does not exist on PhpUnitGen\Model\PropertyInterface\NodeInterface. It seems like you code against a sub-type of PhpUnitGen\Model\PropertyInterface\NodeInterface such as PhpUnitGen\Model\ModelIn...\FunctionModelInterface or PhpUnitGen\Model\Propert...face\ClassLikeInterface or PhpUnitGen\Model\Propert...\DocumentationInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

366
            $parent->/** @scrutinizer ignore-call */ 
367
                     addAnnotation($annotation);
Loading history...
367
            $annotation->compile();
368
        }
369
        if ($annotation->getType() === AnnotationInterface::TYPE_CONSTRUCTOR) {
370
            $annotation->setParentNode($classLike);
371
            $classLike->addAnnotation($annotation);
372
            $annotation->compile();
373
        }
374
        return $classLike;
375
    }
376
}
377