Completed
Pull Request — master (#119)
by
unknown
02:35
created

Parser::expectTokenType()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
ccs 12
cts 12
cp 1
rs 9.4285
cc 3
eloc 11
nc 2
nop 1
crap 3
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
    private $stack = array();
43
44
    /**
45
     * Initializes parser.
46
     *
47
     * @param Lexer $lexer Lexer instance
48
     */
49 56
    public function __construct(Lexer $lexer)
50
    {
51 56
        $this->lexer = $lexer;
52 56
    }
53
54
    /**
55
     * Parses input & returns features array.
56
     *
57
     * @param string $input Gherkin string document
58
     * @param string $file  File name
59
     *
60
     * @return FeatureNode|null
61
     *
62
     * @throws ParserException
63
     */
64 54
    public function parse($input, $file = null)
65
    {
66 54
        $this->languageSpecifierLine = null;
67 54
        $this->input = $input;
68 54
        $this->file = $file;
69 54
        $this->tags = array();
70
71
        try {
72 54
            $this->lexer->analyse($this->input, 'en');
73 54
        } catch (LexerException $e) {
74
            throw new ParserException(
75
                sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
76
                0,
77
                $e
78
            );
79
        }
80
81 54
        $feature = null;
82 54
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
83 54
            $node = $this->parseExpression();
84
85 46
            if (null === $node || "\n" === $node) {
86 3
                continue;
87
            }
88
89 45
            if (!$feature && $node instanceof FeatureNode) {
90 45
                $feature = $node;
91 45
                continue;
92
            }
93
94
            if ($feature && $node instanceof FeatureNode) {
95
                throw new ParserException(sprintf(
96
                    'Only one feature is allowed per feature file. But %s got multiple.',
97
                    $this->file
98
                ));
99
            }
100
101 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...
102
                throw new ParserException(sprintf(
103
                    'Expected Feature, but got text: "%s"%s',
104
                    $node,
105
                    $this->file ? ' in file: ' . $this->file : ''
106
                ));
107
            }
108
109
            if (!$node instanceof FeatureNode) {
110
                throw new ParserException(sprintf(
111
                    'Expected Feature, but got %s on line: %d%s',
112
                    $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...
113
                    $node->getLine(),
114
                    $this->file ? ' in file: ' . $this->file : ''
115
                ));
116
            }
117
        }
118
119 45
        return $feature;
120
    }
121
122
    /**
123
     * Returns next token if it's type equals to expected.
124
     *
125
     * @param string $type Token type
126
     *
127
     * @return array
128
     *
129
     * @throws Exception\ParserException
130
     */
131 54
    protected function expectTokenType($type)
132
    {
133 54
        $types = (array) $type;
134 54
        if (in_array($this->predictTokenType(), $types)) {
135 54
            return $this->lexer->getAdvancedToken();
136
        }
137
138 1
        $token = $this->lexer->predictToken();
139
140 1
        throw new ParserException(sprintf(
141 1
            'Expected %s token, but got %s on line: %d%s',
142 1
            implode(' or ', $types),
143 1
            $this->predictTokenType(),
144 1
            $token['line'],
145 1
            $this->file ? ' in file: ' . $this->file : ''
146 1
        ));
147
    }
148
149
    /**
150
     * Returns next token if it's type equals to expected.
151
     *
152
     * @param string $type Token type
153
     *
154
     * @return null|array
155
     */
156 36
    protected function acceptTokenType($type)
157
    {
158 36
        if ($type !== $this->predictTokenType()) {
159
            return null;
160
        }
161
162 36
        return $this->lexer->getAdvancedToken();
163
    }
164
165
    /**
166
     * Returns next token type without real input reading (prediction).
167
     *
168
     * @return string
169
     */
170 54
    protected function predictTokenType()
171
    {
172 54
        $token = $this->lexer->predictToken();
173
174 54
        return $token['type'];
175
    }
