Completed
Pull Request — master (#150)
by Matt
02:41
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 223
            $node = $this->parseExpression();
85
86 165
            if (null === $node || "\n" === $node) {
87 2
                continue;
88
            }
89
90 165
            if (!$feature && $node instanceof FeatureNode) {
91 165
                $feature = $node;
92 165
                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 165
        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 223
    protected function expectTokenType($type)
133
    {
134 223
        $types = (array) $type;
135 223
        if (in_array($this->predictTokenType(), $types)) {
136 223
            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 156
    protected function acceptTokenType($type)
158
    {
159 156
        if ($type !== $this->predictTokenType()) {
160
            return null;
161
        }
162
163 156
        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 223
        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 223
    protected function parseExpression()
186
    {
187 223
        switch ($type = $this->predictTokenType()) {
188 223
            case 'Feature':
189 165
                return $this->parseFeature();
190 223
            case 'Background':
191 136
                return $this->parseBackground();
192 223
            case 'Rule':
193 1
                return $this->parseRule();
194 223
            case 'Outline':
195 139
                return $this->parseOutline();
196 223
            case 'Examples':
197 139
                return $this->parseExamples();
198 223
            case 'TableRow':
199 3
                return $this->parseTable();
200 223
            case 'PyStringOp':
201 6
                return $this->parsePyString();
202 223
            case 'Example':
203 223
            case 'Scenario':
204 157
                return $this->parseScenario();
205 223
            case 'Step':
206 159
                return $this->parseStep();
207 222
            case 'Text':
208 151
                return $this->parseText();
209 222
            case 'Newline':
210 161
                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 165
    protected function parseFeature()
232
    {
233 165
        $token = $this->expectTokenType('Feature');
234
235 165
        $title = trim($token['value']) ?: null;
236 165
        $description = null;
237 165
        $tags = $this->popTags();
238 165
        $background = null;
239 165
        $rules = array();
240 165
        $scenarios = array();
241 165
        $keyword = $token['keyword'];
242 165
        $language = $this->lexer->getLanguage();
243 165
        $file = $this->file;
244 165
        $line = $token['line'];
245
246
        // Parse description, background, scenarios & outlines
247 165
        while ('EOS' !== $this->predictTokenType()) {
248 165
            $node = $this->parseExpression();
249
250 165 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 163
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
252 163
                $description .= (null !== $description ? "\n" : '') . $text;
253 163
                continue;
254
            }
255
256 162
            if (!$background && $node instanceof BackgroundNode) {
257 135
                $background = $node;
258 135
                continue;
259
            }
260
261 162
            if ($node instanceof RuleNode) {
262 1
                $rules[] = $node;
263 1
                continue;
264
            }
265
266 161
            if ($node instanceof ScenarioInterface) {
267 161
                $scenarios[] = $node;
268 161
                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 165
        return new FeatureNode(
291 165
            rtrim($title) ?: null,
292 165
            rtrim($description) ?: null,
293 165
            $tags,
294 165
            $background,
295 165
            $scenarios,
296 165
            $keyword,
297 165
            $language,
298 165
            $file,
299 165
            $line,
300 165
            $rules
301
        );
302
    }
303
304
    /**
305
     * Parses background token & returns it's node.
306
     *
307
     * @return BackgroundNode
308
     *
309
     * @throws ParserException
310
     */
311 136
    protected function parseBackground()
312
    {
313 136
        $token = $this->expectTokenType('Background');
314
315 136
        $title = trim($token['value']);
316 136
        $keyword = $token['keyword'];
317 136
        $line = $token['line'];
318
319 136
        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 136
        $steps = array();
329 136
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
330 136
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
331 136
            $node = $this->parseExpression();
332
333 136
            if ($node instanceof StepNode) {
334 136
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
335 136
                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 136
        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 157
    protected function parseScenario()
415
    {
416 157
        $token = $this->expectTokenType('Scenario');
417
418 157
        $title = trim($token['value']);
419 157
        $tags = $this->popTags();
420 157
        $keyword = $token['keyword'];
421 157
        $line = $token['line'];
422
423
        // Parse description and steps
424 157
        $steps = array();
425 157
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
426 157
            $node = $this->parseExpression();
427
428 157
            if ($node instanceof StepNode) {
429 154
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
430 154
                continue;
431
            }
432
433 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...
434 7
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
435 7
                $title .= "\n" . $text;
436 7
                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 157
        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 139
    protected function parseOutline()
472
    {
473 139
        $token = $this->expectTokenType('Outline');
474
475 139
        $title = trim($token['value']);
476 139
        $tags = $this->popTags();
477 139
        $keyword = $token['keyword'];
478 139
        $examples = null;
479 139
        $line = $token['line'];
480
481
        // Parse description, steps and examples
482 139
        $steps = array();
483 139
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
484 139
            $node = $this->parseExpression();
485
486 139
            if ($node instanceof StepNode) {
487 139
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
488 139
                continue;
489
            }
490
491 139
            if ($node instanceof ExampleTableNode) {
492 139
                $examples = $node;
493 139
                continue;
494
            }
495
496 3 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 3
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
498 3
                $title .= "\n" . $text;
499 3
                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 139
        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 139
        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 159
    protected function parseStep()
542
    {
543 159
        $token = $this->expectTokenType('Step');
544
545 159
        $keyword = $token['value'];
546 159
        $keywordType = $token['keyword_type'];
547 159
        $text = trim($token['text']);
548 159
        $line = $token['line'];
549
550 159
        $arguments = array();
551 159
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
552 156
            if ('Comment' === $predicted || 'Newline' === $predicted) {
553 155
                $this->acceptTokenType($predicted);
554 155
                continue;
555
            }
556
557 8
            $node = $this->parseExpression();
558
559 8
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
560 8
                $arguments[] = $node;
561
            }
562
        }
563
564 159
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
565
    }
566
567
    /**
568
     * Parses examples table node.
569
     *
570
     * @return ExampleTableNode
571
     */
572 139
    protected function parseExamples()
573
    {
574 139
        $token = $this->expectTokenType('Examples');
575
576 139
        $keyword = $token['keyword'];
577
578 139
        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 6
    protected function parsePyString()
597
    {
598 6
        $token = $this->expectTokenType('PyStringOp');
599
600 6
        $line = $token['line'];
601
602 6
        $strings = array();
603 6
        while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
604 6
            $token = $this->expectTokenType('Text');
605
606 6
            $strings[] = $token['value'];
607
        }
608
609 6
        $this->expectTokenType('PyStringOp');
610
611 6
        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 165
    protected function popTags()
633
    {
634 165
        $tags = $this->tags;
635 165
        $this->tags = array();
636
637 165
        return $tags;
638
    }
639
640
    /**
641
     * Parses next text line & returns it.
642
     *
643
     * @return string
644
     */
645 151
    protected function parseText()
646
    {
647 151
        $token = $this->expectTokenType('Text');
648
649 151
        return $token['value'];
650
    }
651
652
    /**
653
     * Parses next newline & returns \n.
654
     *
655
     * @return string
656
     */
657 161
    protected function parseNewline()
658
    {
659 161
        $this->expectTokenType('Newline');
660
661 161
        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 140
    private function parseTableRows()
708
    {
709 140
        $table = array();
710 140
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
711 140
            if ('Comment' === $predicted || 'Newline' === $predicted) {
712 77
                $this->acceptTokenType($predicted);
713 77
                continue;
714
            }
715
716 140
            $token = $this->expectTokenType('TableRow');
717
718 140
            $table[$token['line']] = $token['columns'];
719
        }
720
721 140
        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 159
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
732
    {
733 159
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
734 141
            if (($prev = end($steps))) {
735 141
                $keywordType = $prev->getKeywordType();
736
            } else {
737
                $keywordType = 'Given';
738
            }
739
740 141
            $node = new StepNode(
741 141
                $node->getKeyword(),
742 141
                $node->getText(),
743 141
                $node->getArguments(),
744 141
                $node->getLine(),
745 141
                $keywordType
746
            );
747
        }
748 159
        return $node;
749
    }
750
}
751