Completed
Pull Request — master (#160)
by
unknown
02:37
created

Parser::parseScenario()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 49

Duplication

Lines 20
Ratio 40.82 %

Code Coverage

Tests 23
CRAP Score 12.5364

Importance

Changes 0
Metric Value
dl 20
loc 49
ccs 23
cts 30
cp 0.7667
rs 7.3166
c 0
b 0
f 0
cc 11
nc 7
nop 0
crap 12.5364

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