Completed
Pull Request — master (#162)
by
unknown
02:07
created

Parser::parseNewline()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
/*
4
 * This file is part of the Behat Gherkin.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Behat\Gherkin;
12
13
use Behat\Gherkin\Exception\LexerException;
14
use Behat\Gherkin\Exception\ParserException;
15
use Behat\Gherkin\Node\BackgroundNode;
16
use Behat\Gherkin\Node\ExampleTableNode;
17
use Behat\Gherkin\Node\FeatureNode;
18
use Behat\Gherkin\Node\OutlineNode;
19
use Behat\Gherkin\Node\PyStringNode;
20
use Behat\Gherkin\Node\ScenarioInterface;
21
use Behat\Gherkin\Node\ScenarioNode;
22
use Behat\Gherkin\Node\StepNode;
23
use Behat\Gherkin\Node\TableNode;
24
25
/**
26
 * Gherkin parser.
27
 *
28
 * $lexer  = new Behat\Gherkin\Lexer($keywords);
29
 * $parser = new Behat\Gherkin\Parser($lexer);
30
 * $featuresArray = $parser->parse('/path/to/feature.feature');
31
 *
32
 * @author Konstantin Kudryashov <[email protected]>
33
 */
34
class Parser
35
{
36
    private $lexer;
37
    private $input;
38
    private $file;
39
    private $tags = array();
40
    private $languageSpecifierLine;
41
42
    /**
43
     * Initializes parser.
44
     *
45
     * @param Lexer $lexer Lexer instance
46
     */
47 211
    public function __construct(Lexer $lexer)
48
    {
49 211
        $this->lexer = $lexer;
50 211
    }
51
52
    /**
53
     * Parses input & returns features array.
54
     *
55
     * @param string $input Gherkin string document
56
     * @param string $file  File name
57
     *
58
     * @return FeatureNode|null
59
     *
60
     * @throws ParserException
61
     */
62 209
    public function parse($input, $file = null)
63
    {
64 209
        $this->languageSpecifierLine = null;
65 209
        $this->input = $input;
66 209
        $this->file = $file;
67 209
        $this->tags = array();
68
69
        try {
70 209
            $this->lexer->analyse($this->input, 'en');
71
        } catch (LexerException $e) {
72
            throw new ParserException(
73
                sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
74
                0,
75
                $e
76
            );
77
        }
78
79 209
        $feature = null;
80 209
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
81 209
            $node = $this->parseExpression();
82
83 201
            if (null === $node || "\n" === $node) {
84 1
                continue;
85
            }
86
87 200
            if (!$feature && $node instanceof FeatureNode) {
88 200
                $feature = $node;
89 200
                continue;
90
            }
91
92
            if ($feature && $node instanceof FeatureNode) {
93
                throw new ParserException(sprintf(
94
                    'Only one feature is allowed per feature file. But %s got multiple.',
95
                    $this->file
96
                ));
97
            }
98
99
            if (is_string($node)) {
100
                throw new ParserException(sprintf(
101
                    'Expected Feature, but got text: "%s"%s',
102
                    $node,
103
                    $this->file ? ' in file: ' . $this->file : ''
104
                ));
105
            }
106
107
            if (!$node instanceof FeatureNode) {
108
                throw new ParserException(sprintf(
109
                    'Expected Feature, but got %s on line: %d%s',
110
                    $node->getKeyword(),
0 ignored issues
show
Bug introduced by
The method getKeyword does only exist in Behat\Gherkin\Node\Backg...t\Gherkin\Node\StepNode, but not in Behat\Gherkin\Node\PyStr...\Gherkin\Node\TableNode.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
111
                    $node->getLine(),
112
                    $this->file ? ' in file: ' . $this->file : ''
113
                ));
114
            }
115
        }
116
117 200
        return $feature;
118
    }
119
120
    /**
121
     * Returns next token if it's type equals to expected.
122
     *
123
     * @param string $type Token type
124
     *
125
     * @return array
126
     *
127
     * @throws Exception\ParserException
128
     */
129 209
    protected function expectTokenType($type)
130
    {
131 209
        $types = (array) $type;
132 209
        if (in_array($this->predictTokenType(), $types)) {
133 209
            return $this->lexer->getAdvancedToken();
134
        }
135
136 1
        $token = $this->lexer->predictToken();
137
138 1
        throw new ParserException(sprintf(
139 1
            'Expected %s token, but got %s on line: %d%s',
140 1
            implode(' or ', $types),
141 1
            $this->predictTokenType(),
142 1
            $token['line'],
143 1
            $this->file ? ' in file: ' . $this->file : ''
144
        ));
145
    }
146
147
    /**
148
     * Returns next token if it's type equals to expected.
149
     *
150
     * @param string $type Token type
151
     *
152
     * @return null|array
153
     */
154 199
    protected function acceptTokenType($type)
155
    {
156 199
        if ($type !== $this->predictTokenType()) {
157
            return null;
158
        }
159
160 199
        return $this->lexer->getAdvancedToken();
161
    }
162
163
    /**
164
     * Returns next token type without real input reading (prediction).
165
     *
166
     * @return string
167
     */
168 209
    protected function predictTokenType()
169
    {
170 209
        $token = $this->lexer->predictToken();
171
172 209
        return $token['type'];
173
    }
174
175
    /**
176
     * Parses current expression & returns Node.
177
     *
178
     * @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
179
     *
180
     * @throws ParserException
181
     */
182 209
    protected function parseExpression()
183
    {
184 209
        switch ($type = $this->predictTokenType()) {
185 209
            case 'Feature':
186 208
                return $this->parseFeature();
187 209
            case 'Background':
188 194
                return $this->parseBackground();
189 209
            case 'Scenario':
190 203
                return $this->parseScenario();
191 209
            case 'Outline':
192 196
                return $this->parseOutline();
193 209
            case 'Examples':
194 196
                return $this->parseExamples();
195 209
            case 'TableRow':
196
                return $this->parseTable();
197 209
            case 'PyStringOp':
198 4
                return $this->parsePyString();
199 209
            case 'Step':
200 202
                return $this->parseStep();
201 205
            case 'Text':
202 196
                return $this->parseText();
203 205
            case 'Newline':
204 204
                return $this->parseNewline();
205 188
            case 'Tag':
206 3
                return $this->parseTags();
207 186
            case 'Comment':
208
                return $this->parseComment();
209 186
            case 'Language':
210 185
                return $this->parseLanguage();
211 1
            case 'EOS':
212 1
                return '';
213
        }
214
215
        throw new ParserException(sprintf('Unknown token type: %s', $type));
216
    }
217
218
    /**
219
     * Parses feature token & returns it's node.
220
     *
221
     * @return FeatureNode
222
     *
223
     * @throws ParserException
224
     */
225 208
    protected function parseFeature()
226
    {
227 208
        $token = $this->expectTokenType('Feature');
228
229 208
        $title = trim($token['value']) ?: null;
230 208
        $description = null;
231 208
        $tags = $this->popTags();
232 208
        $background = null;
233 208
        $scenarios = array();
234 208
        $keyword = $token['keyword'];
235 208
        $language = $this->lexer->getLanguage();
236 208
        $file = $this->file;
237 208
        $line = $token['line'];
238
239
        // Parse description, background, scenarios & outlines
240 208
        while ('EOS' !== $this->predictTokenType()) {
241 208
            $node = $this->parseExpression();
242
243 208
            if (is_string($node)) {
244 202
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
245 202
                $description .= (null !== $description ? "\n" : '') . $text;
246 202
                continue;
247
            }
248
249 202
            if (!$background && $node instanceof BackgroundNode) {
250 193
                $background = $node;
251 193
                continue;
252
            }
253
254 202
            if ($node instanceof ScenarioInterface) {
255 200
                $scenarios[] = $node;
256 200
                continue;
257
            }
258
259 3
            if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
260 1
                throw new ParserException(sprintf(
261 1
                    'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
262 1
                    $background->getLine(),
263 1
                    $node->getLine(),
264 1
                    $this->file ? ' in file: ' . $this->file : ''
265
                ));
266
            }
267
268 2
            if (!$node instanceof ScenarioNode) {
269 2
                throw new ParserException(sprintf(
270 2
                    'Expected Scenario, Outline or Background, but got %s on line: %d%s',
271 2
                    $node->getNodeType(),
272 2
                    $node->getLine(),
273 2
                    $this->file ? ' in file: ' . $this->file : ''
274
                ));
275
            }
276
        }
277
278 201
        return new FeatureNode(
279 201
            rtrim($title) ?: null,
280 201
            rtrim($description) ?: null,
281 201
            $tags,
282 201
            $background,
283 201
            $scenarios,
284 201
            $keyword,
285 201
            $language,
286 201
            $file,
287 201
            $line
288
        );
289
    }
