packages/sql-parser/tests/Unit/Grammar/GrammarBuilderTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Grammar;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Small;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlParser\Grammar\Associativity;
12use SqlParser\Grammar\Grammar;
13use SqlParser\Grammar\GrammarBuilder;
14use SqlParser\Grammar\GrammarException;
15use SqlParser\Grammar\Precedence;
16use SqlParser\Grammar\PrecedencePolicy;
17use SqlParser\Grammar\Rule;
18use SqlParser\Grammar\SymbolTable;
19use SqlParser\Grammar\UnknownSymbolException;
20
21#[CoversClass(GrammarBuilder::class)]
22#[UsesClass(Grammar::class)]
23#[UsesClass(GrammarException::class)]
24#[UsesClass(Precedence::class)]
25#[UsesClass(Rule::class)]
26#[UsesClass(SymbolTable::class)]
27#[UsesClass(UnknownSymbolException::class)]
28#[Small]
29final class GrammarBuilderTest extends TestCase
30{
31 public function testTerminal(): void
32 {
33 $builder = new GrammarBuilder();
34 $builder->terminal('NUM');
35
36 self::assertTrue($builder->isTerminal('NUM'));
37 self::assertFalse($builder->isNonterminal('NUM'));
38 }
39
40 public function testNonterminal(): void
41 {
42 $builder = new GrammarBuilder();
43 $builder->nonterminal('expr');
44
45 self::assertTrue($builder->isNonterminal('expr'));
46 self::assertFalse($builder->isTerminal('expr'));
47 }
48
49 public function testIsTerminalCountsTokenClasses(): void
50 {
51 $builder = new GrammarBuilder();
52 $builder->tokenClass('id', ['ID', 'INDEXED']);
53
54 self::assertTrue($builder->isTerminal('id'));
55 self::assertTrue($builder->isTerminal('INDEXED'));
56 }
57
58 public function testIsNonterminal(): void
59 {
60 $builder = new GrammarBuilder();
61 $builder->rule('expr', ['NUM']);
62
63 self::assertTrue($builder->isNonterminal('expr'));
64 }
65
66 public function testRule(): void
67 {
68 $builder = new GrammarBuilder();
69 $builder->terminal('NUM');
70 $builder->terminal('+');
71 $builder->rule('expr', ['expr', '+', 'expr']);
72 $builder->rule('expr', ['NUM']);
73 $grammar = $builder->build();
74
75 self::assertSame(['$end', 'NUM', '+'], $grammar->symbols->terminals());
76 self::assertSame(['$accept', 'expr'], $grammar->symbols->nonterminals());
77 self::assertSame([4, 2, 4], $grammar->rules[1]->rhs);
78 self::assertSame(1, $grammar->rules[2]->ordinal);
79 }
80
81 public function testPrecedence(): void
82 {
83 $builder = new GrammarBuilder();
84 $builder->precedence(['+', '-'], Associativity::Left);
85 $builder->precedence(['*'], Associativity::Right);
86 $builder->rule('expr', ['expr', '+', 'expr']);
87 $grammar = $builder->build();
88
89 self::assertSame(1, $grammar->precedenceOf($grammar->symbols->id('-') ?? -1)?->level);
90 self::assertSame(2, $grammar->precedenceOf($grammar->symbols->id('*') ?? -1)?->level);
91 self::assertSame(Associativity::Right, $grammar->precedenceOf($grammar->symbols->id('*') ?? -1)->associativity);
92 }
93
94 public function testStart(): void
95 {
96 $builder = new GrammarBuilder();
97 $builder->terminal('A');
98 $builder->rule('first', ['A']);
99 $builder->rule('second', ['first']);
100 $builder->start('second');
101
102 self::assertSame('second', $builder->build()->symbols->name($builder->build()->startSymbol()));
103 }
104
105 public function testExpect(): void
106 {
107 $builder = new GrammarBuilder();
108 $builder->expect(59);
109 $builder->rule('s', []);
110
111 self::assertSame(59, $builder->build()->expectedConflicts);
112 }
113
114 public function testPolicy(): void
115 {
116 $builder = new GrammarBuilder();
117 $builder->policy(PrecedencePolicy::FirstRankedTerminal);
118 $builder->rule('s', []);
119
120 self::assertSame(PrecedencePolicy::FirstRankedTerminal, $builder->build()->policy);
121 }
122
123 public function testTokenClass(): void
124 {
125 $builder = new GrammarBuilder();
126 $builder->tokenClass('id', ['ID', 'INDEXED']);
127 $builder->rule('nm', ['id']);
128 $grammar = $builder->build();
129 $class = $grammar->symbols->id('id') ?? -1;
130
131 self::assertTrue($grammar->symbols->isTerminal($class));
132 self::assertSame([$grammar->symbols->id('ID'), $grammar->symbols->id('INDEXED')], $grammar->tokenClasses[$class]);
133 }
134
135 public function testFallback(): void
136 {
137 $builder = new GrammarBuilder();
138 $builder->fallback('ID', ['ABORT', 'AFTER']);
139 $builder->rule('nm', ['ID']);
140 $grammar = $builder->build();
141
142 self::assertSame($grammar->symbols->id('ID'), $grammar->fallbacks[$grammar->symbols->id('ABORT') ?? -1]);
143 self::assertCount(2, $grammar->fallbacks);
144 }
145
146 public function testWildcard(): void
147 {
148 $builder = new GrammarBuilder();
149 $builder->wildcard('ANY');
150 $builder->rule('any', ['ANY']);
151 $grammar = $builder->build();
152
153 self::assertSame($grammar->symbols->id('ANY'), $grammar->wildcard);
154 }
155
156 public function testBuildAugmentsTheGrammar(): void
157 {
158 $builder = new GrammarBuilder();
159 $builder->terminal('A');
160 $builder->rule('s', ['A']);
161 $grammar = $builder->build();
162
163 self::assertSame('$accept', $grammar->symbols->name($grammar->rules[0]->lhs));
164 self::assertSame([$grammar->symbols->id('s'), 0], $grammar->rules[0]->rhs);
165 self::assertNull($grammar->wildcard);
166 self::assertSame([], $grammar->fallbacks);
167 }
168
169 public function testBuildNumbersHiddenRulesWithoutOrdinals(): void
170 {
171 $builder = new GrammarBuilder();
172 $builder->terminal('A');
173 $builder->rule('$@1', [], null, true);
174 $builder->rule('s', ['A', '$@1', 'A']);
175 $builder->rule('s', ['A']);
176 $grammar = $builder->build();
177
178 self::assertTrue($grammar->rules[1]->hidden);
179 self::assertSame(0, $grammar->rules[1]->ordinal);
180 self::assertSame(0, $grammar->rules[2]->ordinal);
181 self::assertSame(1, $grammar->rules[3]->ordinal);
182 }
183
184 public function testFirstVisibleLhs(): void
185 {
186 $builder = new GrammarBuilder();
187 $builder->terminal('A');
188 $builder->rule('$@1', [], null, true);
189 $builder->rule('s', ['A', '$@1', 'A']);
190
191 self::assertSame('s', $builder->firstVisibleLhs());
192 self::assertSame('s', $builder->build()->symbols->name($builder->build()->startSymbol()));
193 }
194
195 public function testBuildResolvesThePrecedenceSymbolOfARule(): void
196 {
197 $builder = new GrammarBuilder();
198 $builder->precedence(['NEG'], Associativity::Right);
199 $builder->terminal('-');
200 $builder->rule('expr', ['-', 'expr'], 'NEG');
201
202 self::assertSame($builder->build()->symbols->id('NEG'), $builder->build()->rules[1]->precedenceSymbol);
203 }
204
205 public function testBuildRejectsAnEmptyGrammar(): void
206 {
207 $this->expectException(GrammarException::class);
208
209 (new GrammarBuilder())->build();
210 }
211
212 public function testBuildRejectsANameThatIsBothTerminalAndNonterminal(): void
213 {
214 $builder = new GrammarBuilder();
215 $builder->terminal('x');
216 $builder->rule('x', []);
217
218 $this->expectException(GrammarException::class);
219
220 $builder->build();
221 }
222
223 public function testBuildRejectsAnUndeclaredSymbol(): void
224 {
225 $builder = new GrammarBuilder();
226 $builder->rule('s', ['missing']);
227
228 $this->expectException(UnknownSymbolException::class);
229
230 $builder->build();
231 }
232
233 public function testNumberedRules(): void
234 {
235 $builder = new GrammarBuilder();
236 $builder->terminal('A');
237 $builder->rule('s', ['A']);
238 $builder->rule('s', []);
239 $rules = $builder->numberedRules(new SymbolTable(['$end', 'A'], ['$accept', 's']));
240
241 self::assertSame([1, 2], [$rules[0]->index, $rules[1]->index]);
242 self::assertSame([0, 1], [$rules[0]->ordinal, $rules[1]->ordinal]);
243 self::assertSame([1], $rules[0]->rhs);
244 }
245
246 public function testId(): void
247 {
248 $builder = new GrammarBuilder();
249 $symbols = new SymbolTable(['$end', 'A'], ['$accept']);
250
251 self::assertSame(1, $builder->id($symbols, 'A'));
252 }
253
254 public function testIdRejectsAnUnknownName(): void
255 {
256 $this->expectException(UnknownSymbolException::class);
257
258 (new GrammarBuilder())->id(new SymbolTable(['$end'], ['$accept']), 'B');
259 }
260}
261