packages/bison-parser/spec/Context/GrammarContext.php
1<?php
2
3declare(strict_types=1);
4
5namespace Spec\Context;
6
7use Behat\Behat\Context\Context;
8use Behat\Gherkin\Node\PyStringNode;
9use Behat\Step\Given;
10use Behat\Step\Then;
11use Behat\Step\When;
12use BisonParser\Ast\GrammarFile;
13use BisonParser\Parser;
14use BisonParser\SyntaxException;
15use PHPUnit\Framework\Assert;
16use PHPUnit\Framework\AssertionFailedError;
17
18/**
19 * Steps of the specification: give a grammar file, parse it with the public
20 * parser, and state either the whole resulting tree or the position at which
21 * parsing fails.
22 */
23final class GrammarContext implements Context
24{
25 private string $source = '';
26
27 private ?GrammarFile $tree = null;
28
29 private ?SyntaxException $failure = null;
30
31 /**
32 * Wires the parser under specification and the dumper that renders its trees.
33 *
34 * @param Parser $parser The parser every scenario runs
35 * @param TreeDumper $dumper Renders a tree as the lines a scenario states
36 */
37 public function __construct(
38 private readonly Parser $parser = new Parser(),
39 private readonly TreeDumper $dumper = new TreeDumper(),
40 ) {
41 }
42
43 /**
44 * Takes the grammar file of the scenario, ending it with a newline as files do.
45 *
46 * @param PyStringNode $file The text of the grammar file
47 */
48 #[Given('the grammar file:')]
49 public function theGrammarFile(PyStringNode $file): void
50 {
51 $this->source = $file->getRaw() . "\n";
52 $this->tree = null;
53 $this->failure = null;
54 }
55
56 /**
57 * Takes the grammar file of the scenario exactly as written, without a final newline.
58 *
59 * @param PyStringNode $file The text of the grammar file
60 */
61 #[Given('the grammar file without a final newline:')]
62 public function theGrammarFileWithoutAFinalNewline(PyStringNode $file): void
63 {
64 $this->source = $file->getRaw();
65 $this->tree = null;
66 $this->failure = null;
67 }
68
69 /**
70 * Runs the parser on the grammar file, keeping either the tree or the syntax error.
71 */
72 #[When('the file is parsed')]
73 public function theFileIsParsed(): void
74 {
75 try {
76 $this->tree = $this->parser->parse($this->source);
77 } catch (SyntaxException $failure) {
78 $this->failure = $failure;
79 }
80 }
81
82 /**
83 * States the whole tree the parse produced, one node per line.
84 *
85 * @param PyStringNode $expected The rendering of the tree, as TreeDumper writes it
86 *
87 * @throws AssertionFailedError When parsing failed or produced another tree
88 */
89 #[Then('the tree is:')]
90 public function theTreeIs(PyStringNode $expected): void
91 {
92 Assert::assertSame($expected->getRaw(), $this->dumper->dump($this->tree()));
93 }
94
95 /**
96 * States that the parse produced the same tree as another grammar file does.
97 *
98 * @param PyStringNode $other The other grammar file, ended with a newline
99 *
100 * @throws AssertionFailedError When parsing failed or the trees differ
101 */
102 #[Then('the tree is the same as for:')]
103 public function theTreeIsTheSameAsFor(PyStringNode $other): void
104 {
105 Assert::assertSame($this->dumper->dump($this->parser->parse($other->getRaw() . "\n")), $this->dumper->dump($this->tree()));
106 }
107
108 /**
109 * States that parsing failed, and where.
110 *
111 * @param string $line The line of the error, counted from 1
112 * @param string $column The column of the error, counted from 1
113 *
114 * @throws AssertionFailedError When parsing succeeded or failed elsewhere
115 */
116 #[Then('parsing fails at line :line column :column')]
117 public function parsingFailsAt(string $line, string $column): void
118 {
119 if ($this->failure === null) {
120 throw new AssertionFailedError("Parsing succeeded:\n" . $this->dumper->dump($this->tree()));
121 }
122 Assert::assertSame("{$line}:{$column}", (string) $this->failure->location, $this->failure->getMessage());
123 }
124
125 /**
126 * The tree of the last parse.
127 *
128 * @return GrammarFile The tree
129 *
130 * @throws AssertionFailedError When there was no parse, or it failed
131 */
132 public function tree(): GrammarFile
133 {
134 if ($this->failure !== null) {
135 throw new AssertionFailedError('Parsing failed: ' . $this->failure->getMessage());
136 }
137 if ($this->tree === null) {
138 throw new AssertionFailedError('The file has not been parsed');
139 }
140
141 return $this->tree;
142 }
143}
144