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