Completed
Pull Request — master (#152)
by Ciaran
02:52
created

Parser   F

Complexity

Total Complexity 115

Size/Duplication

Total Lines 666
Duplicated Lines 12.01 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 84.54%

Importance

Changes 0
Metric Value
wmc 115
lcom 1
cbo 10
dl 80
loc 666
ccs 268
cts 317
cp 0.8454
rs 1.934
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A expectTokenType() 0 17 3
A acceptTokenType() 0 8 2
A predictTokenType() 0 6 1
C parseExpression() 0 35 15
C parseFeature() 13 65 15
C parseBackground() 20 57 13
B parseScenario() 20 49 11
B parseStep() 0 25 6
A parseExamples() 0 8 1
A parseTable() 0 4 1
A parsePyString() 0 17 3
A parseTags() 0 7 1
A popTags() 0 7 1
A parseText() 0 6 1
A parseNewline() 0 6 1
A parseComment() 0 6 1
A parseLanguage() 0 18 4
A parseTableRows() 0 16 4
C parseOutline() 20 64 14
C parse() 7 57 13
A normalizeStepNodeKeywordType() 0 19 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

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
    /**
43
     * Initializes parser.
44
     *
45
     * @param Lexer $lexer Lexer instance
46
     */
47 269
    public function __construct(Lexer $lexer)
48
    {
49 269
        $this->lexer = $lexer;
50 269
    }
51
52
    /**
53
     * Parses input & returns features array.
54
     *
55
     * @param string $input Gherkin string document
56
     * @param string $file  File name
57
     *
58
     * @return FeatureNode|null
59
     *
60
     * @throws ParserException
61
     */
62 267
    public function parse($input, $file = null)
63
    {
64 267
        $this->languageSpecifierLine = null;
65 267
        $this->input = $input;
66 267
        $this->file = $file;
67 267
        $this->tags = array();
68
69
        try {
70 267
            $this->lexer->analyse($this->input, 'en');
71
        } catch (LexerException $e) {
72
            throw new ParserException(
73
                sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
74
                0,
75
                $e
76
            );
77
        }
78
79 267
        $feature = null;
80 267
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
81 267
            $node = $this->parseExpression();
82
83 257
            if (null === $node || "\n" === $node) {
84 7
                continue;
85
            }
86
87 255
            if (!$feature && $node instanceof FeatureNode) {
88 252
                $feature = $node;
89 252
                continue;
90
            }
91
92 3
            if ($feature && $node instanceof FeatureNode) {
93
                throw new ParserException(sprintf(
94
                    'Only one feature is allowed per feature file. But %s got multiple.',
95
                    $this->file
96
                ));
97
            }
98
99 3 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...
100 3
                throw new ParserException(sprintf(
101 3
                    'Expected Feature, but got text: "%s"%s',
102 3
                    $node,
103 3
                    $this->file ? ' in file: ' . $this->file : ''
104
                ));
105
            }
106
107
            if (!$node instanceof FeatureNode) {
108
                throw new ParserException(sprintf(
109
                    'Expected Feature, but got %s on line: %d%s',
110
                    $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...
111
                    $node->getLine(),
112
                    $this->file ? ' in file: ' . $this->file : ''
113
                ));
114
            }
115
        }
116
117 253
        return $feature;
118
    }
119
120
    /**
121
     * Returns next token if it's type equals to expected.
122
     *
123
     * @param string $type Token type
124
     *
125
     * @return array
126
     *
127
     * @throws Exception\ParserException
128
     */
129 267
    protected function expectTokenType($type)
130
    {
131 267
        $types = (array) $type;
132 267
        if (in_array($this->predictTokenType(), $types)) {
133 267
            return $this->lexer->getAdvancedToken();
134
        }
135
136 1
        $token = $this->lexer->predictToken();
137
138 1
        throw new ParserException(sprintf(
139 1
            'Expected %s token, but got %s on line: %d%s',
140 1
            implode(' or ', $types),
141 1
            $this->predictTokenType(),
142 1
            $token['line'],
143 1
            $this->file ? ' in file: ' . $this->file : ''
144
        ));
145
    }
146
147
    /**
148
     * Returns next token if it's type equals to expected.
149
     *
150
     * @param string $type Token type
151
     *
152
     * @return null|array
153
     */
154 242
    protected function acceptTokenType($type)
155
    {
156 242
        if ($type !== $this->predictTokenType()) {
157
            return null;
158
        }
159
160 242
        return $this->lexer->getAdvancedToken();
161
    }
162
163
    /**
164
     * Returns next token type without real input reading (prediction).
165
     *
166
     * @return string
167
     */
168 267
    protected function predictTokenType()
169
    {
170 267
        $token = $this->lexer->predictToken();
171
172 267
        return $token['type'];
173
    }