176
177
    /**
178
     * Parses current expression & returns Node.
179
     *
180
     * @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
181
     *
182
     * @throws ParserException
183
     */
184 54
    protected function parseExpression()
185
    {
186 54
        switch ($type = $this->predictTokenType()) {
187 54
            case 'Feature':
188 53
                return $this->parseFeature();
189 54
            case 'Background':
190 10
                return $this->parseBackground();
191 54
            case 'Scenario':
192 39
                return $this->parseScenario();
193 54
            case 'Outline':
194 18
                return $this->parseOutline();
195 54
            case 'Examples':
196 17
                return $this->parseExamples();
197 54
            case 'TableRow':
198 3
                return $this->parseTable();
199 54
            case 'PyStringOp':
200 10
                return $this->parsePyString();
201 54
            case 'Step':
202 41
                return $this->parseStep();
203 53
            case 'Text':
204 29
                return $this->parseText();
205 53
            case 'Newline':
206 46
                return $this->parseNewline();
207 25
            case 'Tag':
208 10
                return $this->parseTags();
209 16
            case 'Comment':
210 8
                return $this->parseComment();
211 12
            case 'Language':
212 9
                return $this->parseLanguage();
213 3
            case 'EOS':
214 3
                return '';
215
        }
216
217
        throw new ParserException(sprintf('Unknown token type: %s', $type));
218
    }
219
220
    /**
221
     * Parses feature token & returns it's node.
222
     *
223
     * @return FeatureNode
224
     *
225
     * @throws ParserException
226
     */
227 53
    protected function parseFeature()
228
    {
229 53
        $token = $this->expectTokenType('Feature');
230
231 53
        $title = trim($token['value']) ?: null;
232 53
        $description = null;
233 53
        $tags = $this->popTags();
234 53
        $background = null;
235 53
        $scenarios = array();
236 53
        $keyword = $token['keyword'];
237 53
        $language = $this->lexer->getLanguage();
238 53
        $file = $this->file;
239 53
        $line = $token['line'];
240
241 53
        array_push($this->stack, 'Feature');
242
243
        // Parse description, background, scenarios & outlines
244 53
        while ('EOS' !== $this->predictTokenType()) {
245 53
            $node = $this->parseExpression();
246
247 53 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...
248 45
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
249 45
                $description .= (null !== $description ? "\n" : '') . $text;
250 45
                continue;
251
            }
252
253 44
            if (!$background && $node instanceof BackgroundNode) {
254 9
                $background = $node;
255 9
                continue;
256
            }
257
258 44
            if ($node instanceof ScenarioInterface) {
259 42
                $scenarios[] = $node;
260 42
                continue;
261
            }
262
263 3
            if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
264 1
                throw new ParserException(sprintf(
265 1
                    'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
266 1
                    $background->getLine(),
267 1
                    $node->getLine(),
268 1
                    $this->file ? ' in file: ' . $this->file : ''
269 1
                ));
270
            }
271
272 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...
273 2
                throw new ParserException(sprintf(
274 2
                    'Expected Scenario, Outline or Background, but got %s on line: %d%s',
275 2
                    $node->getNodeType(),
276 2
                    $node->getLine(),
277 2
                    $this->file ? ' in file: ' . $this->file : ''
278 2
                ));
279
            }
280
        }
281
282 46
        return new FeatureNode(
283 46
            rtrim($title) ?: null,
284 46
            rtrim($description) ?: null,
285 46
            $tags,
286 46
            $background,
287 46
            $scenarios,
288 46
            $keyword,
289 46
            $language,
290 46
            $file,
291
            $line
292 46
        );
293
    }
294
295
    /**
296
     * Parses background token & returns it's node.
297
     *
298
     * @return BackgroundNode
299
     *
300
     * @throws ParserException
301
     */
302 10
    protected function parseBackground()
