Completed
Pull Request — master (#150)
by Matt
02:32
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 227
            $node = $this->parseExpression();
85
86 169
            if (null === $node || "\n" === $node) {
87 2
                continue;
88
            }
89
90 169
            if (!$feature && $node instanceof FeatureNode) {
91 169
                $feature = $node;
92 169
                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 169
        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 227
    protected function expectTokenType($type)
133
    {
134 227
        $types = (array) $type;
135 227
        if (in_array($this->predictTokenType(), $types)) {
136 227
            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 160
    protected function acceptTokenType($type)
158
    {
159 160
        if ($type !== $this->predictTokenType()) {
160
            return null;
161
        }
162
163 160
        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 227
        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 227
    protected function parseExpression()
186
    {
187 227
        switch ($type = $this->predictTokenType()) {
188 227
            case 'Feature':
189 169
                return $this->parseFeature();
190 227
            case 'Background':
191 136
                return $this->parseBackground();
192 227
            case 'Rule':
193 1
                return $this->parseRule();
194 227
            case 'Outline':
195 143
                return $this->parseOutline();
196 227
            case 'Examples':
197 143
                return $this->parseExamples();
198 227
            case 'TableRow':
199 3
                return $this->parseTable();
200 227
            case 'PyStringOp':
201 6
                return $this->parsePyString();
202 227
            case 'Example':
203 227
            case 'Scenario':
204 161
                return $this->parseScenario();
205 227
            case 'Step':
206 163
                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 169
    protected function parseFeature()
232
    {
233 169
        $token = $this->expectTokenType('Feature');
234
235 169
        $title = trim($token['value']) ?: null;
236 169
        $description = null;
237 169
        $tags = $this->popTags();
238 169
        $background = null;
239 169
        $rules = array();
240 169
        $scenarios = array();
241 169
        $keyword = $token['keyword'];
242 169
        $language = $this->lexer->getLanguage();
243 169
        $file = $this->file;
244 169
        $line = $token['line'];
245
246
        // Parse description, background, scenarios & outlines
247 169
        while ('EOS' !== $this->predictTokenType()) {
248 169
            $node = $this->parseExpression();
249
250 169 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 166
            if (!$background && $node instanceof BackgroundNode) {
257 135
                $background = $node;
258 135
                continue;
259
            }
260
261 166
            if ($node instanceof RuleNode) {
262 1
                $rules[] = $node;
263 1
                continue;
264
            }
265
266 165
            if ($node instanceof ScenarioInterface) {
267 165
                $scenarios[] = $node;
268 165
                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 169
        return new FeatureNode(
291 169
            rtrim($title) ?: null,
292 169
            rtrim($description) ?: null,
293 169
            $tags,
294 169
            $background,
295 169
            $scenarios,
296 169
            $keyword,
297 169
            $language,
298 169
            $file,
299 169
            $line,
300 169
            $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 161
    protected function parseScenario()
415
    {
416 161
        $token = $this->expectTokenType('Scenario');
417
418 161
        $title = trim($token['value']);
419 161
        $tags = $this->popTags();
420 161
        $keyword = $token['keyword'];
421 161
        $line = $token['line'];
422
423
        // Parse description and steps
424 161
        $steps = array();
425 161
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
426 161
            $node = $this->parseExpression();
427
428 161
            if ($node instanceof StepNode) {
429 158
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
430 158
                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 161
        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 143
    protected function parseOutline()
472
    {
473 143
        $token = $this->expectTokenType('Outline');
474
475 143
        $title = trim($token['value']);
476 143
        $tags = $this->popTags();
477 143
        $keyword = $token['keyword'];
478 143
        $examples = null;
479 143
        $line = $token['line'];
480
481
        // Parse description, steps and examples
482 143
        $steps = array();
483 143
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
484 143
            $node = $this->parseExpression();
485
486 143
            if ($node instanceof StepNode) {
487 143
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
488 143
                continue;
489
            }
490
491 143
            if ($node instanceof ExampleTableNode) {
492 143
                $examples = $node;
493 143
                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 143
        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 143
        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 163
    protected function parseStep()
542
    {
543 163
        $token = $this->expectTokenType('Step');
544
545 163
        $keyword = $token['value'];
546 163
        $keywordType = $token['keyword_type'];
547 163
        $text = trim($token['text']);
548 163
        $line = $token['line'];
549
550 163
        $arguments = array();
551 163
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
552 160
            if ('Comment' === $predicted || 'Newline' === $predicted) {
553 159
                $this->acceptTokenType($predicted);
554 159
                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 163
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
565
    }
566
567
    /**
568
     * Parses examples table node.
569
     *
570
     * @return ExampleTableNode
571
     */
572 143
    protected function parseExamples()
573
    {
574 143
        $token = $this->expectTokenType('Examples');
575
576 143
        $keyword = $token['keyword'];
577
578 143
        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 169
    protected function popTags()
633
    {
634 169
        $tags = $this->tags;
635 169
        $this->tags = array();
636
637 169
        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 144
    private function parseTableRows()
708
    {
709 144
        $table = array();
710 144
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
711 144
            if ('Comment' === $predicted || 'Newline' === $predicted) {
712 77
                $this->acceptTokenType($predicted);
713 77
                continue;
714
            }
715
716 144
            $token = $this->expectTokenType('TableRow');
717
718 144
            $table[$token['line']] = $token['columns'];
719
        }
720
721 144
        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 163
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
732
    {
733 163
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
734 145
            if (($prev = end($steps))) {
735 145
                $keywordType = $prev->getKeywordType();
736
            } else {
737
                $keywordType = 'Given';
738
            }
739
740 145
            $node = new StepNode(
741 145
                $node->getKeyword(),
742 145
                $node->getText(),
743 145
                $node->getArguments(),
744 145
                $node->getLine(),
745 145
                $keywordType
746
            );
747
        }
748 163
        return $node;
749
    }
750
}
751