packages/sql-parser/tests/Unit/Parser/LrParserTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Parser;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Small;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlParser\Automaton\ParseTableBuilder;
12use SqlParser\Grammar\Associativity;
13use SqlParser\Grammar\GrammarBuilder;
14use SqlParser\Lexer\Token;
15use SqlParser\Parser\LrParser;
16use SqlParser\Parser\Node;
17use SqlParser\Parser\SyntaxException;
18
19#[CoversClass(LrParser::class)]
20#[UsesClass(Node::class)]
21#[UsesClass(SyntaxException::class)]
22#[UsesClass(Token::class)]
23#[UsesClass(GrammarBuilder::class)]
24#[UsesClass(ParseTableBuilder::class)]
25#[UsesClass(\SqlParser\Automaton\Bitset::class)]
26#[UsesClass(\SqlParser\Automaton\BuildResult::class)]
27#[UsesClass(\SqlParser\Automaton\ClosureIndex::class)]
28#[UsesClass(\SqlParser\Automaton\ConflictResolver::class)]
29#[UsesClass(\SqlParser\Automaton\ConflictSummary::class)]
30#[UsesClass(\SqlParser\Automaton\Digraph::class)]
31#[UsesClass(\SqlParser\Automaton\LookaheadSets::class)]
32#[UsesClass(\SqlParser\Automaton\Lr0Automaton::class)]
33#[UsesClass(\SqlParser\Automaton\Lr0Builder::class)]
34#[UsesClass(\SqlParser\Automaton\NullableSet::class)]
35#[UsesClass(\SqlParser\Automaton\ResolvedState::class)]
36#[UsesClass(\SqlParser\Grammar\Grammar::class)]
37#[UsesClass(\SqlParser\Grammar\Precedence::class)]
38#[UsesClass(\SqlParser\Grammar\Rule::class)]
39#[UsesClass(\SqlParser\Grammar\SymbolTable::class)]
40#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
41#[UsesClass(\SqlParser\Table\ActionCode::class)]
42#[UsesClass(\SqlParser\Table\ArrayRows::class)]
43#[UsesClass(\SqlParser\Table\ParseTable::class)]
44#[UsesClass(\SqlParser\Table\TableRule::class)]
45#[UsesClass(\SqlParser\Lexer\SourceException::class)]
46#[Small]
47final class LrParserTest extends TestCase
48{
49 public function testParseBuildsATreeNamedAfterTheRules(): void
50 {
51 $builder = new GrammarBuilder();
52 $builder->precedence(['+'], Associativity::Left);
53 $builder->precedence(['*'], Associativity::Left);
54 $builder->terminal('NUM');
55 $builder->rule('expr', ['expr', '+', 'expr']);
56 $builder->rule('expr', ['expr', '*', 'expr']);
57 $builder->rule('expr', ['NUM']);
58 $table = (new ParseTableBuilder())->build($builder->build())->table;
59 $symbols = $table->symbols;
60 $num = $symbols->id('NUM') ?? -1;
61 $sql = '1+2*3';
62 $tokens = [new Token($num, 'NUM', '1', 0), new Token($symbols->id('+') ?? -1, '+', '+', 1), new Token($num, 'NUM', '2', 2), new Token($symbols->id('*') ?? -1, '*', '*', 3), new Token($num, 'NUM', '3', 4), new Token(0, '$end', '', 5)];
63 $tree = (new LrParser($table))->parse($tokens, $sql);
64
65 self::assertSame('expr', $tree->name);
66 self::assertSame(0, $tree->ordinal);
67 self::assertSame('1', $tree->find('expr')[1]->text($sql));
68 self::assertSame('2*3', $tree->find('expr')[2]->text($sql));
69 self::assertSame($sql, $tree->text($sql));
70 }
71
72 public function testParseLeavesMidRuleActionsOutOfTheTree(): void
73 {
74 $builder = new GrammarBuilder();
75 $builder->terminal('A');
76 $builder->rule('$@1', [], null, true);
77 $builder->rule('s', ['A', '$@1', 'A']);
78 $table = (new ParseTableBuilder())->build($builder->build())->table;
79 $a = $table->symbols->id('A') ?? -1;
80 $tree = (new LrParser($table))->parse([new Token($a, 'A', 'a', 0), new Token($a, 'A', 'a', 1), new Token(0, '$end', '', 2)], 'aa');
81
82 self::assertCount(2, $tree->children);
83 self::assertSame('aa', $tree->text('aa'));
84 }
85
86 public function testParseGivesTheTreeTheTextThatFollowsTheLastToken(): void
87 {
88 $builder = new GrammarBuilder();
89 $builder->terminal('NUM');
90 $builder->rule('expr', ['NUM']);
91 $table = (new ParseTableBuilder())->build($builder->build())->table;
92 $sql = " 1 -- done\n";
93 $tokens = [new Token($table->symbols->id('NUM') ?? -1, 'NUM', '1', 1, ' '), new Token(0, '$end', '', 12, " -- done\n")];
94 $tree = (new LrParser($table))->parse($tokens, $sql);
95
96 self::assertSame(" -- done\n", $tree->trailing);
97 self::assertSame($sql, $tree->toString());
98 }
99
100 public function testParseRejectsAnUnexpectedToken(): void
101 {
102 $builder = new GrammarBuilder();
103 $builder->terminal('A');
104 $builder->terminal('B');
105 $builder->rule('s', ['A', 'B']);
106 $table = (new ParseTableBuilder())->build($builder->build())->table;
107 $a = $table->symbols->id('A') ?? -1;
108
109 $this->expectException(SyntaxException::class);
110 $this->expectExceptionMessage("Unexpected 'a' at line 1, column 2, expected B");
111
112 (new LrParser($table))->parse([new Token($a, 'A', 'a', 0), new Token($a, 'A', 'a', 1), new Token(0, '$end', '', 2)], 'aa');
113 }
114
115 public function testParseSuppliesTheEndMarkerWhenTheStreamLacksIt(): void
116 {
117 $builder = new GrammarBuilder();
118 $builder->terminal('A');
119 $builder->rule('s', ['A']);
120 $builder->rule('s', []);
121 $table = (new ParseTableBuilder())->build($builder->build())->table;
122
123 self::assertTrue((new LrParser($table))->parse([], '')->isEmpty());
124 self::assertSame(1, (new LrParser($table))->parse([new Token($table->symbols->id('A') ?? -1, 'A', 'a', 0)], 'a')->children === [] ? 0 : 1);
125 }
126
127 public function testExpected(): void
128 {
129 $builder = new GrammarBuilder();
130 $builder->terminal('A');
131 $builder->terminal('B');
132 $builder->rule('s', ['A']);
133 $builder->rule('s', ['B']);
134 $table = (new ParseTableBuilder())->build($builder->build())->table;
135
136 self::assertSame(['A', 'B'], (new LrParser($table))->expected(0));
137 }
138}
139