Completed
Push — master ( cd607a...5ed365 )
by Christophe
12:54 queued 11:29
created

Parser::parseExpression()   C

Complexity

Conditions 15
Paths 15

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 15.0549

Importance

Changes 0
Metric Value
dl 0
loc 35
ccs 30
cts 32
cp 0.9375
rs 5.9166
c 0
b 0
f 0
cc 15
nc 15
nop 0
crap 15.0549

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 243
    public function __construct(Lexer $lexer)
48
    {
49 243
        $this->lexer = $lexer;
50 243
    }
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 241
    public function parse($input, $file = null)
63
    {
64 241
        $this->languageSpecifierLine = null;
65 241
        $this->input = $input;
66 241
        $this->file = $file;
67 241
        $this->tags = array();
68
69
        try {
70 241
            $this->lexer->analyse($this->input, 'en');
71 241
        } 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 241
        $feature = null;
80 241
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
81 241
            $node = $this->parseExpression();
82
83 233
            if (null === $node || "\n" === $node) {
84 3
                continue;
85
            }
86
87 232
            if (!$feature && $node instanceof FeatureNode) {
88 232
                $feature = $node;
89 232
                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 232
        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 241
    protected function expectTokenType($type)
130
    {
131 241
        $types = (array) $type;
132 241
        if (in_array($this->predictTokenType(), $types)) {
133 241
            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 1
        ));
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 241
    protected function predictTokenType()
169
    {
170 241
        $token = $this->lexer->predictToken();
171
172 241
        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 241
    protected function parseExpression()
183
    {
184 241
        switch ($type = $this->predictTokenType()) {
185 241
            case 'Feature':
186 240
                return $this->parseFeature();
187 241
            case 'Background':
188 198
                return $this->parseBackground();
189 241
            case 'Scenario':
190 227
                return $this->parseScenario();
191 241
            case 'Outline':
192 205
                return $this->parseOutline();
193 241
            case 'Examples':
194 204
                return $this->parseExamples();
195 241
            case 'TableRow':
196 3
                return $this->parseTable();
197 241
            case 'PyStringOp':
198 10
                return $this->parsePyString();
199 241
            case 'Step':
200 228
                return $this->parseStep();
201 236
            case 'Text':
202 217
                return $this->parseText();
203 236
            case 'Newline':
204 233
                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 240
    protected function parseFeature()
226
    {
227 240
        $token = $this->expectTokenType('Feature');
228
229 240
        $title = trim($token['value']) ?: null;
230 240
        $description = null;
231 240
        $tags = $this->popTags();
232 240
        $background = null;
233 240
        $scenarios = array();
234 240
        $keyword = $token['keyword'];
235 240
        $language = $this->lexer->getLanguage();
236 240
        $file = $this->file;
237 240
        $line = $token['line'];
238
239
        // Parse description, background, scenarios & outlines
240 240
        while ('EOS' !== $this->predictTokenType()) {
241 240
            $node = $this->parseExpression();
242
243 240 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 231
            if (!$background && $node instanceof BackgroundNode) {
250 197
                $background = $node;
251 197
                continue;
252
            }
253
254 231
            if ($node instanceof ScenarioInterface) {
255 229
                $scenarios[] = $node;
256 229
                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 1
                ));
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 2
                ));
275
            }
276
        }
277
278 233
        return new FeatureNode(
279 233
            rtrim($title) ?: null,
280 233
            rtrim($description) ?: null,
281 233
            $tags,
282 233
            $background,
283 233
            $scenarios,
284 233
            $keyword,
285 233
            $language,
286 233
            $file,
287
            $line
288 233
        );
289
    }
290
291
    /**
292
     * Parses background token & returns it's node.
293
     *
294
     * @return BackgroundNode
295
     *
296
     * @throws ParserException
297
     */
298 198
    protected function parseBackground()
299
    {
300 198
        $token = $this->expectTokenType('Background');
301
302 198
        $title = trim($token['value']);
303 198
        $keyword = $token['keyword'];
304 198
        $line = $token['line'];
305
306 198
        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 1
            ));
312
        }
313
314
        // Parse description and steps
315 197
        $steps = array();
316 197
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
317 197
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
318 197
            $node = $this->parseExpression();
319
320 197
            if ($node instanceof StepNode) {
321 196
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
322 196
                continue;
323
            }
324
325 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...
326 4
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
327 4
                $title .= "\n" . $text;
328 4
                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 197
        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 227
    protected function parseScenario()
364
    {
365 227
        $token = $this->expectTokenType('Scenario');
366
367 227
        $title = trim($token['value']);
368 227
        $tags = $this->popTags();
369 227
        $keyword = $token['keyword'];
370 227
        $line = $token['line'];
371
372
        // Parse description and steps
373 227
        $steps = array();
374 227
        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 2
                ));
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 224
        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 1
            ));
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 11
            }
511 11
        }
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 10
        }
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 240
    protected function popTags()
582
    {
583 240
        $tags = $this->tags;
584 240
        $this->tags = array();
585
586 240
        return $tags;
587
    }
588
589
    /**
590
     * Parses next text line & returns it.
591
     *
592
     * @return string
593
     */
594 217
    protected function parseText()
595
    {
596 217
        $token = $this->expectTokenType('Text');
597
598 217
        return $token['value'];
599
    }
600
601
    /**
602
     * Parses next newline & returns \n.
603
     *
604
     * @return string
605
     */
606 233
    protected function parseNewline()
607
    {
608 233
        $this->expectTokenType('Newline');
609
610 233
        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 1
            ));
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 205
        }
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 202
            } 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
                $keywordType
695 202
            );
696 202
        }
697 227
        return $node;
698
    }
699
}
700