174
175
    /**
176
     * Parses current expression & returns Node.
177
     *
178
     * @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
179
     *
180
     * @throws ParserException
181
     */
182 267
    protected function parseExpression()
183
    {
184 267
        switch ($type = $this->predictTokenType()) {
185 267
            case 'Feature':
186 262
                return $this->parseFeature();
187 267
            case 'Background':
188 202
                return $this->parseBackground();
189 267
            case 'Scenario':
190 240
                return $this->parseScenario();
191 267
            case 'Outline':
192 212
                return $this->parseOutline();
193 267
            case 'Examples':
194 210
                return $this->parseExamples();
195 267
            case 'TableRow':
196 7
                return $this->parseTable();
197 267
            case 'PyStringOp':
198 12
                return $this->parsePyString();
199 267
            case 'Step':
200 247
                return $this->parseStep();
201 262
            case 'Text':
202 224
                return $this->parseText();
203 261
            case 'Newline':
204 258
                return $this->parseNewline();
205 209
            case 'Tag':
206 6
                return $this->parseTags();
207 204
            case 'Comment':
208 8
                return $this->parseComment();
209 200
            case 'Language':
210 197
                return $this->parseLanguage();
211 3
            case 'EOS':
212 3
                return '';
213
        }
214
215
        throw new ParserException(sprintf('Unknown token type: %s', $type));
216
    }
217
218
    /**
219
     * Parses feature token & returns it's node.
220
     *
221
     * @return FeatureNode
222
     *
223
     * @throws ParserException
224
     */
225 262
    protected function parseFeature()
226
    {
227 262
        $token = $this->expectTokenType('Feature');
228
229 262
        $title = trim($token['value']) ?: null;
230 262
        $description = null;
231 262
        $tags = $this->popTags();
232 262
        $background = null;
233 262
        $scenarios = array();
234 262
        $keyword = $token['keyword'];
235 262
        $language = $this->lexer->getLanguage();
236 262
        $file = $this->file;
237 262
        $line = $token['line'];
238
239
        // Parse description, background, scenarios & outlines
240 262
        while ('EOS' !== $this->predictTokenType()) {
241 262
            $node = $this->parseExpression();
242
243 262 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...
244 254
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
245 254
                $description .= (null !== $description ? "\n" : '') . $text;
246 254
                continue;
247
            }
248
249 249
            if (!$background && $node instanceof BackgroundNode) {
250 201
                $background = $node;
251 201
                continue;
252
            }
253
254 249
            if ($node instanceof ScenarioInterface) {
255 247
                $scenarios[] = $node;
256 247
                continue;
257
            }
258
259 3
            if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
260 1
                throw new ParserException(sprintf(
261 1
                    'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
262 1
                    $background->getLine(),
263 1
                    $node->getLine(),
264 1
                    $this->file ? ' in file: ' . $this->file : ''
265
                ));
266
            }
267
268 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...
269 2
                throw new ParserException(sprintf(
270 2
                    'Expected Scenario, Outline or Background, but got %s on line: %d%s',
271 2
                    $node->getNodeType(),
272 2
                    $node->getLine(),
273 2
                    $this->file ? ' in file: ' . $this->file : ''
274
                ));
275
            }
276
        }
277
278 253
        return new FeatureNode(
279 253
            rtrim($title) ?: null,
280 253
            rtrim($description) ?: null,
281 253
            $tags,
282 253
            $background,
283 253
            $scenarios,
284 253
            $keyword,
285 253
            $language,
286 253
            $file,
287 253
            $line
288
        );
289
    }
290
291
    /**
292
     * Parses background token & returns it's node.
293
     *
294
     * @return BackgroundNode
295
     *
296
     * @throws ParserException
297
     */
298 202
    protected function parseBackground()
299
    {
300 202
        $token = $this->expectTokenType('Background');
301
302 202
        $title = trim($token['value']);
303 202
        $keyword = $token['keyword'];
304 202
        $line = $token['line'];
305
306 202
        if (count($this->popTags())) {
307 1
            throw new ParserException(sprintf(
308 1
                'Background can not be tagged, but it is on line: %d%s',
309 1
                $line,
310 1
                $this->file ? ' in file: ' . $this->file : ''
311
            ));
312
        }
313
314
        // Parse description and steps
315 201
        $steps = array();
316 201
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
317 201
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
318 201
            $node = $this->parseExpression();
319
320 201
            if ($node instanceof StepNode) {
321 198
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
322 198
                continue;
323
            }
324
325 6 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...
326 6
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
327 6
                $title .= "\n" . $text;
328 6
                continue;
329
            }
330
331
            if ("\n" === $node) {
332
                continue;
333
            }
