4 * This file is part of the Behat Gherkin.
5 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 namespace Behat\Gherkin;
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;
28 * $lexer = new Behat\Gherkin\Lexer($keywords);
29 * $parser = new Behat\Gherkin\Parser($lexer);
30 * $featuresArray = $parser->parse('/path/to/feature.feature');
32 * @author Konstantin Kudryashov <ever.zet@gmail.com>
39 private $tags = array();
40 private $languageSpecifierLine;
45 * @param Lexer $lexer Lexer instance
47 public function __construct(Lexer $lexer)
49 $this->lexer = $lexer;
53 * Parses input & returns features array.
55 * @param string $input Gherkin string document
56 * @param string $file File name
58 * @return FeatureNode|null
60 * @throws ParserException
62 public function parse($input, $file = null)
64 $this->languageSpecifierLine = null;
65 $this->input = $input;
67 $this->tags = array();
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),
80 while ('EOS' !== ($predicted = $this->predictTokenType())) {
81 $node = $this->parseExpression();
83 if (null === $node || "\n" === $node) {
87 if (!$feature && $node instanceof FeatureNode) {
92 if ($feature && $node instanceof FeatureNode) {
93 throw new ParserException(sprintf(
94 'Only one feature is allowed per feature file. But %s got multiple.',
99 if (is_string($node)) {
100 throw new ParserException(sprintf(
101 'Expected Feature, but got text: "%s"%s',
103 $this->file ? ' in file: ' . $this->file : ''
107 if (!$node instanceof FeatureNode) {
108 throw new ParserException(sprintf(
109 'Expected Feature, but got %s on line: %d%s',
112 $this->file ? ' in file: ' . $this->file : ''
121 * Returns next token if it's type equals to expected.
123 * @param string $type Token type
127 * @throws Exception\ParserException
129 protected function expectTokenType($type)
131 $types = (array) $type;
132 if (in_array($this->predictTokenType(), $types)) {
133 return $this->lexer->getAdvancedToken();
136 $token = $this->lexer->predictToken();
138 throw new ParserException(sprintf(
139 'Expected %s token, but got %s on line: %d%s',
140 implode(' or ', $types),
141 $this->predictTokenType(),
143 $this->file ? ' in file: ' . $this->file : ''
148 * Returns next token if it's type equals to expected.
150 * @param string $type Token type
154 protected function acceptTokenType($type)
156 if ($type !== $this->predictTokenType()) {
160 return $this->lexer->getAdvancedToken();
164 * Returns next token type without real input reading (prediction).
168 protected function predictTokenType()
170 $token = $this->lexer->predictToken();
172 return $token['type'];
176 * Parses current expression & returns Node.
178 * @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
180 * @throws ParserException
182 protected function parseExpression()
184 switch ($type = $this->predictTokenType()) {
186 return $this->parseFeature();
188 return $this->parseBackground();
190 return $this->parseScenario();
192 return $this->parseOutline();
194 return $this->parseExamples();
196 return $this->parseTable();
198 return $this->parsePyString();
200 return $this->parseStep();
202 return $this->parseText();
204 return $this->parseNewline();
206 return $this->parseTags();
208 return $this->parseComment();
210 return $this->parseLanguage();
215 throw new ParserException(sprintf('Unknown token type: %s', $type));
219 * Parses feature token & returns it's node.
221 * @return FeatureNode
223 * @throws ParserException
225 protected function parseFeature()
227 $token = $this->expectTokenType('Feature');
229 $title = trim($token['value']) ?: null;
231 $tags = $this->popTags();
233 $scenarios = array();
234 $keyword = $token['keyword'];
235 $language = $this->lexer->getLanguage();
237 $line = $token['line'];
239 // Parse description, background, scenarios & outlines
240 while ('EOS' !== $this->predictTokenType()) {
241 $node = $this->parseExpression();
243 if (is_string($node)) {
244 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
245 $description .= (null !== $description ? "\n" : '') . $text;
249 if (!$background && $node instanceof BackgroundNode) {
254 if ($node instanceof ScenarioInterface) {
255 $scenarios[] = $node;
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(),
264 $this->file ? ' in file: ' . $this->file : ''
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(),
273 $this->file ? ' in file: ' . $this->file : ''
278 return new FeatureNode(
279 rtrim($title) ?: null,
280 rtrim($description) ?: null,
292 * Parses background token & returns it's node.
294 * @return BackgroundNode
296 * @throws ParserException
298 protected function parseBackground()
300 $token = $this->expectTokenType('Background');
302 $title = trim($token['value']);
303 $keyword = $token['keyword'];
304 $line = $token['line'];
306 if (count($this->popTags())) {
307 throw new ParserException(sprintf(
308 'Background can not be tagged, but it is on line: %d%s',
310 $this->file ? ' in file: ' . $this->file : ''
314 // Parse description and steps
316 $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
317 while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
318 $node = $this->parseExpression();
320 if ($node instanceof StepNode) {
321 $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
325 if (!count($steps) && is_string($node)) {
326 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
327 $title .= "\n" . $text;
331 if ("\n" === $node) {
335 if (is_string($node)) {
336 throw new ParserException(sprintf(
337 'Expected Step, but got text: "%s"%s',
339 $this->file ? ' in file: ' . $this->file : ''
343 if (!$node instanceof StepNode) {
344 throw new ParserException(sprintf(
345 'Expected Step, but got %s on line: %d%s',
346 $node->getNodeType(),
348 $this->file ? ' in file: ' . $this->file : ''
353 return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
357 * Parses scenario token & returns it's node.
359 * @return ScenarioNode
361 * @throws ParserException
363 protected function parseScenario()
365 $token = $this->expectTokenType('Scenario');
367 $title = trim($token['value']);
368 $tags = $this->popTags();
369 $keyword = $token['keyword'];
370 $line = $token['line'];
372 // Parse description and steps
374 while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
375 $node = $this->parseExpression();
377 if ($node instanceof StepNode) {
378 $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
382 if (!count($steps) && is_string($node)) {
383 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
384 $title .= "\n" . $text;
388 if ("\n" === $node) {
392 if (is_string($node)) {
393 throw new ParserException(sprintf(
394 'Expected Step, but got text: "%s"%s',
396 $this->file ? ' in file: ' . $this->file : ''
400 if (!$node instanceof StepNode) {
401 throw new ParserException(sprintf(
402 'Expected Step, but got %s on line: %d%s',
403 $node->getNodeType(),
405 $this->file ? ' in file: ' . $this->file : ''
410 return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
414 * Parses scenario outline token & returns it's node.
416 * @return OutlineNode
418 * @throws ParserException
420 protected function parseOutline()
422 $token = $this->expectTokenType('Outline');
424 $title = trim($token['value']);
425 $tags = $this->popTags();
426 $keyword = $token['keyword'];
428 $line = $token['line'];
430 // Parse description, steps and examples
432 while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) {
433 $node = $this->parseExpression();
435 if ($node instanceof StepNode) {
436 $steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
440 if ($node instanceof ExampleTableNode) {
445 if (!count($steps) && is_string($node)) {
446 $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
447 $title .= "\n" . $text;
451 if ("\n" === $node) {
455 if (is_string($node)) {
456 throw new ParserException(sprintf(
457 'Expected Step or Examples table, but got text: "%s"%s',
459 $this->file ? ' in file: ' . $this->file : ''
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(),
468 $this->file ? ' in file: ' . $this->file : ''
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',
478 $this->file ? ' in file: ' . $this->file : ''
482 return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
486 * Parses step token & returns it's node.
490 protected function parseStep()
492 $token = $this->expectTokenType('Step');
494 $keyword = $token['value'];
495 $keywordType = $token['keyword_type'];
496 $text = trim($token['text']);
497 $line = $token['line'];
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);
506 $node = $this->parseExpression();
508 if ($node instanceof PyStringNode || $node instanceof TableNode) {
509 $arguments[] = $node;
513 return new StepNode($keyword, $text, $arguments, $line, $keywordType);
517 * Parses examples table node.
519 * @return ExampleTableNode
521 protected function parseExamples()
523 $token = $this->expectTokenType('Examples');
525 $keyword = $token['keyword'];
527 return new ExampleTableNode($this->parseTableRows(), $keyword);
531 * Parses table token & returns it's node.
535 protected function parseTable()
537 return new TableNode($this->parseTableRows());
541 * Parses PyString token & returns it's node.
543 * @return PyStringNode
545 protected function parsePyString()
547 $token = $this->expectTokenType('PyStringOp');
549 $line = $token['line'];
552 while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
553 $token = $this->expectTokenType('Text');
555 $strings[] = $token['value'];
558 $this->expectTokenType('PyStringOp');
560 return new PyStringNode($strings, $line);
566 * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
568 protected function parseTags()
570 $token = $this->expectTokenType('Tag');
571 $this->tags = array_merge($this->tags, $token['tags']);
573 return $this->parseExpression();
577 * Returns current set of tags and clears tag buffer.
581 protected function popTags()
584 $this->tags = array();
590 * Parses next text line & returns it.
594 protected function parseText()
596 $token = $this->expectTokenType('Text');
598 return $token['value'];
602 * Parses next newline & returns \n.
606 protected function parseNewline()
608 $this->expectTokenType('Newline');
614 * Parses next comment token & returns it's string content.
616 * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
618 protected function parseComment()
620 $this->expectTokenType('Comment');
622 return $this->parseExpression();
626 * Parses language block and updates lexer configuration based on it.
628 * @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
630 * @throws ParserException
632 protected function parseLanguage()
634 $token = $this->expectTokenType('Language');
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,
644 $this->file ? ' in file: ' . $this->file : ''
648 return $this->parseExpression();
652 * Parses the rows of a table
656 private function parseTableRows()
659 while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
660 if ('Comment' === $predicted || 'Newline' === $predicted) {
661 $this->acceptTokenType($predicted);
665 $token = $this->expectTokenType('TableRow');
667 $table[$token['line']] = $token['columns'];
674 * Changes step node type for types But, And to type of previous step if it exists else sets to Given
676 * @param StepNode $node
677 * @param StepNode[] $steps
680 private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
682 if (in_array($node->getKeywordType(), array('And', 'But'))) {
683 if (($prev = end($steps))) {
684 $keywordType = $prev->getKeywordType();
686 $keywordType = 'Given';
689 $node = new StepNode(
692 $node->getArguments(),