Completed
Pull Request — master (#161)
by Phil
13:29
created

Parser::parseTableRows()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 3
cts 3
cp 1
rs 9.7333
c 0
b 0
f 0
cc 4
nc 3
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\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 244
     * @param Lexer $lexer Lexer instance
48
     */
49 244
    public function __construct(Lexer $lexer)
50 244
    {
51
        $this->lexer = $lexer;
52
    }
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 242
     * @throws ParserException
63
     */
64 242
    public function parse($input, $file = null)
65 242
    {
66 242
        $this->languageSpecifierLine = null;
67 242
        $this->input = $input;
68
        $this->file = $file;
69
        $this->tags = array();
70 242
71 242
        try {
72
            $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 242
        }
80 242
81 242
        $feature = null;
82
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
83 234
            $node = $this->parseExpression();
84 3
85
            if (null === $node || "\n" === $node) {
86
                continue;
87 233
            }
88 233
89 233
            if (!$feature && $node instanceof FeatureNode) {
90
                $feature = $node;
91
                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 233
        }
118
119
        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 242
     * @throws Exception\ParserException
130
     */
131 242
    protected function expectTokenType($type)
132 242
    {
133 242
        $types = (array) $type;
134
        if (in_array($this->predictTokenType(), $types)) {
135
            return $this->lexer->getAdvancedToken();
136 1
        }
137
138 1
        $token = $this->lexer->predictToken();
139 1
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
            $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 223
     * @return null|array
155
     */
156 223
    protected function acceptTokenType($type)
157
    {
158
        if ($type !== $this->predictTokenType()) {
159
            return null;
160 223
        }
161
162
        return $this->lexer->getAdvancedToken();
163
    }
164
165
    /**
166
     * Returns next token type without real input reading (prediction).
167
     *
168 242
     * @return string
169
     */
170 242
    protected function predictTokenType()
171
    {
172 242
        $token = $this->lexer->predictToken();
173
174
        return $token['type'];
175
    }
176
177
    /**
178
     * Parses current expression & returns Node.
179
     *
180
     * @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
181
     *
182 242
     * @throws ParserException
183
     */
184 242
    protected function parseExpression()
185 242
    {
186 241
        switch ($type = $this->predictTokenType()) {
187 242
            case 'Feature':
188 198
                return $this->parseFeature();
189 242
            case 'Background':
190 228
                return $this->parseBackground();
191 242
            case 'Scenario':
192 205
                return $this->parseScenario();
193 242
            case 'Outline':
194 204
                return $this->parseOutline();
195 242
            case 'Examples':
196 3
                return $this->parseExamples();
197 242
            case 'TableRow':
198 10
                return $this->parseTable();
199 242
            case 'PyStringOp':
200 229
                return $this->parsePyString();
201 236
            case 'Step':
202 217
                return $this->parseStep();
203 236
            case 'Text':
204 233
                return $this->parseText();
205 204
            case 'Newline':
206 5
                return $this->parseNewline();
207 200
            case 'Tag':
208 8
                return $this->parseTags();
209 196
            case 'Comment':
210 193
                return $this->parseComment();
211 3
            case 'Language':
212 3
                return $this->parseLanguage();
213
            case 'EOS':
214
                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 241
     * @throws ParserException
226
     */
227 241
    protected function parseFeature()
228
    {
229 241
        $token = $this->expectTokenType('Feature');
230 241
231 241
        $title = trim($token['value']) ?: null;
232 241
        $description = null;
233 241
        $tags = $this->popTags();
234 241
        $background = null;
235 241
        $scenarios = array();
236 241
        $keyword = $token['keyword'];
237 241
        $language = $this->lexer->getLanguage();
238
        $file = $this->file;
239
        $line = $token['line'];
240 241
241 241
        array_push($this->passedNodesStack, 'Feature');
242
243 241
        // Parse description, background, scenarios & outlines
244 232
        while ('EOS' !== $this->predictTokenType()) {
245 232
            $node = $this->parseExpression();
246 232
247 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
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
249 232
                $description .= (null !== $description ? "\n" : '') . $text;
250 197
                continue;
251 197
            }
252
253
            if (!$background && $node instanceof BackgroundNode) {
254 232
                $background = $node;
255 230
                continue;
256 230
            }
257
258
            if ($node instanceof ScenarioInterface) {
259 3
                $scenarios[] = $node;
260 1
                continue;
261 1
            }
262 1
263 1
            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
                    $background->getLine(),
267
                    $node->getLine(),
268 2
                    $this->file ? ' in file: ' . $this->file : ''
269 2
                ));
270 2
            }
271 2
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
                    $node->getNodeType(),
276
                    $node->getLine(),
277
                    $this->file ? ' in file: ' . $this->file : ''
278 234
                ));
279 234
            }