290
291
    /**
292
     * Parses background token & returns it's node.
293
     *
294
     * @return BackgroundNode
295
     *
296
     * @throws ParserException
297
     */
298 194 View Code Duplication
    protected function parseBackground()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
    {
300 194
        $token = $this->expectTokenType('Background');
301
302 194
        $title = trim($token['value']);
303 194
        $keyword = $token['keyword'];
304 194
        $line = $token['line'];
305 194
        $example = null;
306
307 194
        if (count($this->popTags())) {
308 1
            throw new ParserException(sprintf(
309 1
                'Background can not be tagged, but it is on line: %d%s',
310 1
                $line,
311 1
                $this->file ? ' in file: ' . $this->file : ''
312
            ));
313
        }
314
315
        // Parse description and steps
316 193
        $steps = array();
317 193
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment', 'Examples');
318 193
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
319 193
            $node = $this->parseExpression();
320
321 193
            if ($node instanceof ExampleTableNode) {
322 1
                $example = $node;
323 1
                continue;
324
            }
325
326 193
            if ($node instanceof StepNode) {
327 192
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
328 192
                continue;
329
            }
330
331 2
            if (!count($steps) && is_string($node)) {
332 2
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
333 2
                $title .= "\n" . $text;
334 2
                continue;
335
            }
336
337
            if ("\n" === $node) {
338
                continue;
339
            }
340
341
            if (is_string($node)) {
342
                throw new ParserException(sprintf(
343
                    'Expected Step, but got text: "%s"%s',
344
                    $node,
345
                    $this->file ? ' in file: ' . $this->file : ''
346
                ));
347
            }
348
349
            if (!$node instanceof StepNode) {
350
                throw new ParserException(sprintf(
351
                    'Expected Step, but got %s on line: %d%s',
352
                    $node->getNodeType(),
353
                    $node->getLine(),
354
                    $this->file ? ' in file: ' . $this->file : ''
355
                ));
356
            }
357
        }