334
335 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...
336
                throw new ParserException(sprintf(
337
                    'Expected Step, but got text: "%s"%s',
338
                    $node,
339
                    $this->file ? ' in file: ' . $this->file : ''
340
                ));
341
            }
342
343 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...
344
                throw new ParserException(sprintf(
345
                    'Expected Step, but got %s on line: %d%s',
346
                    $node->getNodeType(),
347
                    $node->getLine(),
348
                    $this->file ? ' in file: ' . $this->file : ''
349
                ));
350
            }
351
        }
352
353 201
        return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
354
    }
355
356
    /**
357
     * Parses scenario token & returns it's node.
358
     *
359
     * @return ScenarioNode
360
     *
361
     * @throws ParserException
362
     */
363 240
    protected function parseScenario()
364
    {
365 240
        $token = $this->expectTokenType('Scenario');
366
367 240
        $title = trim($token['value']);
368 240
        $tags = $this->popTags();
369 240
        $keyword = $token['keyword'];
370 240
        $line = $token['line'];
371
372
        // Parse description and steps
373 240
        $steps = array();
374 240
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
375 239
            $node = $this->parseExpression();
376
377 237
            if ($node instanceof StepNode) {
378 231
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
379 231
                continue;
380
            }
381
382 14 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...
383 12
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
384 12
                $title .= "\n" . $text;
385 12
                continue;
386
            }
387
388 2
            if ("\n" === $node) {
389
                continue;
390
            }
391
392 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...
393 2
                throw new ParserException(sprintf(
394 2
                    'Expected Step, but got text: "%s"%s',
395 2
                    $node,
396 2
                    $this->file ? ' in file: ' . $this->file : ''
397
                ));
398
            }
399
400 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...
401
                throw new ParserException(sprintf(
402
                    'Expected Step, but got %s on line: %d%s',
403
                    $node->getNodeType(),
404
                    $node->getLine(),
405
                    $this->file ? ' in file: ' . $this->file : ''
406
                ));
407
            }
408
        }
409
410 236
        return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
411
    }
412
413
    /**
414
     * Parses scenario outline token & returns it's node.
415
     *
416
     * @return OutlineNode
417
     *
418
     * @throws ParserException
419
     */
420 212
    protected function parseOutline()
421
    {
422 212
        $token = $this->expectTokenType('Outline');
423
424 212
        $title = trim($token['value']);
425 212
        $tags = $this->popTags();
426 212
        $keyword = $token['keyword'];
427 212
        $examples = null;
428 212
        $line = $token['line'];
429
430
        // Parse description, steps and examples
431 212
        $steps = array();
432 212
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
433 211
            $node = $this->parseExpression();
434
435 211
            if ($node instanceof StepNode) {
436 210
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
437 210
                continue;
438
            }
439
440 210
            if ($node instanceof ExampleTableNode) {
441 210
                $examples = $node;
442 210
                continue;
443
            }
444
445 6 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...
446 6
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
447 6
                $title .= "\n" . $text;
448 6
                continue;
449
            }
450
451
            if ("\n" === $node) {
452
                continue;
453
            }
454
455 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...
456
                throw new ParserException(sprintf(
457
                    'Expected Step or Examples table, but got text: "%s"%s',
458
                    $node,
459
                    $this->file ? ' in file: ' . $this->file : ''
460
                ));
461
            }
462
463 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...
464
                throw new ParserException(sprintf(
465
                    'Expected Step or Examples table, but got %s on line: %d%s',
466
                    $node->getNodeType(),
467
                    $node->getLine(),
468
                    $this->file ? ' in file: ' . $this->file : ''
469
                ));
470
            }
471
        }
472
473 212
        if (null === $examples) {
474 2
            throw new ParserException(sprintf(
475 2
                'Outline should have examples table, but got none for outline "%s" on line: %d%s',
476 2
                rtrim($title),
477 2
                $line,
478 2
                $this->file ? ' in file: ' . $this->file : ''
479
            ));
480
        }
481
482 210
        return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
483
    }
484
485
    /**
486
     * Parses step token & returns it's node.
487
     *
488
     * @return StepNode
489
     */
490 247
    protected function parseStep()
491
    {
492 247
        $token = $this->expectTokenType('Step');
493
494 247
        $keyword = $token['value'];
495 247
        $keywordType = $token['keyword_type'];
496 247
        $text = trim($token['text']);
497 247
        $line = $token['line'];
498
499 247
        $arguments = array();
500 247
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
501 242
            if ('Comment' === $predicted || 'Newline' === $predicted) {
502 236
                $this->acceptTokenType($predicted);
503 236
                continue;
504
            }
505
506 17
            $node = $this->parseExpression();
507
508 15
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
509 15
                $arguments[] = $node;
510
            }
511
        }
512
513 245
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
514
    }
