Completed
Push — master ( de4eca...189f7a )
by Konstantin
05:54
created

Parser::parse()   C

Complexity

Conditions 13
Paths 8

Size

Total Lines 57
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 47.6561

Importance

Changes 0
Metric Value
dl 0
loc 57
ccs 16
cts 39
cp 0.4103
rs 6.5962
c 0
b 0
f 0
cc 13
eloc 36
nc 8
nop 2
crap 47.6561

How to fix   Long Method    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 55
    public function __construct(Lexer $lexer)
48
    {
49 55
        $this->lexer = $lexer;
50 55
    }
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 53
    public function parse($input, $file = null)
63
    {
64 53
        $this->languageSpecifierLine = null;
65 53
        $this->input = $input;
66 53
        $this->file = $file;
67 53
        $this->tags = array();
68
69
        try {
70 53
            $this->lexer->analyse($this->input, 'en');
71 53
        } 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 53
        $feature = null;
80 53
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
81 53
            $node = $this->parseExpression();
82
83 45
            if (null === $node || "\n" === $node) {
84 3
                continue;
85
            }
86
87 44
            if (!$feature && $node instanceof FeatureNode) {
88 44
                $feature = $node;
89 44
                continue;
90
            }
91
92
            if ($feature && $node instanceof FeatureNode) {
93
                throw new ParserException(sprintf(
94
                    'Only one feature is allowed per feature file. But %s got multiple.',
95
                    $this->file
96
                ));
97
            }
98
99
            if (is_string($node)) {
100
                throw new ParserException(sprintf(
101
                    'Expected Feature, but got text: "%s"%s',
102
                    $node,
103
                    $this->file ? ' in file: ' . $this->file : ''
104
                ));
105
            }
106
107
            if (!$node instanceof FeatureNode) {
108
                throw new ParserException(sprintf(
109
                    'Expected Feature, but got %s on line: %d%s',
110
                    $node->getKeyword(),
0 ignored issues
show
Bug introduced by
The method getKeyword does only exist in Behat\Gherkin\Node\Backg...t\Gherkin\Node\StepNode, but not in Behat\Gherkin\Node\PyStr...\Gherkin\Node\TableNode.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
111
                    $node->getLine(),
112
                    $this->file ? ' in file: ' . $this->file : ''
113
                ));
114
            }
115
        }
116
117 44
        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 53
    protected function expectTokenType($type)