358
359 193
        return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line, $example);
360
    }
361
362
    /**
363
     * Parses scenario token & returns it's node.
364
     *
365
     * @return ScenarioNode
366
     *
367
     * @throws ParserException
368
     */
369 203
    protected function parseScenario()
370
    {
371 203
        $token = $this->expectTokenType('Scenario');
372
373 203
        $title = trim($token['value']);
374 203
        $tags = $this->popTags();
375 203
        $keyword = $token['keyword'];
376 203
        $line = $token['line'];
377
378
        // Parse description and steps
379 203
        $steps = array();
380 203
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
381 202
            $node = $this->parseExpression();
382
383 201
            if ($node instanceof StepNode) {
384 200
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
385 200
                continue;
386
            }
387
388 5
            if (!count($steps) && is_string($node)) {
389 3
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
390 3
                $title .= "\n" . $text;
391 3
                continue;
392
            }
393
394 2
            if ("\n" === $node) {
395
                continue;
396
            }
397
398 2
            if (is_string($node)) {
399 2
                throw new ParserException(sprintf(
400 2
                    'Expected Step, but got text: "%s"%s',
401 2
                    $node,
402 2
                    $this->file ? ' in file: ' . $this->file : ''
403
                ));
404
            }
405
406
            if (!$node instanceof StepNode) {
407
                throw new ParserException(sprintf(
408
                    'Expected Step, but got %s on line: %d%s',
409
                    $node->getNodeType(),
410
                    $node->getLine(),
411
                    $this->file ? ' in file: ' . $this->file : ''
412
                ));
413
            }
414
        }