280 234
        }
281 234
282 234
        return new FeatureNode(
283 234
            rtrim($title) ?: null,
284 234
            rtrim($description) ?: null,
285 234
            $tags,
286 234
            $background,
287
            $scenarios,
288 234
            $keyword,
289
            $language,
290
            $file,
291
            $line
292
        );
293
    }
294
295
    /**
296
     * Parses background token & returns it's node.
297
     *
298 198
     * @return BackgroundNode
299
     *
300 198
     * @throws ParserException
301
     */
302 198
    protected function parseBackground()
303 198
    {
304 198
        $token = $this->expectTokenType('Background');
305
306 198
        $title = trim($token['value']);
307 1
        $keyword = $token['keyword'];
308 1
        $line = $token['line'];
309 1
310 1
        if (count($this->popTags())) {
311 1
            throw new ParserException(sprintf(
312
                'Background can not be tagged, but it is on line: %d%s',
313
                $line,
314
                $this->file ? ' in file: ' . $this->file : ''
315 197
            ));
316 197
        }
317 197
318 197
        // Parse description and steps
319
        $steps = array();
320 197
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
321 196
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
322 196
            $node = $this->parseExpression();
323
324
            if ($node instanceof StepNode) {
325 4
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
326 4
                continue;
327 4
            }
328 4
329 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
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
331
                $title .= "\n" . $text;
332
                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 197
                ));
354
            }
355
        }
356
357
        return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
358
    }
359
360
    /**
361
     * Parses scenario token & returns it's node.
362
     *
363 228
     * @return ScenarioNode
364
     *
365 228
     * @throws ParserException
366
     */
367 228
    protected function parseScenario()
368 228
    {
369 228
        $token = $this->expectTokenType('Scenario');
370 228
371
        $title = trim($token['value']);
372
        $tags = $this->popTags();
373 228
        $keyword = $token['keyword'];
374 228
        $line = $token['line'];
375 227
376
        array_push($this->passedNodesStack, 'Scenario');
377 226
378 222
        // Parse description and steps
379 222
        $steps = array();
380
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
381
            $node = $this->parseExpression();
382 12
383 10
            if ($node instanceof StepNode) {
384 10
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
385 10
                continue;
386
            }
387
388 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...
389
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
390
                $title .= "\n" . $text;
391
                continue;
392 2
            }
393 2
394 2
            if ("\n" === $node) {
395 2
                continue;
396 2
            }
397 2
398 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
                throw new ParserException(sprintf(
400
                    'Expected Step, but got text: "%s"%s',
401
                    $node,
402
                    $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 225
                    $node->getLine(),
411
                    $this->file ? ' in file: ' . $this->file : ''
412
                ));
413
            }
414
        }
415
416
        array_pop($this->passedNodesStack);
417
418
        return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
419
    }
420 205
421
    /**
422 205
     * Parses scenario outline token & returns it's node.
423
     *
424 205
     * @return OutlineNode
425 205
     *
426 205
     * @throws ParserException
427 205
     */
