Completed
Pull Request — master (#150)
by Matt
02:45
created

Parser::parseRule()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 4

Importance

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