Completed
Pull Request — master (#119)
by
unknown
02:32
created

Parser   D

Complexity

Total Complexity 119

Size/Duplication

Total Lines 704
Duplicated Lines 11.36 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 81.44%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 119
lcom 1
cbo 10
dl 80
loc 704
ccs 294
cts 361
cp 0.8144
rs 4
c 5
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C parse() 7 57 13
A expectTokenType() 0 17 3
A acceptTokenType() 0 8 2
A predictTokenType() 0 6 1
C parseExpression() 0 35 15
C parseFeature() 13 67 15
C parseBackground() 20 57 13
C parseScenario() 20 53 11
C parseOutline() 20 70 14
B parseStep() 0 29 6
A parseExamples() 0 10 2
A parseTable() 0 4 1
A parsePyString() 0 17 3
B parseTags() 0 25 4
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
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
    private $passedNodesStack = array();
43
44
    /**
45
     * Initializes parser.
46
     *
47
     * @param Lexer $lexer Lexer instance
48
     */
49 56
    public function __construct(Lexer $lexer)
50
    {
51 56
        $this->lexer = $lexer;
52 56
    }
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 54
    public function parse($input, $file = null)
65
    {
66 54
        $this->languageSpecifierLine = null;
67 54
        $this->input = $input;
68 54
        $this->file = $file;
69 54
        $this->tags = array();
70
71
        try {
72 54
            $this->lexer->analyse($this->input, 'en');
73 54
        } 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 54
        $feature = null;
82 54
        while ('EOS' !== ($predicted = $this->predictTokenType())) {
83 54
            $node = $this->parseExpression();
84
85 46
            if (null === $node || "\n" === $node) {
86 3
                continue;
87
            }
88
89 45
            if (!$feature && $node instanceof FeatureNode) {
90 45
                $feature = $node;
91 45
                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 45
        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 54
    protected function expectTokenType($type)
132
    {
133 54
        $types = (array) $type;
134 54
        if (in_array($this->predictTokenType(), $types)) {
135 54
            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 1
        ));
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 36
    protected function acceptTokenType($type)
157
    {
158 36
        if ($type !== $this->predictTokenType()) {
159
            return null;
160
        }
161
162 36
        return $this->lexer->getAdvancedToken();
163
    }
164
165
    /**
166
     * Returns next token type without real input reading (prediction).
167
     *
168
     * @return string
169
     */
170 54
    protected function predictTokenType()
171
    {
172 54
        $token = $this->lexer->predictToken();
173
174 54
        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 54
    protected function parseExpression()
185
    {
186 54
        switch ($type = $this->predictTokenType()) {
187 54
            case 'Feature':
188 53
                return $this->parseFeature();
189 54
            case 'Background':
190 10
                return $this->parseBackground();
191 54
            case 'Scenario':
192 39
                return $this->parseScenario();
193 54
            case 'Outline':
194 18
                return $this->parseOutline();
195 54
            case 'Examples':
196 17
                return $this->parseExamples();
197 54
            case 'TableRow':
198 3
                return $this->parseTable();
199 54
            case 'PyStringOp':
200 10
                return $this->parsePyString();
201 54
            case 'Step':
202 41
                return $this->parseStep();
203 53
            case 'Text':
204 29
                return $this->parseText();
205 53
            case 'Newline':
206 46
                return $this->parseNewline();
207 25
            case 'Tag':
208 10
                return $this->parseTags();
209 16
            case 'Comment':
210 8
                return $this->parseComment();
211 12
            case 'Language':
212 9
                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 53
    protected function parseFeature()
228
    {
229 53
        $token = $this->expectTokenType('Feature');
230
231 53
        $title = trim($token['value']) ?: null;
232 53
        $description = null;
233 53
        $tags = $this->popTags();
234 53
        $background = null;
235 53
        $scenarios = array();
236 53
        $keyword = $token['keyword'];
237 53
        $language = $this->lexer->getLanguage();
238 53
        $file = $this->file;
239 53
        $line = $token['line'];
240
241 53
        array_push($this->passedNodesStack, 'Feature');
242
243
        // Parse description, background, scenarios & outlines
244 53
        while ('EOS' !== $this->predictTokenType()) {
245 53
            $node = $this->parseExpression();
246
247 53 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 45
                $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
249 45
                $description .= (null !== $description ? "\n" : '') . $text;
250 45
                continue;
251
            }
252
253 44
            if (!$background && $node instanceof BackgroundNode) {
254 9
                $background = $node;
255 9
                continue;
256
            }
257
258 44
            if ($node instanceof ScenarioInterface) {
259 42
                $scenarios[] = $node;
260 42
                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 1
                ));
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 2
                ));
279
            }
280
        }
281
282 46
        return new FeatureNode(
283 46
            rtrim($title) ?: null,
284 46
            rtrim($description) ?: null,
285 46
            $tags,
286 46
            $background,
287 46
            $scenarios,
288 46
            $keyword,
289 46
            $language,
290 46
            $file,
291
            $line
292 46
        );
293
    }
294
295
    /**
296
     * Parses background token & returns it's node.
297
     *
298
     * @return BackgroundNode
299
     *
300
     * @throws ParserException
301
     */
302 10
    protected function parseBackground()
303
    {
304 10
        $token = $this->expectTokenType('Background');
305
306 10
        $title = trim($token['value']);
307 10
        $keyword = $token['keyword'];
308 10
        $line = $token['line'];
309
310 10
        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 1
            ));
316
        }
317
318
        // Parse description and steps
319 9
        $steps = array();
320 9
        $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
321 9
        while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
322 9
            $node = $this->parseExpression();
