Security update for permissions_by_term
[yaffs-website] / vendor / behat / gherkin / src / Behat / Gherkin / Parser.php
1 <?php
2
3 /*
4  * This file is part of the Behat Gherkin.
5  * (c) Konstantin Kudryashov <ever.zet@gmail.com>
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 <ever.zet@gmail.com>
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     public function __construct(Lexer $lexer)
48     {
49         $this->lexer = $lexer;
50     }
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     public function parse($input, $file = null)
63     {
64         $this->languageSpecifierLine = null;
65         $this->input = $input;
66         $this->file = $file;
67         $this->tags = array();
68
69         try {
70             $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         $feature = null;
80         while ('EOS' !== ($predicted = $this->predictTokenType())) {
81             $node = $this->parseExpression();
82
83             if (null === $node || "\n" === $node) {
84                 continue;
85             }
86
87             if (!$feature && $node instanceof FeatureNode) {
88                 $feature = $node;
89                 continue;
90             }
91
92             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             if (is_string($node)) {
100                 throw new ParserException(sprintf(
101                     'Expected Feature, but got text: "%s"%s',
102                     $node,
103                     $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(),
111                     $node->getLine(),
112                     $this->file ? ' in file: ' . $this->file : ''
113                 ));
114             }
115         }
116
117         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     protected function expectTokenType($type)
130     {
131         $types = (array) $type;
132         if (in_array($this->predictTokenType(), $types)) {
133             return $this->lexer->getAdvancedToken();
134         }
135
136         $token = $this->lexer->predictToken();
137
138         throw new ParserException(sprintf(
139             'Expected %s token, but got %s on line: %d%s',
140             implode(' or ', $types),
141             $this->predictTokenType(),
142             $token['line'],
143             $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     protected function acceptTokenType($type)
155     {
156         if ($type !== $this->predictTokenType()) {
157             return null;
158         }
159
160         return $this->lexer->getAdvancedToken();
161     }
162
163     /**
164      * Returns next token type without real input reading (prediction).
165      *
166      * @return string
167      */
168     protected function predictTokenType()
169     {
170         $token = $this->lexer->predictToken();
171
172         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     protected function parseExpression()
183     {
184         switch ($type = $this->predictTokenType()) {
185             case 'Feature':
186                 return $this->parseFeature();
187             case 'Background':
188                 return $this->parseBackground();
189             case 'Scenario':
190                 return $this->parseScenario();
191             case 'Outline':
192                 return $this->parseOutline();
193             case 'Examples':
194                 return $this->parseExamples();
195             case 'TableRow':
196                 return $this->parseTable();
197             case 'PyStringOp':
198                 return $this->parsePyString();
199             case 'Step':
200                 return $this->parseStep();
201             case 'Text':
202                 return $this->parseText();
203             case 'Newline':
204                 return $this->parseNewline();
205             case 'Tag':
206                 return $this->parseTags();
207             case 'Comment':
208                 return $this->parseComment();
209             case 'Language':
210                 return $this->parseLanguage();
211             case 'EOS':
212                 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     protected function parseFeature()
226     {
227         $token = $this->expectTokenType('Feature');
228
229         $title = trim($token['value']) ?: null;
230         $description = null;
231         $tags = $this->popTags();
232         $background = null;
233         $scenarios = array();
234         $keyword = $token['keyword'];
235         $language = $this->lexer->getLanguage();
236         $file = $this->file;
237         $line = $token['line'];
238
239         // Parse description, background, scenarios & outlines
240         while ('EOS' !== $this->predictTokenType()) {
241             $node = $this->parseExpression();
242
243             if (is_string($node)) {
244                 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
245                 $description .= (null !== $description ? "\n" : '') . $text;
246                 continue;
247             }
248
249             if (!$background && $node instanceof BackgroundNode) {
250                 $background = $node;
251                 continue;
252             }
253
254             if ($node instanceof ScenarioInterface) {
255                 $scenarios[] = $node;
256                 continue;
257             }
258
259             if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
260                 throw new ParserException(sprintf(
261                     'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
262                     $background->getLine(),
263                     $node->getLine(),
264                     $this->file ? ' in file: ' . $this->file : ''
265                 ));
266             }
267
268             if (!$node instanceof ScenarioNode) {
269                 throw new ParserException(sprintf(
270                     'Expected Scenario, Outline or Background, but got %s on line: %d%s',
271                     $node->getNodeType(),
272                     $node->getLine(),
273                     $this->file ? ' in file: ' . $this->file : ''
274                 ));
275             }
276         }
277
278         return new FeatureNode(
279             rtrim($title) ?: null,
280             rtrim($description) ?: null,
281             $tags,
282             $background,
283             $scenarios,
284             $keyword,
285             $language,
286             $file,
287             $line
288         );
289     }
290
291     /**
292      * Parses background token & returns it's node.
293      *
294      * @return BackgroundNode
295      *
296      * @throws ParserException
297      */
298     protected function parseBackground()
299     {
300         $token = $this->expectTokenType('Background');
301
302         $title = trim($token['value']);
303         $keyword = $token['keyword'];
304         $line = $token['line'];
305
306         if (count($this->popTags())) {
307             throw new ParserException(sprintf(
308                 'Background can not be tagged, but it is on line: %d%s',
309                 $line,
310                 $this->file ? ' in file: ' . $this->file : ''
311             ));
312         }
313
314         // Parse description and steps
315         $steps = array();
316         $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
317         while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
318             $node = $this->parseExpression();
319
320             if ($node instanceof StepNode) {
321                 $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
322                 continue;
323             }
324
325             if (!count($steps) && is_string($node)) {
326                 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
327                 $title .= "\n" . $text;
328                 continue;
329             }
330
331             if ("\n" === $node) {
332                 continue;
333             }
334
335             if (is_string($node)) {
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             if (!$node instanceof StepNode) {
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         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     protected function parseScenario()
364     {
365         $token = $this->expectTokenType('Scenario');
366
367         $title = trim($token['value']);
368         $tags = $this->popTags();
369         $keyword = $token['keyword'];
370         $line = $token['line'];
371
372         // Parse description and steps
373         $steps = array();
374         while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
375             $node = $this->parseExpression();
376
377             if ($node instanceof StepNode) {
378                 $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
379                 continue;
380             }
381
382             if (!count($steps) && is_string($node)) {
383                 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
384                 $title .= "\n" . $text;
385                 continue;
386             }
387
388             if ("\n" === $node) {
389                 continue;
390             }
391
392             if (is_string($node)) {
393                 throw new ParserException(sprintf(
394                     'Expected Step, but got text: "%s"%s',
395                     $node,
396                     $this->file ? ' in file: ' . $this->file : ''
397                 ));
398             }
399
400             if (!$node instanceof StepNode) {
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         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     protected function parseOutline()
421     {
422         $token = $this->expectTokenType('Outline');
423
424         $title = trim($token['value']);
425         $tags = $this->popTags();
426         $keyword = $token['keyword'];
427         $examples = null;
428         $line = $token['line'];
429
430         // Parse description, steps and examples
431         $steps = array();
432         while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
433             $node = $this->parseExpression();
434
435             if ($node instanceof StepNode) {
436                 $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
437                 continue;
438             }
439
440             if ($node instanceof ExampleTableNode) {
441                 $examples = $node;
442                 continue;
443             }
444
445             if (!count($steps) && is_string($node)) {
446                 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
447                 $title .= "\n" . $text;
448                 continue;
449             }
450
451             if ("\n" === $node) {
452                 continue;
453             }
454
455             if (is_string($node)) {
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             if (!$node instanceof StepNode) {
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         if (null === $examples) {
474             throw new ParserException(sprintf(
475                 'Outline should have examples table, but got none for outline "%s" on line: %d%s',
476                 rtrim($title),
477                 $line,
478                 $this->file ? ' in file: ' . $this->file : ''
479             ));
480         }
481
482         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     protected function parseStep()
491     {
492         $token = $this->expectTokenType('Step');
493
494         $keyword = $token['value'];
495         $keywordType = $token['keyword_type'];
496         $text = trim($token['text']);
497         $line = $token['line'];
498
499         $arguments = array();
500         while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
501             if ('Comment' === $predicted || 'Newline' === $predicted) {
502                 $this->acceptTokenType($predicted);
503                 continue;
504             }
505
506             $node = $this->parseExpression();
507
508             if ($node instanceof PyStringNode || $node instanceof TableNode) {
509                 $arguments[] = $node;
510             }
511         }
512
513         return new StepNode($keyword, $text, $arguments, $line, $keywordType);
514     }
515
516     /**
517      * Parses examples table node.
518      *
519      * @return ExampleTableNode
520      */
521     protected function parseExamples()
522     {
523         $token = $this->expectTokenType('Examples');
524
525         $keyword = $token['keyword'];
526
527         return new ExampleTableNode($this->parseTableRows(), $keyword);
528     }
529
530     /**
531      * Parses table token & returns it's node.
532      *
533      * @return TableNode
534      */
535     protected function parseTable()
536     {
537         return new TableNode($this->parseTableRows());
538     }
539
540     /**
541      * Parses PyString token & returns it's node.
542      *
543      * @return PyStringNode
544      */
545     protected function parsePyString()
546     {
547         $token = $this->expectTokenType('PyStringOp');
548
549         $line = $token['line'];
550
551         $strings = array();
552         while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
553             $token = $this->expectTokenType('Text');
554
555             $strings[] = $token['value'];
556         }
557
558         $this->expectTokenType('PyStringOp');
559
560         return new PyStringNode($strings, $line);
561     }
562
563     /**
564      * Parses tags.
565      *
566      * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
567      */
568     protected function parseTags()
569     {
570         $token = $this->expectTokenType('Tag');
571         $this->tags = array_merge($this->tags, $token['tags']);
572
573         return $this->parseExpression();
574     }
575
576     /**
577      * Returns current set of tags and clears tag buffer.
578      *
579      * @return array
580      */
581     protected function popTags()
582     {
583         $tags = $this->tags;
584         $this->tags = array();
585
586         return $tags;
587     }
588
589     /**
590      * Parses next text line & returns it.
591      *
592      * @return string
593      */
594     protected function parseText()
595     {
596         $token = $this->expectTokenType('Text');
597
598         return $token['value'];
599     }
600
601     /**
602      * Parses next newline & returns \n.
603      *
604      * @return string
605      */
606     protected function parseNewline()
607     {
608         $this->expectTokenType('Newline');
609
610         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     protected function parseComment()
619     {
620         $this->expectTokenType('Comment');
621
622         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     protected function parseLanguage()
633     {
634         $token = $this->expectTokenType('Language');
635
636         if (null === $this->languageSpecifierLine) {
637             $this->lexer->analyse($this->input, $token['value']);
638             $this->languageSpecifierLine = $token['line'];
639         } elseif ($token['line'] !== $this->languageSpecifierLine) {
640             throw new ParserException(sprintf(
641                 'Ambiguous language specifiers on lines: %d and %d%s',
642                 $this->languageSpecifierLine,
643                 $token['line'],
644                 $this->file ? ' in file: ' . $this->file : ''
645             ));
646         }
647
648         return $this->parseExpression();
649     }
650
651     /**
652      * Parses the rows of a table
653      *
654      * @return string[][]
655      */
656     private function parseTableRows()
657     {
658         $table = array();
659         while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
660             if ('Comment' === $predicted || 'Newline' === $predicted) {
661                 $this->acceptTokenType($predicted);
662                 continue;
663             }
664
665             $token = $this->expectTokenType('TableRow');
666
667             $table[$token['line']] = $token['columns'];
668         }
669
670         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     private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
681     {
682         if (in_array($node->getKeywordType(), array('And', 'But'))) {
683             if (($prev = end($steps))) {
684                 $keywordType = $prev->getKeywordType();
685             } else {
686                 $keywordType = 'Given';
687             }
688
689             $node = new StepNode(
690                 $node->getKeyword(),
691                 $node->getText(),
692                 $node->getArguments(),
693                 $node->getLine(),
694                 $keywordType
695             );
696         }
697         return $node;
698     }
699 }