303
    {
304 10
        $token = $this->expectTokenType('Background');
305
306 10
        $title = trim($token['value']);
307 10
        $keyword = $token['keyword'];
308 10
        $line = $token['line'];
309
310 10
        if (count($this->popTags())) {
311 1
            throw new ParserException(sprintf(
312 1
                'Background can not be tagged, but it is on line: %d%s',
313 1
                $line,
314 1
                $this->file ? ' in file: ' . $this->file : ''
315 1
            ));
316
        }
317
318
        // Parse description and steps
319 9
        $steps = array();
320 9
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
321 9
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
322 9
            $node = $this->parseExpression();
323
324 9
            if ($node instanceof StepNode) {
325 8
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
326 8
                continue;
327
            }
328
329 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...
330 4
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
331 4
                $title .= "\n" . $text;
332 4
                continue;
333
            }
334
335
            if ("\n" === $node) {
336
                continue;
337
            }
338
339 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...
340
                throw new ParserException(sprintf(
341
                    'Expected Step, but got text: "%s"%s',
342
                    $node,
343
                    $this->file ? ' in file: ' . $this->file : ''
344
                ));
345
            }
346
347 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...
348
                throw new ParserException(sprintf(
349
                    'Expected Step, but got %s on line: %d%s',
350
                    $node->getNodeType(),
351
                    $node->getLine(),
352
                    $this->file ? ' in file: ' . $this->file : ''
353
                ));
354
            }
355
        }
356
357 9
        return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
358
    }
359
360
    /**
361
     * Parses scenario token & returns it's node.
362
     *
363
     * @return ScenarioNode
364
     *
365
     * @throws ParserException
366
     */
367 39
    protected function parseScenario()
368
    {
369 39
        $token = $this->expectTokenType('Scenario');
370
371 39
        $title = trim($token['value']);
372 39
        $tags = $this->popTags();
373 39
        $keyword = $token['keyword'];
374 39
        $line = $token['line'];
375
376 39
        array_push($this->stack, 'Scenario');
377
378
        // Parse description and steps
379 39
        $steps = array();
380 39
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
381 38
            $node = $this->parseExpression();
382
383 37
            if ($node instanceof StepNode) {
384 33
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
385 33
                continue;
386
            }
387
388 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...
389 10
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
390 10
                $title .= "\n" . $text;
391 10
                continue;
392
            }
393
394 2
            if ("\n" === $node) {
395
                continue;
396
            }
397
398 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...
399 2
                throw new ParserException(sprintf(
400 2
                    'Expected Step, but got text: "%s"%s',
401 2
                    $node,
402 2
                    $this->file ? ' in file: ' . $this->file : ''
403 2
                ));
404
            }
405
406 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...
407
                throw new ParserException(sprintf(
408
                    'Expected Step, but got %s on line: %d%s',
409
                    $node->getNodeType(),
410
                    $node->getLine(),
411
                    $this->file ? ' in file: ' . $this->file : ''
412
                ));
413
            }
414
        }
415
416 36
        array_pop($this->stack);
417
418 36
        return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
419
    }
420
421
    /**
422
     * Parses scenario outline token & returns it's node.
423
     *
424
     * @return OutlineNode
425
     *
426
     * @throws ParserException
427
     */
428 18
    protected function parseOutline()
429
    {
430 18
        $token = $this->expectTokenType('Outline');
431
432 18
        $title = trim($token['value']);
433 18
        $tags = $this->popTags();
434 18
        $keyword = $token['keyword'];
435
436
        /** @var ExampleTableNode $examples */
437 18
        $examples = array();
438 18
        $line = $token['line'];
439
440
        // Parse description, steps and examples
441 18
        $steps = array();
442
443 18
        array_push($this->stack, 'Outline');
444
445 18
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'))) {
446 17
            $node = $this->parseExpression();
447
448 17
            if ($node instanceof StepNode) {
449 16
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
450 16
                continue;
451
            }
452
453 17
            if ($node instanceof ExampleTableNode) {
454 17
                $examples[] = $node;
455
456 17
                continue;
457
            }
458
459 7 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...
460 6
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
461 6
                $title .= "\n" . $text;
462 6
                continue;
463
            }
