Completed
Push — develop ( 34db25...1b1cc6 )
by Paul
02:36
created

DocumentationNodeParser   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 303
rs 8.3673
c 0
b 0
f 0
wmc 45

11 Methods

Rating   Name   Duplication   Size   Complexity  
B consumeClosingParenthesisToken() 0 15 5
B consumeOpeningParenthesisToken() 0 12 5
A initialize() 0 5 1
C parse() 0 38 12
B consumeQuoteToken() 0 11 5
A addTokenToContent() 0 4 2
C invoke() 0 38 7
A __construct() 0 8 1
A consumeAnnotationToken() 0 17 3
A reset() 0 8 1
A afterConsume() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like DocumentationNodeParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentationNodeParser, and based on these observations, apply Extract Interface, too.

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