Completed
Pull Request — master (#112)
by Christophe
02:45
created

Parser::parseScenario()   C

Complexity

Conditions 11
Paths 7

Size

Total Lines 49
Code Lines 30

Duplication

Lines 20
Ratio 40.82 %

Code Coverage

Tests 24
CRAP Score 13.4538

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 20
loc 49
ccs 24
cts 33
cp 0.7272
rs 5.2653
cc 11
eloc 30
nc 7
nop 0
crap 13.4538

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
/*
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 54
    public function __construct(Lexer $lexer)
48
    {
49 54
        $this->lexer = $lexer;
50 54
    }
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 52
    public function parse($input, $file = null)
63
    {
64 52
        $this->languageSpecifierLine = null;
65 52
        $this->input = $input;
66 52
        $this->file = $file;
67
68
        try {
69 52
            $this->lexer->analyse($this->input, 'en');
70 52
        } catch (LexerException $e) {
71
            throw new ParserException(
72
                sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
73
                0,
74
                $e
75
            );
76
        }
77
78 52
        $feature = null;
79 52
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
80 52
            $node = $this->parseExpression();
81
82 44
            if (null === $node || "\n" === $node) {
83 3
                continue;
84
            }
85
86 43
            if (!$feature && $node instanceof FeatureNode) {
87 43
                $feature = $node;
88 43
                continue;
89
            }
90
91
            if ($feature && $node instanceof FeatureNode) {
92
                throw new ParserException(sprintf(
93
                    'Only one feature is allowed per feature file. But %s got multiple.',
94
                    $this->file
95
                ));
96
            }
97
98 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
99
                throw new ParserException(sprintf(
100
                    'Expected Feature, but got text: "%s"%s',
101
                    $node,
102
                    $this->file ? ' in file: ' . $this->file : ''
103
                ));
104
            }
105
106
            if (!$node instanceof FeatureNode) {
107
                throw new ParserException(sprintf(
108
                    'Expected Feature, but got %s on line: %d%s',
109
                    $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...
110
                    $node->getLine(),
111
                    $this->file ? ' in file: ' . $this->file : ''
112
                ));
113
            }
114
        }
115
116 43
        return $feature;
117
    }
118
119
    /**
120
     * Returns next token if it's type equals to expected.
121
     *
122
     * @param string $type Token type
123
     *
124
     * @return array
125
     *
126
     * @throws Exception\ParserException
127
     */
128 52
    protected function expectTokenType($type)
129
    {
130 52
        $types = (array) $type;
131 52
        if (in_array($this->predictTokenType(), $types)) {
132 52
            return $this->lexer->getAdvancedToken();
133
        }
134
135 1
        $token = $this->lexer->predictToken();
136
137 1
        throw new ParserException(sprintf(
138 1
            'Expected %s token, but got %s on line: %d%s',
139 1
            implode(' or ', $types),
140 1
            $this->predictTokenType(),
141 1
            $token['line'],
142 1
            $this->file ? ' in file: ' . $this->file : ''
143 1
        ));
144
    }
145
146
    /**
147
     * Returns next token if it's type equals to expected.
148
     *
149
     * @param string $type Token type
150
     *
151
     * @return null|array
152
     */
153 35
    protected function acceptTokenType($type)
154
    {
155 35
        if ($type !== $this->predictTokenType()) {
156
            return null;
157
        }
158
159 35
        return $this->lexer->getAdvancedToken();
160
    }
161
162
    /**
163
     * Returns next token type without real input reading (prediction).
164
     *
165
     * @return string
166
     */
167 52
    protected function predictTokenType()
168
    {
169 52
        $token = $this->lexer->predictToken();
170
171 52
        return $token['type'];
172
    }
173
174
    /**
175
     * Parses current expression & returns Node.
176
     *
177
     * @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
178
     *
179
     * @throws ParserException
180
     */
181 52
    protected function parseExpression()
182
    {
183 52
        switch ($type = $this->predictTokenType()) {
184 52
            case 'Feature':
185 51
                return $this->parseFeature();
186 52
            case 'Background':
187 10
                return $this->parseBackground();
188 52
            case 'Scenario':
189 38
                return $this->parseScenario();
190 52
            case 'Outline':
191 17
                return $this->parseOutline();
192 52
            case 'Examples':
193 16
                return $this->parseExamples();
194 52
            case 'TableRow':
195 3
                return $this->parseTable();
196 52
            case 'PyStringOp':
197 10
                return $this->parsePyString();
198 52
            case 'Step':
199 39
                return $this->parseStep();
200 47
            case 'Text':
201 29
                return $this->parseText();
202 47
            case 'Newline':
203 45
                return $this->parseNewline();
204 19
            case 'Tag':
205 4
                return $this->parseTags();
206 15
            case 'Comment':
207 8
                return $this->parseComment();
208 11
            case 'Language':
209 9
                return $this->parseLanguage();
210 2
            case 'EOS':
211 2
                return '';
212
        }
213
214
        throw new ParserException(sprintf('Unknown token type: %s', $type));
215
    }
216
217
    /**
218
     * Parses feature token & returns it's node.
219
     *
220
     * @return FeatureNode
221
     *
222
     * @throws ParserException
223
     */
224 51
    protected function parseFeature()
225
    {
226 51
        $token = $this->expectTokenType('Feature');
227
228 51
        $title = trim($token['value']) ?: null;
229 51
        $description = null;
230 51
        $tags = $this->popTags();
231 51
        $background = null;
232 51
        $scenarios = array();
233 51
        $keyword = $token['keyword'];
234 51
        $language = $this->lexer->getLanguage();
235 51
        $file = $this->file;
236 51
        $line = $token['line'];
237
238
        // Parse description, background, scenarios & outlines
239 51
        while ('EOS' !== $this->predictTokenType()) {
240 51
            $node = $this->parseExpression();
241
242 51 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
243 43
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
244 43
                $description .= (null !== $description ? "\n" : '') . $text;
245 43
                continue;
246
            }
247
248 42
            if (!$background && $node instanceof BackgroundNode) {
249 9
                $background = $node;
250 9
                continue;
251
            }
252
253 42
            if ($node instanceof ScenarioInterface) {
254 40
                $scenarios[] = $node;
255 40
                continue;
256
            }
257
258 3
            if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
259 1
                throw new ParserException(sprintf(
260 1
                    'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
261 1
                    $background->getLine(),
262 1
                    $node->getLine(),
263 1
                    $this->file ? ' in file: ' . $this->file : ''
264 1
                ));
265
            }
266
267 2 View Code Duplication
            if (!$node instanceof ScenarioNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
268 2
                throw new ParserException(sprintf(
269 2
                    'Expected Scenario, Outline or Background, but got %s on line: %d%s',
270 2
                    $node->getNodeType(),
271 2
                    $node->getLine(),
272 2
                    $this->file ? ' in file: ' . $this->file : ''
273 2
                ));
274
            }
275
        }