464
465 1
            if ("\n" === $node) {
466 1
                continue;
467
            }
468
469 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...
470
                throw new ParserException(sprintf(
471
                    'Expected Step or Examples table, but got text: "%s"%s',
472
                    $node,
473
                    $this->file ? ' in file: ' . $this->file : ''
474
                ));
475
            }
476
477 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...
478
                throw new ParserException(sprintf(
479
                    'Expected Step or Examples table, but got %s on line: %d%s',
480
                    $node->getNodeType(),
481
                    $node->getLine(),
482
                    $this->file ? ' in file: ' . $this->file : ''
483
                ));
484
            }
485
        }
486
487 18
        if (empty($examples)) {
488 1
            throw new ParserException(sprintf(
489 1
                'Outline should have examples table, but got none for outline "%s" on line: %d%s',
490 1
                rtrim($title),
491 1
                $line,
492 1
                $this->file ? ' in file: ' . $this->file : ''
493 1
            ));
494
        }
495
496 17
        return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
0 ignored issues
show
Documentation introduced by
$examples is of type object<Behat\Gherkin\Node\ExampleTableNode>, but the function expects a array<integer,object<Beh...Node\ExampleTableNode>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
497
    }
498
499
    /**
500
     * Parses step token & returns it's node.
501
     *
502
     * @return StepNode
503
     */
504 41
    protected function parseStep()
505
    {
506 41
        $token = $this->expectTokenType('Step');
507
508 41
        $keyword = $token['value'];
509 41
        $keywordType = $token['keyword_type'];
510 41
        $text = trim($token['text']);
511 41
        $line = $token['line'];
512
513 41
        array_push($this->stack, 'Step');
514
515 41
        $arguments = array();
516 41
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
517 36
            if ('Comment' === $predicted || 'Newline' === $predicted) {
518 34
                $this->acceptTokenType($predicted);
519 34
                continue;
520
            }
521
522 12
            $node = $this->parseExpression();
523
524 11
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
525 11
                $arguments[] = $node;
526 11
            }
527 11
        }
528
529 40
        array_pop($this->stack);
530
531 40
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
532
    }
533
534
    /**
535
     * Parses examples table node.
536
     *
537
     * @return ExampleTableNode
538
     */
539 17
    protected function parseExamples()
540
    {
541 17
        $token = $this->expectTokenType('Examples');
542
543 17
        $keyword = $token['keyword'];
544
545 17
        $tags = empty($this->tags) ? array() : $this->popTags();
546
547 17
        return new ExampleTableNode($this->parseTableRows(), $keyword, $tags);
548
    }
549
550
    /**
551
     * Parses table token & returns it's node.
552
     *
553
     * @return TableNode
554
     */
555 3
    protected function parseTable()
556
    {
557 3
        return new TableNode($this->parseTableRows());
558
    }
559
560
    /**
561
     * Parses PyString token & returns it's node.
562
     *
563
     * @return PyStringNode
564
     */
565 10
    protected function parsePyString()
566
    {
567 10
        $token = $this->expectTokenType('PyStringOp');
568
569 10
        $line = $token['line'];
570
571 10
        $strings = array();
572 10
        while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
573 10
            $token = $this->expectTokenType('Text');
574
575 10
            $strings[] = $token['value'];
576 10
        }
577
578 10
        $this->expectTokenType('PyStringOp');
579
580 9
        return new PyStringNode($strings, $line);
581
    }
582
583
    /**
584
     * Parses tags.
585
     *
586
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
587
     */
588 10
    protected function parseTags()
