Completed
Push — develop ( 74dbd7...d5cb43 )
by Paul
02:08
created

DocumentationNodeParser::addTokenToContent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 4
rs 10
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\AnnotationRegister;
9
use PhpUnitGen\Annotation\Lexer;
10
use PhpUnitGen\Exception\AnnotationParseException;
11
use PhpUnitGen\Model\PropertyInterface\DocumentationInterface;
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 AnnotationRegister $annotationRegister The annotation register to use.
36
     */
37
    private $annotationRegister;
38
39
    /**
40
     * @var AbstractAnnotation[] $parsedAnnotations The parsed annotation list.
41
     */
42
    private $parsedAnnotations;
43
44
    /**
45
     * @var int $currentLine The current line number.
46
     */
47
    private $currentLine;
48
49
    /**
50
     * @var AbstractAnnotation $currentAnnotation The current parsed annotation, null if none.
51
     */
52
    private $currentAnnotation;
53
54
    /**
55
     * @var string|null $currentAnnotationContent The current parsed annotation content, null if none.
56
     */
57
    private $currentAnnotationContent;
58
59
    /**
60
     * @var int|null $openedStringToken The opened string opening token identifier, null if no string opened.
61
     */
62
    private $openedStringToken;
63
64
    /**
65
     * @var bool $currentlyEscaping Tells if last parsed token was an escape token.
66
     */
67
    private $currentlyEscaping;
68
69
    /**
70
     * @var int $openedParenthesis The opened parenthesis number for an annotation content.
71
     */
72
    private $openedParenthesis;
73
74
    /**
75
     * DocumentationNodeParser constructor.
76
     *
77
     * @param Lexer              $lexer              The lexer to use.
78
     * @param AnnotationFactory  $annotationFactory  The annotation factory to use.
79
     * @param AnnotationRegister $annotationRegister The annotation register to use.
80
     */
81
    public function __construct(
82
        Lexer $lexer,
83
        AnnotationFactory $annotationFactory,
84
        AnnotationRegister $annotationRegister
85
    ) {
86
        $this->lexer              = $lexer;
87
        $this->annotationFactory  = $annotationFactory;
88
        $this->annotationRegister = $annotationRegister;
89
    }
90
91
    /**
92
     * Parse a node to update the parent node model.
93
     *
94
     * @param Doc                    $node   The node to parse.
95
     * @param DocumentationInterface $parent The parent node.
96
     *
97
     * @return DocumentationInterface The updated parent.
98
     *
99
     * @throws AnnotationParseException If an annotation is invalid.
100
     */
101
    public function invoke(Doc $node, DocumentationInterface $parent): DocumentationInterface
102
    {
103
        $documentation = $node->getText();
104
        $parent->setDocumentation($documentation);
105
106
        try {
107
            $this->initialize();
108
109
            $this->lexer->setInput($documentation);
110
            $this->lexer->moveNext();
111
            $this->lexer->moveNext();
112
113
            while ($this->lexer->token) {
114
                $this->parse(
115
                    $this->lexer->token['type'],
116
                    $this->lexer->token['value']
117
                );
118
                $this->lexer->moveNext();
119
                $this->lexer->moveNext();
120
            }
121
122
            if ($this->currentAnnotation !== null) {
0 ignored issues
show
introduced by
The condition $this->currentAnnotation !== null can never be true.
Loading history...
123
                if ($this->currentAnnotationContent === null) {
124
                    $this->parsedAnnotations[] = $this->currentAnnotation;
125
                } else {
126
                    throw new AnnotationParseException(
127
                        'An annotation content is not closed (you probably forget to close a parenthesis or a quote)'
128
                    );
129
                }
130
            }
131
132
            $this->annotationRegister->invoke($parent, $this->parsedAnnotations);
133
        } catch (AnnotationParseException $exception) {
134
            throw new AnnotationParseException($exception->getMessage());
135
        }
136
137
        return $parent;
138
    }
139
140
    /**
141
     * Initialize the documentation parsing process.
142
     */
143
    private function initialize(): void
144
    {
145
        $this->parsedAnnotations = [];
146
        $this->currentLine       = 0;
147
        $this->reset();
148
    }
149
150
    /**
151
     * Reset an annotation parsing.
152
     */
153
    private function reset(): void
154
    {
155
        $this->currentAnnotation        = null;
156
        $this->currentAnnotationContent = null;
157
        $this->openedStringToken        = null;
158
        $this->currentlyEscaping        = false;
159
160
        $this->openedParenthesis = 0;
161
    }
162
163
    /**
164
     * Parse a token with a value and type, and consume it depending on type.
165
     *
166
     * @param int    $type  The token type (an integer from the Lexer class constant).
167
     * @param string $value The token value.
168
     *
169
     * @throws AnnotationParseException If the token type is invalid.
170
     */
171
    private function parse(int $type, string $value): void