323
324 9
            if ($node instanceof StepNode) {
325 8
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
326 8
                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 9
        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 39
    protected function parseScenario()
368
    {
369 39
        $token = $this->expectTokenType('Scenario');
370
371 39
        $title = trim($token['value']);
372 39
        $tags = $this->popTags();
373 39
        $keyword = $token['keyword'];
374 39
        $line = $token['line'];
375
376 39
        array_push($this->passedNodesStack, 'Scenario');
377
378
        // Parse description and steps
379 39
        $steps = array();
380 39
        while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
381 38
            $node = $this->parseExpression();
382
383 37
            if ($node instanceof StepNode) {
384 33
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
385 33
                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 2
                ));
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 36
        array_pop($this->passedNodesStack);
417
418 36
        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 18
    protected function parseOutline()
429
    {
430 18
        $token = $this->expectTokenType('Outline');
431
432 18
        $title = trim($token['value']);
433 18
        $tags = $this->popTags();
434 18
        $keyword = $token['keyword'];
435
436
        /** @var ExampleTableNode $examples */
437 18
        $examples = array();
438 18
        $line = $token['line'];
439
440
        // Parse description, steps and examples
441 18
        $steps = array();
442
443 18
        array_push($this->passedNodesStack, 'Outline');
444
445 18
        while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'))) {
446 17
            $node = $this->parseExpression();
447
448 17
            if ($node instanceof StepNode) {
449 16
                $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
450 16
                continue;
451
            }
452
453 17
            if ($node instanceof ExampleTableNode) {
454 17
                $examples[] = $node;
455
456 17
                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 18
        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 1
            ));
494
        }
495
496 17
        return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
0 ignored issues
show
Documentation introduced by
$examples is of type object<Behat\Gherkin\Node\ExampleTableNode>, but the function expects a array<integer,object<Beh...Node\ExampleTableNode>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
497
    }
498
499
    /**
500
     * Parses step token & returns it's node.
501
     *
502
     * @return StepNode
503
     */
504 41
    protected function parseStep()
505
    {
506 41
        $token = $this->expectTokenType('Step');
507
508 41
        $keyword = $token['value'];
509 41
        $keywordType = $token['keyword_type'];
510 41
        $text = trim($token['text']);
511 41
        $line = $token['line'];
512
513 41
        array_push($this->passedNodesStack, 'Step');
514
515 41
        $arguments = array();
516 41
        while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
517 36
            if ('Comment' === $predicted || 'Newline' === $predicted) {
518 34
                $this->acceptTokenType($predicted);
519 34
                continue;
520
            }
521
522 12
            $node = $this->parseExpression();
523
524 11
            if ($node instanceof PyStringNode || $node instanceof TableNode) {
525 11
                $arguments[] = $node;
526 11
            }
527 11
        }
528
529 40
        array_pop($this->passedNodesStack);
530
531 40
        return new StepNode($keyword, $text, $arguments, $line, $keywordType);
532
    }
533
534
    /**
535
     * Parses examples table node.
536
     *
537
     * @return ExampleTableNode
538
     */
539 17
    protected function parseExamples()
540
    {
541 17
        $token = $this->expectTokenType('Examples');
542
543 17
        $keyword = $token['keyword'];
544
545 17
        $tags = empty($this->tags) ? array() : $this->popTags();
546
547 17
        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 10
        }
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
            'Outline' => array(
595 10
                'Examples',
596
                'Step'
597 10
            )
598 10
        );
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 10
        }
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 53
    protected function popTags()
620
    {
621 53
        $tags = $this->tags;
622 53
        $this->tags = array();
623
624 53
        return $tags;
625
    }
626
627
    /**
628
     * Parses next text line & returns it.
629
     *
630
     * @return string
631
     */
632 29
    protected function parseText()
633
    {
634 29
        $token = $this->expectTokenType('Text');
635
636 29
        return $token['value'];
637
    }
638
639
    /**
640
     * Parses next newline & returns \n.
641
     *
642
     * @return string
643
     */
644 46
    protected function parseNewline()
645
    {
646 46
        $this->expectTokenType('Newline');
647
648 46
        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 9
    protected function parseLanguage()
671
    {
672 9
        $token = $this->expectTokenType('Language');
673
674 9
        if (null === $this->languageSpecifierLine) {
675 9
            $this->lexer->analyse($this->input, $token['value']);
676 9
            $this->languageSpecifierLine = $token['line'];
677 9
        } 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 1
            ));
684
        }
685
686 9
        return $this->parseExpression();
687
    }
688
689
    /**
690
     * Parses the rows of a table
691
     *
692
     * @return string[][]
693
     */
694 18
    private function parseTableRows()
695
    {
696 18
        $table = array();
697 18
        while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
698 18
            if ('Comment' === $predicted || 'Newline' === $predicted) {
699 16
                $this->acceptTokenType($predicted);
700 16
                continue;
701
            }
702
703 18
            $token = $this->expectTokenType('TableRow');
704
705 18
            $table[$token['line']] = $token['columns'];
706 18
        }
707
708 18
        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 40
    private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
719
    {
720 40
        if (in_array($node->getKeywordType(), array('And', 'But'))) {
721 14
            if (($prev = end($steps))) {
722 14
                $keywordType = $prev->getKeywordType();
723 14
            } else {
724
                $keywordType = 'Given';
725
            }
726
727 14
            $node = new StepNode(
728 14
                $node->getKeyword(),
729 14
                $node->getText(),
730 14
                $node->getArguments(),
731 14
                $node->getLine(),
732
                $keywordType
733 14
            );
734 14
        }
735 40
        return $node;
736
    }
737
}
738