415
416 200
        return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
417
    }
418
419
    /**
420
     * Parses scenario outline token & returns it's node.
421
     *
422
     * @return OutlineNode
423
     *
424
     * @throws ParserException
425
     */
426 196 View Code Duplication
    protected function parseOutline()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
427
    {
428 196
        $token = $this->expectTokenType('Outline');
429
430 196
        $title = trim($token['value']);
431 196
        $tags = $this->popTags();
432 196
        $keyword = $token['keyword'];
433 196
        $examples = null;
434 196
        $line = $token['line'];
435
436
        // Parse description, steps and examples
437 196
        $steps = array();
438 196
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
439 195
            $node = $this->parseExpression();
440
441 195
            if ($node instanceof StepNode) {
442 194
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
443 194
                continue;
444
            }
445
446 195
            if ($node instanceof ExampleTableNode) {
447 195
                $examples = $node;
448 195
                continue;
449
            }
450
451 3
            if (!count($steps) && is_string($node)) {
452 3
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
453 3
                $title .= "\n" . $text;
454 3
                continue;
455
            }
456
457
            if ("\n" === $node) {
458
                continue;
459
            }
460
461
            if (is_string($node)) {
462
                throw new ParserException(sprintf(
463
                    'Expected Step or Examples table, but got text: "%s"%s',
464
                    $node,
465
                    $this->file ? ' in file: ' . $this->file : ''
466
                ));
467
            }
468
469
            if (!$node instanceof StepNode) {
470
                throw new ParserException(sprintf(
471
                    'Expected Step or Examples table, but got %s on line: %d%s',
472
                    $node->getNodeType(),
473
                    $node->getLine(),
474
                    $this->file ? ' in file: ' . $this->file : ''
475
                ));
476
            }
477
        }
478
479 196
        if (null === $examples) {
480 1
            throw new ParserException(sprintf(
481 1
                'Outline should have examples table, but got none for outline "%s" on line: %d%s',
482 1
                rtrim($title),
483 1
                $line,
484 1
                $this->file ? ' in file: ' . $this->file : ''
485
            ));
486
        }
487
488 195
        return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
489
    }
490
491
    /**
492
     * Parses step token & returns it's node.
493
     *
494
     * @return StepNode
495
     */
496 202
    protected function parseStep()
497
    {
498 202
        $token = $this->expectTokenType('Step');
499
500 202
        $keyword = $token['value'];
501 202
        $keywordType = $token['keyword_type'];
502 202
        $text = trim($token['text']);
503 202
        $line = $token['line'];
504
505 202
        $arguments = array();
506 202
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
507 199
            if ('Comment' === $predicted || 'Newline' === $predicted) {
508 198
                $this->acceptTokenType($predicted);
509 198
                continue;
510
            }
511
512 4
            $node = $this->parseExpression();
513
514 3
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
515 3
                $arguments[] = $node;
516
            }
517
        }
518
519 201
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
520
    }
521
522
    /**
523
     * Parses examples table node.
524
     *
525
     * @return ExampleTableNode
526
     */
527 196
    protected function parseExamples()
528
    {
529 196
        $token = $this->expectTokenType('Examples');
530
531 196
        $keyword = $token['keyword'];
532
533 196
        return new ExampleTableNode($this->parseTableRows(), $keyword);
534
    }
535
536
    /**
537
     * Parses table token & returns it's node.
538
     *
539
     * @return TableNode
540
     */
541
    protected function parseTable()
542
    {
543
        return new TableNode($this->parseTableRows());
544
    }
545
546
    /**
547
     * Parses PyString token & returns it's node.
548
     *
549
     * @return PyStringNode
550
     */