172
    {
173
        switch ($type) {
174
            case Lexer::T_ANNOTATION:
175
                $this->consumeAnnotationToken($value);
176
                break;
177
            case Lexer::T_O_PARENTHESIS:
178
                $this->consumeOpeningParenthesisToken();
179
                $this->addTokenToContent($value);
180
                break;
181
            case Lexer::T_C_PARENTHESIS:
182
                $this->addTokenToContent($value);
183
                $this->consumeClosingParenthesisToken();
184
                break;
185
            case Lexer::T_SINGLE_QUOTE:
186
            case Lexer::T_DOUBLE_QUOTE:
187
                $this->addTokenToContent($value);
188
                $this->consumeQuoteToken($type);
189
                break;
190
            case Lexer::T_ASTERISK:
191
                if ($this->openedStringToken !== null) {
192
                    // We are in a string, save this token value.
193
                    $this->addTokenToContent($value);
194
                }
195
                break;
196
            case Lexer::T_LINE_BREAK:
197
                $this->addTokenToContent($value);
198
                $this->currentLine++;
199
                break;
200
            case Lexer::T_BACKSLASH:
201
            case Lexer::T_WHITESPACE:
202
            case Lexer::T_OTHER:
203
                $this->addTokenToContent($value);
204
                break;
205
            default:
206
                throw new AnnotationParseException(sprintf('A token of value "%s" has an invalid type', $value));
207
        }
208
        $this->afterConsume($type);
209
    }
210
211
    /**
212
     * Add a token content to the current annotation content.
213
     *
214
     * @param string $value The token value to add.
215
     */
216
    private function addTokenToContent(string $value)
217
    {
218
        if ($this->currentAnnotationContent !== null) {
219
            $this->currentAnnotationContent .= $value;
220
        }
221
    }
222
223
    /**
224
     * Consume an annotation token ("@PhpUnitGen" for example).
225
     *
226
     * @param string $value The annotation token value.
227
     *
228
     * @throws AnnotationParseException If the annotation is unknown.
229
     */
230
    private function consumeAnnotationToken(string $value): void
231
    {
232
        if ($this->currentAnnotation === null) {
233
            // We are not in an annotation, build a new one.
234
            $this->currentAnnotation = $this->annotationFactory
235
                ->invoke($value, $this->currentLine);
236
        } else {
237
            if ($this->currentAnnotationContent === null) {
238
                // It is an annotation without content, save it and create the new one.
239
                $this->parsedAnnotations[] = $this->currentAnnotation;
240
                $this->reset();
241
                $this->currentAnnotation = $this->annotationFactory
242
                    ->invoke($value, $this->currentLine);
243
            } else {
244
                // An annotation content parsing is not finished.
245
                throw new AnnotationParseException(
246
                    'An annotation content is not closed (you probably forget to close a parenthesis or a quote)'
247
                );
248
            }
249
        }
250
    }
251
252
    /**
253
     * Consume an opening parenthesis token.
254
     */
255
    private function consumeOpeningParenthesisToken(): void
256
    {
257
        if ($this->currentAnnotation !== null && $this->openedStringToken === null) {
258
            // We are in an annotation but not in a string, lets do something.
259
            if ($this->currentAnnotationContent === null) {
260
                if ($this->currentAnnotation->getLine() === $this->currentLine) {
261
                    // Begin content parsing only if it is on the same line.
262
                    $this->currentAnnotationContent = '';
263
                    $this->openedParenthesis++;
264
                }
265
            } else {
266
                $this->openedParenthesis++;
267
            }
268
        }
269
    }
270
271
    /**
272
     * Consume an closing parenthesis token.
273
     */
274
    private function consumeClosingParenthesisToken(): void
275
    {
276
        if ($this->currentAnnotationContent !== null && $this->openedStringToken === null) {
277
            // We are in an annotation content and not in a string.
278
            if ($this->openedParenthesis > 0) {
279
                $this->openedParenthesis--;
280
                if ($this->openedParenthesis === 0) {
281
                    // Annotation content is finished.
282
                    $this->currentAnnotation->setStringContent(substr(
283
                        $this->currentAnnotationContent,
284
                        1,
285
                        strlen($this->currentAnnotationContent) - 2
286
                    ));
287
                    $this->parsedAnnotations[] = $this->currentAnnotation;
288
                    $this->reset();
289
                }
290
            }
291
        }
292
    }
293
294
    /**
295
     * Consume a quote token (" or ').
296
     */
297
    private function consumeQuoteToken(int $type): void
298
    {
299
        if ($this->currentAnnotationContent !== null) {
300
            if ($this->openedStringToken === null) {
301
                // It is a string opening.
302
                $this->openedStringToken = $type;
303
            } else {
304
                if (! $this->currentlyEscaping && $type === $this->openedStringToken) {
305
                    // We are in a string, the token is not escaped and is the same as the string opening token.
306
                    // Close the string.
307
                    $this->openedStringToken = null;
308
                }
309
            }
310
        }
311
    }
312
313
    /**
314
     * A method that is executed after consuming a token.
315
     *
316
     * @param int $type The token type (an integer from the Lexer class constant).
317
     */
318
    private function afterConsume(int $type): void
319
    {
320
        // Put in escaping mode if it were not escaping and there is a backslash token.
321
        if (! $this->currentlyEscaping && $type === Lexer::T_BACKSLASH) {
322
            $this->currentlyEscaping = true;
323
        } else {
324
            $this->currentlyEscaping = false;
325
        }
326
    }
327
}
328