276
277 44
        return new FeatureNode(
278 44
            rtrim($title) ?: null,
279 44
            rtrim($description) ?: null,
280 44
            $tags,
281 44
            $background,
282 44
            $scenarios,
283 44
            $keyword,
284 44
            $language,
285 44
            $file,
286
            $line
287 44
        );
288
    }
289
290
    /**
291
     * Parses background token & returns it's node.
292
     *
293
     * @return BackgroundNode
294
     *
295
     * @throws ParserException
296
     */
297 10
    protected function parseBackground()
298
    {
299 10
        $token = $this->expectTokenType('Background');
300
301 10
        $title = trim($token['value']);
302 10
        $keyword = $token['keyword'];
303 10
        $line = $token['line'];
304
305 10
        if (count($this->popTags())) {
306 1
            throw new ParserException(sprintf(
307 1
                'Background can not be tagged, but it is on line: %d%s',
308 1
                $line,
309 1
                $this->file ? ' in file: ' . $this->file : ''
310 1
            ));
311
        }
312
313
        // Parse description and steps
314 9
        $steps = array();
315 9
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
316 9
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
317 9
            $node = $this->parseExpression();
318
319 9
            if ($node instanceof StepNode) {
320 8
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
321 8
                continue;
322
            }
323
324 4 View Code Duplication
            if (!count($steps) && is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
325 4
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
326 4
                $title .= "\n" . $text;
327 4
                continue;
328
            }
329
330
            if ("\n" === $node) {
331
                continue;
332
            }
333
334 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
335
                throw new ParserException(sprintf(
336
                    'Expected Step, but got text: "%s"%s',
337
                    $node,
338
                    $this->file ? ' in file: ' . $this->file : ''
339
                ));
340
            }
341
342 View Code Duplication
            if (!$node instanceof StepNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
343
                throw new ParserException(sprintf(
344
                    'Expected Step, but got %s on line: %d%s',
345
                    $node->getNodeType(),
346
                    $node->getLine(),
347
                    $this->file ? ' in file: ' . $this->file : ''
348
                ));
349
            }
350
        }
351
352 9
        return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
353
    }
