Completed
Pull Request — master (#150)
by Matt
02:43
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 231
            $node = $this->parseExpression();
85
86 230
            if (null === $node || "\n" === $node) {
87 2
                continue;
88
            }
89
90 230
            if (!$feature && $node instanceof FeatureNode) {
91 230
                $feature = $node;
92 230
                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 230
        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 231
    protected function expectTokenType($type)
133
    {
134 231
        $types = (array) $type;
135 231
        if (in_array($this->predictTokenType(), $types)) {
136 231
            return $this->lexer->getAdvancedToken();
137
        }
138
139
        $token = $this->lexer->predictToken();
140
141
        throw new ParserException(sprintf(
142
            'Expected %s token, but got %s on line: %d%s',
143
            implode(' or ', $types),
144
            $this->predictTokenType(),
145
            $token['line'],
146
            $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 221
    protected function acceptTokenType($type)
158
    {
159 221
        if ($type !== $this->predictTokenType()) {
160
            return null;
161
        }
162
163 221
        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 231
        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 231
    protected function parseExpression()
186
    {
187 231
        switch ($type = $this->predictTokenType()) {
188 231
            case 'Feature':
189 230
                return $this->parseFeature();
190 231
            case 'Background':
191 196
                return $this->parseBackground();
192 231
            case 'Rule':
193 1
                return $this->parseRule();
194 231
            case 'Outline':
195 203
                return $this->parseOutline();
196 231
            case 'Examples':
197 203
                return $this->parseExamples();
198 231
            case 'TableRow':
199 3
                return $this->parseTable();
200 231
            case 'PyStringOp':
201 9
                return $this->parsePyString();
202 231
            case 'Example':
203 231
            case 'Scenario':
204 222
                return $this->parseScenario();
205 231
            case 'Step':
206 224
                return $this->parseStep();
207 226
            case 'Text':
208 211
                return $this->parseText();
209 226
            case 'Newline':
210 222
                return $this->parseNewline();
211 202
            case 'Tag':
212 3
                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 230
    protected function parseFeature()
232
    {
233 230
        $token = $this->expectTokenType('Feature');
234
235 230
        $title = trim($token['value']) ?: null;
236 230
        $description = null;
237 230
        $tags = $this->popTags();
238 230
        $background = null;
239 230
        $rules = array();
240 230
        $scenarios = array();
241 230
        $keyword = $token['keyword'];
242 230
        $language = $this->lexer->getLanguage();
243 230
        $file = $this->file;
244 230
        $line = $token['line'];
245
246
        // Parse description, background, scenarios & outlines
247 230
        while ('EOS' !== $this->predictTokenType()) {
248 230
            $node = $this->parseExpression();
249
250 230 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 224
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
252 224
                $description .= (null !== $description ? "\n" : '') . $text;
253 224
                continue;
254
            }
255
256 227
            if (!$background && $node instanceof BackgroundNode) {
257 195
                $background = $node;
258 195
                continue;
259
            }
260
261 227
            if ($node instanceof RuleNode) {
262 1
                $rules[] = $node;
263 1
                continue;
264
            }
265
266 226
            if ($node instanceof ScenarioInterface) {
267 226
                $scenarios[] = $node;
268 226
                continue;
269
            }
270
271
            if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
272
                throw new ParserException(sprintf(
273
                    'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
274
                    $background->getLine(),
275
                    $node->getLine(),
276
                    $this->file ? ' in file: ' . $this->file : ''
277
                ));
278
            }
279
280 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
                throw new ParserException(sprintf(
282
                    'Expected Scenario, Outline or Background, but got %s on line: %d%s',
283
                    $node->getNodeType(),
284
                    $node->getLine(),
285
                    $this->file ? ' in file: ' . $this->file : ''
286
                ));
287
            }
288
        }
289
290 230
        return new FeatureNode(
291 230
            rtrim($title) ?: null,
292 230
            rtrim($description) ?: null,
293 230
            $tags,
294 230
            $background,
295 230
            $scenarios,
296 230
            $keyword,
297 230
            $language,
298 230
            $file,
299 230
            $line,
300 230
            $rules
301
        );
302
    }
303
304
    /**
305
     * Parses background token & returns it's node.
306
     *
307
     * @return BackgroundNode
308
     *
309
     * @throws ParserException
310
     */
311 196
    protected function parseBackground()
312
    {
313 196
        $token = $this->expectTokenType('Background');
314
315 196
        $title = trim($token['value']);
316 196
        $keyword = $token['keyword'];
317 196
        $line = $token['line'];
318
319 196
        if (count($this->popTags())) {
320
            throw new ParserException(sprintf(
321
                'Background can not be tagged, but it is on line: %d%s',
322
                $line,
323
                $this->file ? ' in file: ' . $this->file : ''
324
            ));
325
        }
326
327
        // Parse description and steps
328 196
        $steps = array();
329 196
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
330 196
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
331 196
            $node = $this->parseExpression();
332
333 196
            if ($node instanceof StepNode) {
334 196
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
335 196
                continue;
336
            }
337
338 2 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 2
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
340 2
                $title .= "\n" . $text;
341 2
                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 196
        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 222
    protected function parseScenario()
415
    {
416 222
        $token = $this->expectTokenType('Scenario');
417
418 222
        $title = trim($token['value']);
419 222
        $tags = $this->popTags();
420 222
        $keyword = $token['keyword'];
421 222
        $line = $token['line'];
422
423
        // Parse description and steps
424 222
        $steps = array();
425 222
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
426 222
            $node = $this->parseExpression();
427
428 222
            if ($node instanceof StepNode) {
429 219
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
430 219
                continue;
431
            }
432
433 9 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 9
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
435 9
                $title .= "\n" . $text;
436 9
                continue;
437
            }
438
439
            if ("\n" === $node) {
440
                continue;
441
            }
442
443 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
                throw new ParserException(sprintf(
445
                    'Expected Step, but got text: "%s"%s',
446
                    $node,
447
                    $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 222
        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 203
    protected function parseOutline()
472
    {
473 203
        $token = $this->expectTokenType('Outline');
474
475 203
        $title = trim($token['value']);
476 203
        $tags = $this->popTags();
477 203
        $keyword = $token['keyword'];
478 203
        $examples = null;
479 203
        $line = $token['line'];
480
481
        // Parse description, steps and examples
482 203
        $steps = array();
483 203
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
484 203
            $node = $this->parseExpression();
485
486 203
            if ($node instanceof StepNode) {
487 203
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
488 203
                continue;
489
            }
490
491 203
            if ($node instanceof ExampleTableNode) {
492 203
                $examples = $node;
493 203
                continue;
494
            }
495
496 5 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 5
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
498 5
                $title .= "\n" . $text;
499 5
                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 203
        if (null === $examples) {
525
            throw new ParserException(sprintf(
526
                'Outline should have examples table, but got none for outline "%s" on line: %d%s',
527
                rtrim($title),
528
                $line,
529
                $this->file ? ' in file: ' . $this->file : ''
530
            ));
531
        }
532
533 203
        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 224
    protected function parseStep()
542
    {
543 224
        $token = $this->expectTokenType('Step');
544
545 224
        $keyword = $token['value'];
546 224
        $keywordType = $token['keyword_type'];
547 224
        $text = trim($token['text']);
548 224
        $line = $token['line'];
549
550 224
        $arguments = array();
551 224
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
552 221
            if ('Comment' === $predicted || 'Newline' === $predicted) {
553 220
                $this->acceptTokenType($predicted);
554 220
                continue;
555
            }
556
557 11
            $node = $this->parseExpression();
558
559 11
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
560 11
                $arguments[] = $node;
561
            }
562
        }
563
564 224
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
565
    }
566
567
    /**
568
     * Parses examples table node.
569
     *
570
     * @return ExampleTableNode
571
     */
572 203
    protected function parseExamples()
573
    {
574 203
        $token = $this->expectTokenType('Examples');
575
576 203
        $keyword = $token['keyword'];
577
578 203
        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 9
    protected function parsePyString()
597
    {
598 9
        $token = $this->expectTokenType('PyStringOp');
599
600 9
        $line = $token['line'];
601
602 9
        $strings = array();
603 9
        while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
604 9
            $token = $this->expectTokenType('Text');
605
606 9
            $strings[] = $token['value'];
607
        }
608
609 9
        $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 3
    protected function parseTags()
620
    {
621 3
        $token = $this->expectTokenType('Tag');
622 3
        $this->tags = array_merge($this->tags, $token['tags']);
623
624 3
        return $this->parseExpression();
625
    }
626
627
    /**
628
     * Returns current set of tags and clears tag buffer.
629
     *
630
     * @return array
631
     */
632 230
    protected function popTags()
633
    {
634 230
        $tags = $this->tags;
635 230
        $this->tags = array();
636
637 230
        return $tags;
638
    }
639
640
    /**
641
     * Parses next text line & returns it.
642
     *
643
     * @return string
644
     */
645 211
    protected function parseText()
646
    {
647 211
        $token = $this->expectTokenType('Text');
648
649 211
        return $token['value'];
650
    }
651
652
    /**
653
     * Parses next newline & returns \n.
654
     *
655
     * @return string
656
     */
657 222
    protected function parseNewline()
658
    {
659 222
        $this->expectTokenType('Newline');
660
661 222
        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
            throw new ParserException(sprintf(
692
                'Ambiguous language specifiers on lines: %d and %d%s',
693
                $this->languageSpecifierLine,
694
                $token['line'],
695
                $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 204
    private function parseTableRows()
708
    {
709 204
        $table = array();
710 204
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
711 204
            if ('Comment' === $predicted || 'Newline' === $predicted) {
712 103
                $this->acceptTokenType($predicted);
713 103
                continue;
714
            }
715
716 204
            $token = $this->expectTokenType('TableRow');
717
718 204
            $table[$token['line']] = $token['columns'];
719
        }
720
721 204
        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 224
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
732
    {
733 224
        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 224
        return $node;
749
    }
750
}
751