589
    {
590 10
        $token = $this->expectTokenType('Tag');
591 10
        $this->tags = array_merge($this->tags, $token['tags']);
592
593
        $possibleTransitions = array(
594
            'Outline' => array(
595 10
                'Examples',
596
                'Step'
597 10
            )
598 10
        );
599
600 10
        $currentType = '-1';
601
        // check if that is ok to go inside:
602 10
        if (!empty($this->stack)) {
603 10
            $currentType = $this->stack[count($this->stack) - 1];
604 10
        }
605 10
        $nextType = $this->predictTokenType();
606 10
        if (!isset($possibleTransitions[$currentType]) || in_array($nextType, $possibleTransitions[$currentType])) {
607 10
            return $this->parseExpression();
608
        }
609
610 1
        return "\n";
611
    }
612
613
    /**
614
     * Returns current set of tags and clears tag buffer.
615
     *
616
     * @return array
617
     */
618 53
    protected function popTags()
619
    {
620 53
        $tags = $this->tags;
621 53
        $this->tags = array();
622
623 53
        return $tags;
624
    }
625
626
    /**
627
     * Parses next text line & returns it.
628
     *
629
     * @return string
630
     */
631 29
    protected function parseText()
632
    {
633 29
        $token = $this->expectTokenType('Text');
634
635 29
        return $token['value'];
636
    }
637
638
    /**
639
     * Parses next newline & returns \n.
640
     *
641
     * @return string
642
     */
643 46
    protected function parseNewline()
644
    {
645 46
        $this->expectTokenType('Newline');
646
647 46
        return "\n";
648
    }
649
650
    /**
651
     * Parses next comment token & returns it's string content.
652
     *
653
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
654
     */
655 8
    protected function parseComment()
656
    {
657 8
        $this->expectTokenType('Comment');
658
659 8
        return $this->parseExpression();
660
    }
661
662
    /**
663
     * Parses language block and updates lexer configuration based on it.
664
     *
665
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
666
     *
667
     * @throws ParserException
668
     */
669 9
    protected function parseLanguage()
670
    {
671 9
        $token = $this->expectTokenType('Language');
672
673 9
        if (null === $this->languageSpecifierLine) {
674 9
            $this->lexer->analyse($this->input, $token['value']);
675 9
            $this->languageSpecifierLine = $token['line'];
676 9
        } elseif ($token['line'] !== $this->languageSpecifierLine) {
677 1
            throw new ParserException(sprintf(
678 1
                'Ambiguous language specifiers on lines: %d and %d%s',
679 1
                $this->languageSpecifierLine,
680 1
                $token['line'],
681 1
                $this->file ? ' in file: ' . $this->file : ''
682 1
            ));
683
        }
684
685 9
        return $this->parseExpression();
686
    }
687
688
    /**
689
     * Parses the rows of a table
690
     *
691
     * @return string[][]
692
     */
693 18
    private function parseTableRows()
694
    {
695 18
        $table = array();
696 18
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
697 18
            if ('Comment' === $predicted || 'Newline' === $predicted) {
698 16
                $this->acceptTokenType($predicted);
699 16
                continue;
700
            }
701
702 18
            $token = $this->expectTokenType('TableRow');
703
704 18
            $table[$token['line']] = $token['columns'];
705 18
        }
706
707 18
        return $table;
708
    }
709
710
    /**
711
     * Changes step node type for types But, And to type of previous step if it exists else sets to Given
712
     *
713
     * @param StepNode   $node
714
     * @param StepNode[] $steps
715
     * @return StepNode
716
     */
717 40
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
718
    {
719 40
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
720 14
            if (($prev = end($steps))) {
721 14
                $keywordType = $prev->getKeywordType();
722 14
            } else {
723
                $keywordType = 'Given';
724
            }
725
726 14
            $node = new StepNode(
727 14
                $node->getKeyword(),
728 14
                $node->getText(),
729 14
                $node->getArguments(),
730 14
                $node->getLine(),
731
                $keywordType
732 14
            );
733 14
        }
734 40
        return $node;
735
    }
736
}
737