354
355
    /**
356
     * Parses scenario token & returns it's node.
357
     *
358
     * @return ScenarioNode
359
     *
360
     * @throws ParserException
361
     */
362 38
    protected function parseScenario()
363
    {
364 38
        $token = $this->expectTokenType('Scenario');
365
366 38
        $title = trim($token['value']);
367 38
        $tags = $this->popTags();
368 38
        $keyword = $token['keyword'];
369 38
        $line = $token['line'];
370
371
        // Parse description and steps
372 38
        $steps = array();
373 38
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
374 37
            $node = $this->parseExpression();
375
376 36
            if ($node instanceof StepNode) {
377 32
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
378 32
                continue;
379
            }
380
381 12 View Code Duplication
            if (!count($steps) && is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
382 10
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
383 10
                $title .= "\n" . $text;
384 10
                continue;
385
            }
386
387 2
            if ("\n" === $node) {
388
                continue;
389
            }
390
391 2 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
392 2
                throw new ParserException(sprintf(
393 2
                    'Expected Step, but got text: "%s"%s',
394 2
                    $node,
395 2
                    $this->file ? ' in file: ' . $this->file : ''
396 2
                ));
397
            }
398
399 View Code Duplication
            if (!$node instanceof StepNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
400
                throw new ParserException(sprintf(
401
                    'Expected Step, but got %s on line: %d%s',
402
                    $node->getNodeType(),
403
                    $node->getLine(),
404
                    $this->file ? ' in file: ' . $this->file : ''
405
                ));
406
            }
407
        }
408
409 35
        return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
410
    }
411
412
    /**
413
     * Parses scenario outline token & returns it's node.
414
     *
415
     * @return OutlineNode
416
     *
417
     * @throws ParserException
418
     */
419 17
    protected function parseOutline()
420
    {
421 17
        $token = $this->expectTokenType('Outline');
422
423 17
        $title = trim($token['value']);
424 17
        $tags = $this->popTags();
425 17
        $keyword = $token['keyword'];
426 17
        $examples = null;
427 17
        $line = $token['line'];
428
429
        // Parse description, steps and examples
430 17
        $steps = array();
431 17
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
432 16
            $node = $this->parseExpression();
433
434 16
            if ($node instanceof StepNode) {
435 15
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
436 15
                continue;
437
            }
438
439 16
            if ($node instanceof ExampleTableNode) {
440 16
                $examples = $node;
441 16
                continue;
442
            }
443
444 6 View Code Duplication
            if (!count($steps) && is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
445 6
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
446 6
                $title .= "\n" . $text;
447 6
                continue;
448
            }
449
450
            if ("\n" === $node) {
451
                continue;
452
            }
453
454 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
455
                throw new ParserException(sprintf(
456
                    'Expected Step or Examples table, but got text: "%s"%s',
457
                    $node,
458
                    $this->file ? ' in file: ' . $this->file : ''
459
                ));
460
            }
461
462 View Code Duplication
            if (!$node instanceof StepNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
463
                throw new ParserException(sprintf(
464
                    'Expected Step or Examples table, but got %s on line: %d%s',
465
                    $node->getNodeType(),
466
                    $node->getLine(),
467
                    $this->file ? ' in file: ' . $this->file : ''
468
                ));
469
            }
470
        }
471
472 17
        if (null === $examples) {
473 1
            throw new ParserException(sprintf(
474 1
                'Outline should have examples table, but got none for outline "%s" on line: %d%s',
475 1
                rtrim($title),
476 1
                $line,
477 1
                $this->file ? ' in file: ' . $this->file : ''
478 1
            ));
479
        }
480
481 16
        return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
482
    }
483
484
    /**
485
     * Parses step token & returns it's node.
486
     *
487
     * @return StepNode
488
     */
489 39
    protected function parseStep()
490
    {
491 39
        $token = $this->expectTokenType('Step');
492
493 39
        $keyword = $token['value'];
494 39
        $keywordType = $token['keyword_type'];
495 39
        $text = trim($token['text']);
496 39
        $line = $token['line'];
497
498 39
        $arguments = array();
499 39
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
500 35
            if ('Comment' === $predicted || 'Newline' === $predicted) {
501 33
                $this->acceptTokenType($predicted);
502 33
                continue;
503
            }
504
505 12
            $node = $this->parseExpression();
506
507 11
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
508 11
                $arguments[] = $node;
509 11
            }
510 11
        }
511
512 38
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
513
    }
514
515
    /**
516
     * Parses examples table node.
517
     *
518
     * @return ExampleTableNode
519
     */
520 16
    protected function parseExamples()