551 4
    protected function parsePyString()
552
    {
553 4
        $token = $this->expectTokenType('PyStringOp');
554
555 4
        $line = $token['line'];
556
557 4
        $strings = array();
558 4
        while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
559 4
            $token = $this->expectTokenType('Text');
560
561 4
            $strings[] = $token['value'];
562
        }
563
564 4
        $this->expectTokenType('PyStringOp');
565
566 3
        return new PyStringNode($strings, $line);
567
    }
568
569
    /**
570
     * Parses tags.
571
     *
572
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
573
     */
574 3
    protected function parseTags()
575
    {
576 3
        $token = $this->expectTokenType('Tag');
577 3
        $this->tags = array_merge($this->tags, $token['tags']);
578
579 3
        return $this->parseExpression();
580
    }
581
582
    /**
583
     * Returns current set of tags and clears tag buffer.
584
     *
585
     * @return array
586
     */
587 208
    protected function popTags()
588
    {
589 208
        $tags = $this->tags;
590 208
        $this->tags = array();
591
592 208
        return $tags;
593
    }
594
595
    /**
596
     * Parses next text line & returns it.
597
     *
598
     * @return string
599
     */
600 196
    protected function parseText()
601
    {
602 196
        $token = $this->expectTokenType('Text');
603
604 196
        return $token['value'];
605
    }
606
607
    /**
608
     * Parses next newline & returns \n.
609
     *
610
     * @return string
611
     */
612 204
    protected function parseNewline()
613
    {
614 204
        $this->expectTokenType('Newline');
615
616 204
        return "\n";
617
    }
618
619
    /**
620
     * Parses next comment token & returns it's string content.
621
     *
622
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
623
     */
624
    protected function parseComment()
625
    {
626
        $this->expectTokenType('Comment');
627
628
        return $this->parseExpression();
629
    }
630
631
    /**
632
     * Parses language block and updates lexer configuration based on it.
633
     *
634
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
635
     *
636
     * @throws ParserException
637
     */
638 185
    protected function parseLanguage()
639
    {
640 185
        $token = $this->expectTokenType('Language');
641
642 185
        if (null === $this->languageSpecifierLine) {
643 185
            $this->lexer->analyse($this->input, $token['value']);
644 185
            $this->languageSpecifierLine = $token['line'];
645 185
        } elseif ($token['line'] !== $this->languageSpecifierLine) {
646 1
            throw new ParserException(sprintf(
647 1
                'Ambiguous language specifiers on lines: %d and %d%s',
648 1
                $this->languageSpecifierLine,
649 1
                $token['line'],
650 1
                $this->file ? ' in file: ' . $this->file : ''
651
            ));
652
        }
653
654 185
        return $this->parseExpression();
655
    }
656
657
    /**
658
     * Parses the rows of a table
659
     *
660
     * @return string[][]
661
     */
662 196
    private function parseTableRows()
663
    {
664 196
        $table = array();
665 196
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
666 196
            if ('Comment' === $predicted || 'Newline' === $predicted) {
667 96
                $this->acceptTokenType($predicted);
668 96
                continue;
669
            }
670
671 196
            $token = $this->expectTokenType('TableRow');
672
673 196
            $table[$token['line']] = $token['columns'];
674
        }
675
676 196
        return $table;
677
    }
678
679
    /**
680
     * Changes step node type for types But, And to type of previous step if it exists else sets to Given
681
     *
682
     * @param StepNode   $node
683
     * @param StepNode[] $steps
684
     * @return StepNode
685
     */
686 201
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
687
    {
688 201
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
689 193
            if (($prev = end($steps))) {
690 193
                $keywordType = $prev->getKeywordType();
691
            } else {
692
                $keywordType = 'Given';
693
            }
694
695 193
            $node = new StepNode(
696 193
                $node->getKeyword(),
697 193
                $node->getText(),
698 193
                $node->getArguments(),
699 193
                $node->getLine(),
700 193
                $keywordType
701
            );
702
        }
703 201
        return $node;
704
    }
705
}
706