428 205
    protected function parseOutline()
429
    {
430
        $token = $this->expectTokenType('Outline');
431 205
432 205
        $title = trim($token['value']);
433 204
        $tags = $this->popTags();
434
        $keyword = $token['keyword'];
435 204
436 203
        /** @var ExampleTableNode $examples */
437 203
        $examples = array();
438
        $line = $token['line'];
439
440 204
        // Parse description, steps and examples
441 204
        $steps = array();
442 204
443
        array_push($this->passedNodesStack, 'Outline');
444
445 6
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'))) {
446 6
            $node = $this->parseExpression();
447 6
448 6
            if ($node instanceof StepNode) {
449
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
450
                continue;
451
            }
452
453
            if ($node instanceof ExampleTableNode) {
454
                $examples[] = $node;
455
456
                continue;
457
            }
458
459 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
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
461
                $title .= "\n" . $text;
462
                continue;
463
            }
464
465
            if ("\n" === $node) {
466
                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 205
                    $this->file ? ' in file: ' . $this->file : ''
474 1
                ));
475 1
            }
476 1
477 1 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 1
                throw new ParserException(sprintf(
479 1
                    'Expected Step or Examples table, but got %s on line: %d%s',
480
                    $node->getNodeType(),
481
                    $node->getLine(),
482 204
                    $this->file ? ' in file: ' . $this->file : ''
483
                ));
484
            }
485
        }
486
487
        if (empty($examples)) {
488
            throw new ParserException(sprintf(
489
                'Outline should have examples table, but got none for outline "%s" on line: %d%s',
490 229
                rtrim($title),
491
                $line,
492 229
                $this->file ? ' in file: ' . $this->file : ''
493
            ));
494 229
        }
495 229
496 229
        return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
497 229
    }
498
499 229
    /**
500 229
     * Parses step token & returns it's node.
501 223
     *
502 221
     * @return StepNode
503 221
     */
504
    protected function parseStep()
505
    {
506 12
        $token = $this->expectTokenType('Step');
507
508 11
        $keyword = $token['value'];
509 11
        $keywordType = $token['keyword_type'];
510 11
        $text = trim($token['text']);
511 11
        $line = $token['line'];
512
513 228
        array_push($this->passedNodesStack, 'Step');
514
515
        $arguments = array();
516
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
517
            if ('Comment' === $predicted || 'Newline' === $predicted) {
518
                $this->acceptTokenType($predicted);
519
                continue;
520
            }
521 204
522
            $node = $this->parseExpression();
523 204
524
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
525 204
                $arguments[] = $node;
526
            }
527 204
        }
528
529
        array_pop($this->passedNodesStack);
530
531
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
532
    }
533
534
    /**
535 3
     * Parses examples table node.
536
     *
537 3
     * @return ExampleTableNode
538
     */
539
    protected function parseExamples()
540
    {
541
        $token = $this->expectTokenType('Examples');
542
543
        $keyword = $token['keyword'];
544
545 10
        $tags = empty($this->tags) ? array() : $this->popTags();
546
547 10
        return new ExampleTableNode($this->parseTableRows(), $keyword, $tags);
548
    }
549 10
550
    /**
551 10
     * Parses table token & returns it's node.
552 10
     *
553 10
     * @return TableNode
554
     */
555 10
    protected function parseTable()
556 10
    {
557
        return new TableNode($this->parseTableRows());
558 10
    }
559
560 9
    /**
561
     * Parses PyString token & returns it's node.
562
     *
563
     * @return PyStringNode
564
     */
565
    protected function parsePyString()
566
    {
567
        $token = $this->expectTokenType('PyStringOp');
568 5
569
        $line = $token['line'];
570 5
571 5
        $strings = array();
572
        while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
573 5
            $token = $this->expectTokenType('Text');
574
575
            $strings[] = $token['value'];
576
        }