515
516
    /**
517
     * Parses examples table node.
518
     *
519
     * @return ExampleTableNode
520
     */
521 210
    protected function parseExamples()
522
    {
523 210
        $token = $this->expectTokenType('Examples');
524
525 210
        $keyword = $token['keyword'];
526
527 210
        return new ExampleTableNode($this->parseTableRows(), $keyword);
528
    }
529
530
    /**
531
     * Parses table token & returns it's node.
532
     *
533
     * @return TableNode
534
     */
535 7
    protected function parseTable()
536
    {
537 7
        return new TableNode($this->parseTableRows());
538
    }
539
540
    /**
541
     * Parses PyString token & returns it's node.
542
     *
543
     * @return PyStringNode
544
     */
545 12
    protected function parsePyString()
546
    {
547 12
        $token = $this->expectTokenType('PyStringOp');
548
549 12
        $line = $token['line'];
550
551 12
        $strings = array();
552 12
        while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
553 12
            $token = $this->expectTokenType('Text');
554
555 12
            $strings[] = $token['value'];
556
        }
557
558 12
        $this->expectTokenType('PyStringOp');
559
560 11
        return new PyStringNode($strings, $line);
561
    }
562
563
    /**
564
     * Parses tags.
565
     *
566
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
567
     */
568 6
    protected function parseTags()
569
    {
570 6
        $token = $this->expectTokenType('Tag');
571 6
        $this->tags = array_merge($this->tags, $token['tags']);
572
573 6
        return $this->parseExpression();
574
    }
575
576
    /**
577
     * Returns current set of tags and clears tag buffer.
578
     *
579
     * @return array
580
     */
581 262
    protected function popTags()
582
    {
583 262
        $tags = $this->tags;
584 262
        $this->tags = array();
585
586 262
        return $tags;
587
    }
588
589
    /**
590
     * Parses next text line & returns it.
591
     *
592
     * @return string
593
     */
594 224
    protected function parseText()
595
    {
596 224
        $token = $this->expectTokenType('Text');
597
598 224
        return $token['value'];
599
    }
600
601
    /**
602
     * Parses next newline & returns \n.
603
     *
604
     * @return string
605
     */
606 258
    protected function parseNewline()
607
    {
608 258
        $this->expectTokenType('Newline');
609
610 258
        return "\n";
611
    }
612
613
    /**
614
     * Parses next comment token & returns it's string content.
615
     *
616
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
617
     */
618 8
    protected function parseComment()
619
    {
620 8
        $this->expectTokenType('Comment');
621
622 8
        return $this->parseExpression();
623
    }
624
625
    /**
626
     * Parses language block and updates lexer configuration based on it.
627
     *
628
     * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
629
     *
630
     * @throws ParserException
631
     */
632 197
    protected function parseLanguage()
633
    {
634 197
        $token = $this->expectTokenType('Language');
635
636 197
        if (null === $this->languageSpecifierLine) {
637 197
            $this->lexer->analyse($this->input, $token['value']);
638 197
            $this->languageSpecifierLine = $token['line'];
639 197
        } elseif ($token['line'] !== $this->languageSpecifierLine) {
640 1
            throw new ParserException(sprintf(
641 1
                'Ambiguous language specifiers on lines: %d and %d%s',
642 1
                $this->languageSpecifierLine,
643 1
                $token['line'],
644 1
                $this->file ? ' in file: ' . $this->file : ''
645
            ));
646
        }
647
648 197
        return $this->parseExpression();
649
    }
650
651
    /**
652
     * Parses the rows of a table
653
     *
654
     * @return string[][]
655
     */
656 214
    private function parseTableRows()
657
    {
658 214
        $table = array();
659 214
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
660 214
            if ('Comment' === $predicted || 'Newline' === $predicted) {
661 112
                $this->acceptTokenType($predicted);
662 112
                continue;
663
            }
664
665 214
            $token = $this->expectTokenType('TableRow');
666
667 214
            $table[$token['line']] = $token['columns'];
668
        }
669
670 214
        return $table;
671
    }
672
673
    /**
674
     * Changes step node type for types But, And to type of previous step if it exists else sets to Given
675
     *
676
     * @param StepNode   $node
677
     * @param StepNode[] $steps
678
     * @return StepNode
679
     */
680 245
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
681
    {
682 245
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
683 207
            if (($prev = end($steps))) {
684 204
                $keywordType = $prev->getKeywordType();
685
            } else {
686 3
                $keywordType = 'Given';
687
            }
688
689 207
            $node = new StepNode(
690 207
                $node->getKeyword(),
691 207
                $node->getText(),
692 207
                $node->getArguments(),
693 207
                $node->getLine(),
694 207
                $keywordType
695
            );
696
        }
697 245
        return $node;
698
    }
699
}
700