130
    {
131 53
        $types = (array) $type;
132 53
        if (in_array($this->predictTokenType(), $types)) {
133 53
            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 35
    protected function acceptTokenType($type)
155
    {
156 35
        if ($type !== $this->predictTokenType()) {
157
            return null;
158
        }
159
160 35
        return $this->lexer->getAdvancedToken();
161
    }
162
163
    /**
164
     * Returns next token type without real input reading (prediction).
165
     *
166
     * @return string
167
     */
168 53
    protected function predictTokenType()
169
    {
170 53
        $token = $this->lexer->predictToken();
171
172 53
        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 53
    protected function parseExpression()
183
    {
184 53
        switch ($type = $this->predictTokenType()) {
185 53
            case 'Feature':
186 52
                return $this->parseFeature();
187 53
            case 'Background':
188 10
                return $this->parseBackground();
189 53
            case 'Scenario':
190 39
                return $this->parseScenario();
191 53
            case 'Outline':
192 17
                return $this->parseOutline();
193 53
            case 'Examples':
194 16
                return $this->parseExamples();
195 53
            case 'TableRow':
196 3
                return $this->parseTable();
197 53
            case 'PyStringOp':
198 10
                return $this->parsePyString();
199 53
            case 'Step':
200 40
                return $this->parseStep();
201 48
            case 'Text':
202 29
                return $this->parseText();
203 48
            case 'Newline':
204 45
                return $this->parseNewline();
205 20
            case 'Tag':
206 5
                return $this->parseTags();
207 16
            case 'Comment':
208 8
                return $this->parseComment();
209 12
            case 'Language':
210 9
                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 52
    protected function parseFeature()
226
    {
227 52
        $token = $this->expectTokenType('Feature');
228
229 52
        $title = trim($token['value']) ?: null;
230 52
        $description = null;
231 52
        $tags = $this->popTags();
232 52
        $background = null;
233 52
        $scenarios = array();
234 52
        $keyword = $token['keyword'];
235 52
        $language = $this->lexer->getLanguage();
236 52
        $file = $this->file;
237 52
        $line = $token['line'];
238
239
        // Parse description, background, scenarios & outlines
240 52
        while ('EOS' !== $this->predictTokenType()) {
241 52
            $node = $this->parseExpression();
242
243 52 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 44
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
245 44
                $description .= (null !== $description ? "\n" : '') . $text;
246 44
                continue;
247
            }
248
249 43
            if (!$background && $node instanceof BackgroundNode) {
250 9
                $background = $node;
251 9
                continue;
252
            }
253
254 43
            if ($node instanceof ScenarioInterface) {
255 41
                $scenarios[] = $node;
256 41
                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 45
        return new FeatureNode(
279 45
            rtrim($title) ?: null,
280 45
            rtrim($description) ?: null,
281 45
            $tags,
282 45
            $background,
283 45
            $scenarios,
284 45
            $keyword,
285 45
            $language,
286 45
            $file,
287
            $line
288 45
        );
289
    }
290
291
    /**
292
     * Parses background token & returns it's node.
293
     *
294
     * @return BackgroundNode
295
     *
296
     * @throws ParserException
297
     */
298 10
    protected function parseBackground()
299
    {
300 10
        $token = $this->expectTokenType('Background');
301
302 10
        $title = trim($token['value']);
303 10
        $keyword = $token['keyword'];
304 10
        $line = $token['line'];
305
306 10
        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 9
        $steps = array();
316 9
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
317 9
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
318 9
            $node = $this->parseExpression();
319
320 9
            if ($node instanceof StepNode) {
321 8
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
322 8
                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 9
        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 39
    protected function parseScenario()
364
    {
365 39
        $token = $this->expectTokenType('Scenario');
366
367 39
        $title = trim($token['value']);
368 39
        $tags = $this->popTags();
369 39
        $keyword = $token['keyword'];
370 39
        $line = $token['line'];
371
372
        // Parse description and steps
373 39
        $steps = array();
374 39
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
375 38
            $node = $this->parseExpression();
376
377 37
            if ($node instanceof StepNode) {
378 33
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
379 33
                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 36
        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 17
    protected function parseOutline()
421
    {
422 17
        $token = $this->expectTokenType('Outline');
423
424 17
        $title = trim($token['value']);
425 17
        $tags = $this->popTags();
426 17
        $keyword = $token['keyword'];
427 17
        $examples = null;
428 17
        $line = $token['line'];
429
430
        // Parse description, steps and examples
431 17
        $steps = array();
432 17
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
433 16
            $node = $this->parseExpression();
434
435 16
            if ($node instanceof StepNode) {
436 15
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
437 15
                continue;
438
            }
439
440 16
            if ($node instanceof ExampleTableNode) {
441 16
                $examples = $node;
442 16
                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 17
        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 16
        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 40
    protected function parseStep()
491
    {
492 40
        $token = $this->expectTokenType('Step');
493
494 40
        $keyword = $token['value'];
495 40
        $keywordType = $token['keyword_type'];
496 40
        $text = trim($token['text']);
497 40
        $line = $token['line'];
498
499 40
        $arguments = array();
500 40
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
501 35
            if ('Comment' === $predicted || 'Newline' === $predicted) {
502 33
                $this->acceptTokenType($predicted);
503 33
                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 39
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
514
    }
515
516
    /**
517
     * Parses examples table node.
518
     *
519
     * @return ExampleTableNode
520
     */
521 16
    protected function parseExamples()
522
    {
523 16
        $token = $this->expectTokenType('Examples');
524
525 16
        $keyword = $token['keyword'];
526
527 16
        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 52
    protected function popTags()
582
    {
583 52
        $tags = $this->tags;
584 52
        $this->tags = array();
585
586 52
        return $tags;
587
    }
588
589
    /**
590
     * Parses next text line & returns it.
591
     *
592
     * @return string
593
     */
594 29
    protected function parseText()
595
    {
596 29
        $token = $this->expectTokenType('Text');
597
598 29
        return $token['value'];
599
    }
600
601
    /**
602
     * Parses next newline & returns \n.
603
     *
604
     * @return string
605
     */
606 45
    protected function parseNewline()
607
    {
608 45
        $this->expectTokenType('Newline');
609
610 45
        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 9
    protected function parseLanguage()
633
    {
634 9
        $token = $this->expectTokenType('Language');
635
636 9
        if (null === $this->languageSpecifierLine) {
637 9
            $this->lexer->analyse($this->input, $token['value']);
638 9
            $this->languageSpecifierLine = $token['line'];
639 9
        } 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 9
        return $this->parseExpression();
649
    }
650
651
    /**
652
     * Parses the rows of a table
653
     *
654
     * @return string[][]
655
     */
656 17
    private function parseTableRows()
657
    {
658 17
        $table = array();
659 17
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
660 17
            if ('Comment' === $predicted || 'Newline' === $predicted) {
661 11
                $this->acceptTokenType($predicted);
662 11
                continue;
663
            }
664
665 17
            $token = $this->expectTokenType('TableRow');
666
667 17
            $table[$token['line']] = $token['columns'];
668 17
        }
669
670 17
        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 39
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
681
    {
682 39
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
683 14
            if (($prev = end($steps))) {
684 14
                $keywordType = $prev->getKeywordType();
685 14
            } else {
686
                $keywordType = 'Given';
687
            }
688
689 14
            $node = new StepNode(
690 14
                $node->getKeyword(),
691 14
                $node->getText(),
692 14
                $node->getArguments(),
693 14
                $node->getLine(),
694
                $keywordType
695 14
            );
696 14
        }
697 39
        return $node;
698
    }
699
}
700