Parser::parseScenario()   C
last analyzed

Complexity

Conditions 11
Paths 7

Size

Total Lines 53

Duplication

Lines 20
Ratio 37.74 %

Code Coverage

Tests 25
CRAP Score 12.2657

Importance

Changes 0
Metric Value
dl 20
loc 53
ccs 25
cts 32
cp 0.7813
rs 6.8787
c 0
b 0
f 0
cc 11
nc 7
nop 0
crap 12.2657

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Behat Gherkin.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Behat\Gherkin;
12
13
use Behat\Gherkin\Exception\LexerException;
14
use Behat\Gherkin\Exception\ParserException;
15
use Behat\Gherkin\Node\BackgroundNode;
16
use Behat\Gherkin\Node\ExampleTableNode;
17
use Behat\Gherkin\Node\FeatureNode;
18
use Behat\Gherkin\Node\OutlineNode;
19
use Behat\Gherkin\Node\PyStringNode;
20
use Behat\Gherkin\Node\ScenarioInterface;
21
use Behat\Gherkin\Node\ScenarioNode;
22
use Behat\Gherkin\Node\StepNode;
23
use Behat\Gherkin\Node\TableNode;
24
25
/**
26
 * Gherkin parser.
27
 *
28
 * $lexer  = new Behat\Gherkin\Lexer($keywords);
29
 * $parser = new Behat\Gherkin\Parser($lexer);
30
 * $featuresArray = $parser->parse('/path/to/feature.feature');
31
 *
32
 * @author Konstantin Kudryashov <[email protected]>
33
 */
34
class Parser
35
{
36
    private $lexer;
37
    private $input;
38
    private $file;
39
    private $tags = array();
40
    private $languageSpecifierLine;
41
42
    private $passedNodesStack = array();
43
44
    /**
45
     * Initializes parser.
46
     *
47
     * @param Lexer $lexer Lexer instance
48
     */
49 245
    public function __construct(Lexer $lexer)
50
    {
51 245
        $this->lexer = $lexer;
52 245
    }
53
54
    /**
55
     * Parses input & returns features array.
56
     *
57
     * @param string $input Gherkin string document
58
     * @param string $file  File name
59
     *
60
     * @return FeatureNode|null
61
     *
62
     * @throws ParserException
63
     */
64 243
    public function parse($input, $file = null)
65
    {
66 243
        $this->languageSpecifierLine = null;
67 243
        $this->input = $input;
68 243
        $this->file = $file;
69 243
        $this->tags = array();
70
71
        try {
72 243
            $this->lexer->analyse($this->input, 'en');
73
        } catch (LexerException $e) {
74
            throw new ParserException(
75
                sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
76
                0,
77
                $e
78
            );
79
        }
80
81 243
        $feature = null;
82 243
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
83 243
            $node = $this->parseExpression();
84
85 235
            if (null === $node || "\n" === $node) {
86 3
                continue;
87
            }
88
89 234
            if (!$feature && $node instanceof FeatureNode) {
90 234
                $feature = $node;
91 234
                continue;
92
            }
93
94
            if ($feature && $node instanceof FeatureNode) {
95
                throw new ParserException(sprintf(
96
                    'Only one feature is allowed per feature file. But %s got multiple.',
97
                    $this->file
98
                ));
99
            }
100
101 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
102
                throw new ParserException(sprintf(
103
                    'Expected Feature, but got text: "%s"%s',
104
                    $node,
105
                    $this->file ? ' in file: ' . $this->file : ''
106
                ));
107
            }
108
109
            if (!$node instanceof FeatureNode) {
110
                throw new ParserException(sprintf(
111
                    'Expected Feature, but got %s on line: %d%s',
112
                    $node->getKeyword(),
0 ignored issues
show
Bug introduced by
The method getKeyword does only exist in Behat\Gherkin\Node\Backg...t\Gherkin\Node\StepNode, but not in Behat\Gherkin\Node\PyStr...\Gherkin\Node\TableNode.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
113
                    $node->getLine(),
114
                    $this->file ? ' in file: ' . $this->file : ''
115
                ));
116
            }
117
        }
118
119 234
        return $feature;
120
    }
121
122
    /**
123
     * Returns next token if it's type equals to expected.
124
     *
125
     * @param string $type Token type
126
     *
127
     * @return array
128
     *
129
     * @throws Exception\ParserException
130
     */
131 243
    protected function expectTokenType($type)
132
    {
133 243
        $types = (array) $type;
134 243
        if (in_array($this->predictTokenType(), $types)) {
135 243
            return $this->lexer->getAdvancedToken();
136
        }
137
138 1
        $token = $this->lexer->predictToken();
139
140 1
        throw new ParserException(sprintf(
141 1
            'Expected %s token, but got %s on line: %d%s',
142 1
            implode(' or ', $types),
143 1
            $this->predictTokenType(),
144 1
            $token['line'],
145 1
            $this->file ? ' in file: ' . $this->file : ''
146
        ));
147
    }