577
578
        $this->expectTokenType('PyStringOp');
579
580
        return new PyStringNode($strings, $line);
581 241
    }
582
583 241
    /**
584 241
     * Parses tags.
585
     *
586 241
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
587
     */
588
    protected function parseTags()
589
    {
590
        $token = $this->expectTokenType('Tag');
591
        $this->tags = array_merge($this->tags, $token['tags']);
592
593
        $possibleTransitions = array(
594 217
            'Outline' => array(
595
                'Examples',
596 217
                'Step'
597
            )
598 217
        );
599
600
        $currentType = '-1';
601
        // check if that is ok to go inside:
602
        if (!empty($this->passedNodesStack)) {
603
            $currentType = $this->passedNodesStack[count($this->passedNodesStack) - 1];
604
        }
605
606 233
        $nextType = $this->predictTokenType();
607
        if (!isset($possibleTransitions[$currentType]) || in_array($nextType, $possibleTransitions[$currentType])) {
608 233
            return $this->parseExpression();
609
        }
610 233
611
        return "\n";
612
    }
613
614
    /**
615
     * Returns current set of tags and clears tag buffer.
616
     *
617
     * @return array
618 8
     */
619
    protected function popTags()
620 8
    {
621
        $tags = $this->tags;
622 8
        $this->tags = array();
623
624
        return $tags;
625
    }
626
627
    /**
628
     * Parses next text line & returns it.
629
     *
630
     * @return string
631
     */
632 193
    protected function parseText()
633
    {
634 193
        $token = $this->expectTokenType('Text');
635
636 193
        return $token['value'];
637 193
    }
638 193
639 193
    /**
640 1
     * Parses next newline & returns \n.
641 1
     *
642 1
     * @return string
643 1
     */
644 1
    protected function parseNewline()
645 1
    {
646
        $this->expectTokenType('Newline');
647
648 193
        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 205
    protected function parseComment()
657
    {
658 205
        $this->expectTokenType('Comment');
659 205
660 205
        return $this->parseExpression();
661 104
    }
662 104
663
    /**
664
     * Parses language block and updates lexer configuration based on it.
665 205
     *
666
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
667 205
     *
668 205
     * @throws ParserException
669
     */
670 205
    protected function parseLanguage()
671
    {
672
        $token = $this->expectTokenType('Language');
673
674
        if (null === $this->languageSpecifierLine) {
675
            $this->lexer->analyse($this->input, $token['value']);
676
            $this->languageSpecifierLine = $token['line'];
677
        } elseif ($token['line'] !== $this->languageSpecifierLine) {
678
            throw new ParserException(sprintf(
679
                'Ambiguous language specifiers on lines: %d and %d%s',
680 228
                $this->languageSpecifierLine,
681
                $token['line'],
682 228
                $this->file ? ' in file: ' . $this->file : ''
683 202
            ));
684 202
        }
685 202
686
        return $this->parseExpression();
687
    }
688
689 202
    /**
690 202
     * Parses the rows of a table
691 202
     *
692 202
     * @return string[][]
693 202
     */
694
    private function parseTableRows()
695 202
    {
696 202
        $table = array();
697 228
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
698
            if ('Comment' === $predicted || 'Newline' === $predicted) {
699
                $this->acceptTokenType($predicted);
700
                continue;
701
            }
702
703
            $token = $this->expectTokenType('TableRow');
704
705
            $table[$token['line']] = $token['columns'];
706
        }
707
708
        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
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
719
    {
720
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
721
            if (($prev = end($steps))) {
722
                $keywordType = $prev->getKeywordType();
723
            } else {
724
                $keywordType = 'Given';
725
            }
726
727
            $node = new StepNode(
728
                $node->getKeyword(),
729
                $node->getText(),
730
                $node->getArguments(),
731
                $node->getLine(),
732
                $keywordType
733
            );
734
        }
735
        return $node;
736
    }
737
}
738