521
    {
522 16
        $token = $this->expectTokenType('Examples');
523
524 16
        $keyword = $token['keyword'];
525
526 16
        return new ExampleTableNode($this->parseTableRows(), $keyword);
527
    }
528
529
    /**
530
     * Parses table token & returns it's node.
531
     *
532
     * @return TableNode
533
     */
534 3
    protected function parseTable()
535
    {
536 3
        return new TableNode($this->parseTableRows());
537
    }
538
539
    /**
540
     * Parses PyString token & returns it's node.
541
     *
542
     * @return PyStringNode
543
     */
544 10
    protected function parsePyString()
545
    {
546 10
        $token = $this->expectTokenType('PyStringOp');
547
548 10
        $line = $token['line'];
549
550 10
        $strings = array();
551 10
        while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
552 10
            $token = $this->expectTokenType('Text');
553
554 10
            $strings[] = $token['value'];
555 10
        }
556
557 10
        $this->expectTokenType('PyStringOp');
558
559 9
        return new PyStringNode($strings, $line);
560
    }
561
562
    /**
563
     * Parses tags.
564
     *
565
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
566
     */
567 4
    protected function parseTags()
568
    {
569 4
        $token = $this->expectTokenType('Tag');
570 4
        $this->tags = array_merge($this->tags, $token['tags']);
571
572 4
        return $this->parseExpression();
573
    }
574
575
    /**
576
     * Returns current set of tags and clears tag buffer.
577
     *
578
     * @return array
579
     */
580 51
    protected function popTags()
581
    {
582 51
        $tags = $this->tags;
583 51
        $this->tags = array();
584
585 51
        return $tags;
586
    }
587
588
    /**
589
     * Parses next text line & returns it.
590
     *
591
     * @return string
592
     */
593 29
    protected function parseText()
594
    {
595 29
        $token = $this->expectTokenType('Text');
596
597 29
        return $token['value'];
598
    }
599
600
    /**
601
     * Parses next newline & returns \n.
602
     *
603
     * @return string
604
     */
605 45
    protected function parseNewline()
606
    {
607 45
        $this->expectTokenType('Newline');
608
609 45
        return "\n";
610
    }
611
612
    /**
613
     * Parses next comment token & returns it's string content.
614
     *
615
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
616
     */
617 8
    protected function parseComment()
618
    {
619 8
        $this->expectTokenType('Comment');
620
621 8
        return $this->parseExpression();
622
    }
623
624
    /**
625
     * Parses language block and updates lexer configuration based on it.
626
     *
627
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
628
     *
629
     * @throws ParserException
630
     */
631 9
    protected function parseLanguage()
632
    {
633 9
        $token = $this->expectTokenType('Language');
634
635 9
        if (null === $this->languageSpecifierLine) {
636 9
            $this->lexer->analyse($this->input, $token['value']);
637 9
            $this->languageSpecifierLine = $token['line'];
638 9
        } elseif ($token['line'] !== $this->languageSpecifierLine) {
639 1
            throw new ParserException(sprintf(
640 1
                'Ambiguous language specifiers on lines: %d and %d%s',
641 1
                $this->languageSpecifierLine,
642 1
                $token['line'],
643 1
                $this->file ? ' in file: ' . $this->file : ''
644 1
            ));
645
        }
646
647 9
        return $this->parseExpression();
648
    }
649
650
    /**
651
     * Parses the rows of a table
652
     *
653
     * @return string[][]
654
     */
655 17
    private function parseTableRows()
656
    {
657 17
        $table = array();
658 17
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
659 17
            if ('Comment' === $predicted || 'Newline' === $predicted) {
660 11
                $this->acceptTokenType($predicted);
661 11
                continue;
662
            }
663
664 17
            $token = $this->expectTokenType('TableRow');
665
666 17
            $table[$token['line']] = $token['columns'];
667 17
        }
668
669 17
        return $table;
670
    }
671
672
    /**
673
     * Changes step node type for types But, And to type of previous step if it exists else sets to Given
674
     *
675
     * @param StepNode   $node
676
     * @param StepNode[] $steps
677
     * @return StepNode
678
     */
679 38
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
680
    {
681 38
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
682 14
            if (($prev = end($steps))) {
683 14
                $keywordType = $prev->getKeywordType();
684 14
            } else {
685
                $keywordType = 'Given';
686
            }
687
688 14
            $node = new StepNode(
689 14
                $node->getKeyword(),
690 14
                $node->getText(),
691 14
                $node->getArguments(),
692 14
                $node->getLine(),
693
                $keywordType
694 14
            );
695 14
        }
696 38
        return $node;
697
    }
698
}
699