148
149
    /**
150
     * Returns next token if it's type equals to expected.
151
     *
152
     * @param string $type Token type
153
     *
154
     * @return null|array
155
     */
156 224
    protected function acceptTokenType($type)
157
    {
158 224
        if ($type !== $this->predictTokenType()) {
159
            return null;
160
        }
161
162 224
        return $this->lexer->getAdvancedToken();
163
    }
164
165
    /**
166
     * Returns next token type without real input reading (prediction).
167
     *
168
     * @return string
169
     */
170 243
    protected function predictTokenType()
171
    {
172 243
        $token = $this->lexer->predictToken();
173
174 243
        return $token['type'];
175
    }
176
177
    /**
178
     * Parses current expression & returns Node.
179
     *
180
     * @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
181
     *
182
     * @throws ParserException
183
     */
184 243
    protected function parseExpression()
185
    {
186 243
        switch ($type = $this->predictTokenType()) {
187 243
            case 'Feature':
188 242
                return $this->parseFeature();
189 243
            case 'Background':
190 198
                return $this->parseBackground();
191 243
            case 'Scenario':
192 228
                return $this->parseScenario();
193 243
            case 'Outline':
194 206
                return $this->parseOutline();
195 243
            case 'Examples':
196 205
                return $this->parseExamples();
197 243
            case 'TableRow':
198 3
                return $this->parseTable();
199 243
            case 'PyStringOp':
200 10
                return $this->parsePyString();
201 243
            case 'Step':
202 230
                return $this->parseStep();
203 241
            case 'Text':
204 217
                return $this->parseText();
205 241
            case 'Newline':
206 234
                return $this->parseNewline();
207 209
            case 'Tag':
208 10
                return $this->parseTags();
209 200
            case 'Comment':
210 8
                return $this->parseComment();
211 196
            case 'Language':
212 193
                return $this->parseLanguage();
213 3
            case 'EOS':
214 3
                return '';
215
        }
216
217
        throw new ParserException(sprintf('Unknown token type: %s', $type));
218
    }
219
220
    /**
221
     * Parses feature token & returns it's node.
222
     *
223
     * @return FeatureNode
224
     *
225
     * @throws ParserException
226
     */
227 242
    protected function parseFeature()
228
    {
229 242
        $token = $this->expectTokenType('Feature');
230
231 242
        $title = trim($token['value']) ?: null;
232 242
        $description = null;
233 242
        $tags = $this->popTags();
234 242
        $background = null;
235 242
        $scenarios = array();
236 242
        $keyword = $token['keyword'];
237 242
        $language = $this->lexer->getLanguage();
238 242
        $file = $this->file;
239 242
        $line = $token['line'];
240
241 242
        array_push($this->passedNodesStack, 'Feature');
242
243
        // Parse description, background, scenarios & outlines
244 242
        while ('EOS' !== $this->predictTokenType()) {
245 242
            $node = $this->parseExpression();
246
247 242 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
248 233
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
249 233
                $description .= (null !== $description ? "\n" : '') . $text;
250 233
                continue;
251
            }
252
253 233
            if (!$background && $node instanceof BackgroundNode) {
254 197
                $background = $node;
255 197
                continue;
256
            }
257
258 233
            if ($node instanceof ScenarioInterface) {
259 231
                $scenarios[] = $node;
260 231
                continue;
261
            }
262
263 3
            if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
264 1
                throw new ParserException(sprintf(
265 1
                    'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
266 1
                    $background->getLine(),
267 1
                    $node->getLine(),
268 1
                    $this->file ? ' in file: ' . $this->file : ''
269
                ));
270
            }
271
272 2 View Code Duplication
            if (!$node instanceof ScenarioNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273 2
                throw new ParserException(sprintf(
274 2
                    'Expected Scenario, Outline or Background, but got %s on line: %d%s',
275 2
                    $node->getNodeType(),
276 2
                    $node->getLine(),
277 2
                    $this->file ? ' in file: ' . $this->file : ''
278
                ));
279
            }
280
        }
281
282 235
        return new FeatureNode(
283 235
            rtrim($title) ?: null,
284 235
            rtrim($description) ?: null,
285 235
            $tags,
286 235
            $background,
287 235
            $scenarios,
288 235
            $keyword,
289 235
            $language,
290 235
            $file,
291 235
            $line
292
        );
293
    }
294
295
    /**
296
     * Parses background token & returns it's node.
297
     *
298
     * @return BackgroundNode
299
     *
300
     * @throws ParserException
301
     */
302 198
    protected function parseBackground()
303
    {
304 198
        $token = $this->expectTokenType('Background');
305
306 198
        $title = trim($token['value']);
307 198
        $keyword = $token['keyword'];
308 198
        $line = $token['line'];
309
310 198
        if (count($this->popTags())) {
311 1
            throw new ParserException(sprintf(
312 1
                'Background can not be tagged, but it is on line: %d%s',
313 1
                $line,
314 1
                $this->file ? ' in file: ' . $this->file : ''
315
            ));
316
        }
317
318
        // Parse description and steps
319 197
        $steps = array();
320 197
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
321 197
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
322 197
            $node = $this->parseExpression();
323
324 197
            if ($node instanceof StepNode) {
325 196
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
326 196
                continue;
327
            }
328
329 4 View Code Duplication
            if (!count($steps) && is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
330 4
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
331 4
                $title .= "\n" . $text;
332 4
                continue;
333
            }
334
335
            if ("\n" === $node) {
336
                continue;
337
            }
338
339 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
340
                throw new ParserException(sprintf(
341
                    'Expected Step, but got text: "%s"%s',
342
                    $node,
343
                    $this->file ? ' in file: ' . $this->file : ''
344
                ));
345
            }
346
347 View Code Duplication
            if (!$node instanceof StepNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
348
                throw new ParserException(sprintf(
349
                    'Expected Step, but got %s on line: %d%s',
350
                    $node->getNodeType(),
351
                    $node->getLine(),
352
                    $this->file ? ' in file: ' . $this->file : ''
353
                ));
354
            }
355
        }
356
357 197
        return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
358
    }
359
360
    /**
361
     * Parses scenario token & returns it's node.
362
     *
363
     * @return ScenarioNode
364
     *
365
     * @throws ParserException
366
     */
367 228
    protected function parseScenario()
368
    {
369 228
        $token = $this->expectTokenType('Scenario');
370
371 228
        $title = trim($token['value']);
372 228
        $tags = $this->popTags();
373 228
        $keyword = $token['keyword'];
374 228
        $line = $token['line'];
375
376 228
        array_push($this->passedNodesStack, 'Scenario');
377
378
        // Parse description and steps
379 228
        $steps = array();
380 228
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
381 227
            $node = $this->parseExpression();
382
383 226
            if ($node instanceof StepNode) {
384 222
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
385 222
                continue;
386
            }
387
388 12 View Code Duplication
            if (!count($steps) && is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389 10
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
390 10
                $title .= "\n" . $text;
391 10
                continue;
392
            }
393
394 2
            if ("\n" === $node) {
395
                continue;
396
            }
397
398 2 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
399 2
                throw new ParserException(sprintf(
400 2
                    'Expected Step, but got text: "%s"%s',
401 2
                    $node,
402 2
                    $this->file ? ' in file: ' . $this->file : ''
403
                ));
404
            }
405
406 View Code Duplication
            if (!$node instanceof StepNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
407
                throw new ParserException(sprintf(
408
                    'Expected Step, but got %s on line: %d%s',
409
                    $node->getNodeType(),
410
                    $node->getLine(),
411
                    $this->file ? ' in file: ' . $this->file : ''
412
                ));
413
            }
414
        }
415
416 225
        array_pop($this->passedNodesStack);
417
418 225
        return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
419
    }
420
421
    /**
422
     * Parses scenario outline token & returns it's node.
423
     *
424
     * @return OutlineNode
425
     *
426
     * @throws ParserException
427
     */
428 206
    protected function parseOutline()
429
    {
430 206
        $token = $this->expectTokenType('Outline');
431
432 206
        $title = trim($token['value']);
433 206
        $tags = $this->popTags();
434 206
        $keyword = $token['keyword'];
435
436
        /** @var ExampleTableNode $examples */
437 206
        $examples = array();
438 206
        $line = $token['line'];
439
440
        // Parse description, steps and examples
441 206
        $steps = array();
442
443 206
        array_push($this->passedNodesStack, 'Outline');
444
445 206
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'))) {
446 205
            $node = $this->parseExpression();
447
448 205
            if ($node instanceof StepNode) {
449 204
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
450 204
                continue;
451
            }
452
453 205
            if ($node instanceof ExampleTableNode) {
454 205
                $examples[] = $node;
455
456 205
                continue;
457
            }
458
459 7 View Code Duplication
            if (!count($steps) && is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
460 6
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
461 6
                $title .= "\n" . $text;
462 6
                continue;
463
            }
464
465 1
            if ("\n" === $node) {
466 1
                continue;
467
            }
468
469 View Code Duplication
            if (is_string($node)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
470
                throw new ParserException(sprintf(
471
                    'Expected Step or Examples table, but got text: "%s"%s',
472
                    $node,
473
                    $this->file ? ' in file: ' . $this->file : ''
474
                ));
475
            }
476
477 View Code Duplication
            if (!$node instanceof StepNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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