packages/bison-parser/tests/Unit/Syntax/GrammarParserTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Syntax;
6
7use BisonParser\Ast\Declaration\Flag;
8use BisonParser\Ast\Declaration\Prologue;
9use BisonParser\Ast\Declaration\Start;
10use BisonParser\Ast\Epilogue;
11use BisonParser\Ast\GrammarFile;
12use BisonParser\Ast\Line;
13use BisonParser\Ast\Location;
14use BisonParser\Ast\Rule\Alternative;
15use BisonParser\Ast\Rule\Rule;
16use BisonParser\Ast\Rule\SymbolItem;
17use BisonParser\Ast\Symbol;
18use BisonParser\Ast\SymbolKind;
19use BisonParser\Scanner\CodeReader;
20use BisonParser\Scanner\Cursor;
21use BisonParser\Scanner\Directives;
22use BisonParser\Scanner\Escapes;
23use BisonParser\Scanner\Scanner;
24use BisonParser\Scanner\Token;
25use BisonParser\Scanner\TokenKind;
26use BisonParser\Syntax\DeclarationParser;
27use BisonParser\Syntax\GrammarParser;
28use BisonParser\Syntax\RuleParser;
29use BisonParser\Syntax\SymbolListParser;
30use BisonParser\Syntax\TokenStream;
31use BisonParser\SyntaxException;
32use PHPUnit\Framework\Attributes\CoversClass;
33use PHPUnit\Framework\Attributes\Small;
34use PHPUnit\Framework\Attributes\UsesClass;
35use PHPUnit\Framework\TestCase;
36
37#[CoversClass(GrammarParser::class)]
38#[UsesClass(Alternative::class)]
39#[UsesClass(CodeReader::class)]
40#[UsesClass(Cursor::class)]
41#[UsesClass(DeclarationParser::class)]
42#[UsesClass(Directives::class)]
43#[UsesClass(Epilogue::class)]
44#[UsesClass(Escapes::class)]
45#[UsesClass(Flag::class)]
46#[UsesClass(GrammarFile::class)]
47#[UsesClass(Line::class)]
48#[UsesClass(Location::class)]
49#[UsesClass(Prologue::class)]
50#[UsesClass(Rule::class)]
51#[UsesClass(RuleParser::class)]
52#[UsesClass(Scanner::class)]
53#[UsesClass(Start::class)]
54#[UsesClass(Symbol::class)]
55#[UsesClass(SymbolItem::class)]
56#[UsesClass(SymbolKind::class)]
57#[UsesClass(SymbolListParser::class)]
58#[UsesClass(SyntaxException::class)]
59#[UsesClass(Token::class)]
60#[UsesClass(TokenKind::class)]
61#[UsesClass(TokenStream::class)]
62#[Small]
63final class GrammarParserTest extends TestCase
64{
65 public function testParse(): void
66 {
67 $file = (new GrammarParser())->parse(new TokenStream((new Scanner())->scan("%{ int x; %}\n%debug;\n%%\n;\na: b;\n%start a;\nb: ;\n%%\nint main() {}\n")));
68
69 self::assertSame([Prologue::class, Flag::class], array_map(static fn (object $declaration): string => $declaration::class, $file->declarations));
70 self::assertSame([Rule::class, Start::class, Rule::class], array_map(static fn (object $item): string => $item::class, $file->grammar));
71 self::assertSame("\nint main() {}\n", $file->epilogue?->code);
72 }
73
74 public function testParseKeepsLineDirectivesAmongTheRules(): void
75 {
76 $file = (new GrammarParser())->parse(new TokenStream((new Scanner())->scan("#line 3 \"a.y\"\n%debug\n%%\n#line 9\na: ;\n")));
77
78 self::assertSame([Line::class, Flag::class], array_map(static fn (object $declaration): string => $declaration::class, $file->declarations));
79 self::assertSame([Line::class, Rule::class], array_map(static fn (object $item): string => $item::class, $file->grammar));
80 }
81
82 public function testParseWithoutAnEpilogue(): void
83 {
84 $file = (new GrammarParser())->parse(new TokenStream((new Scanner())->scan('%% a: ; %%')));
85
86 self::assertCount(1, $file->rules());
87 self::assertSame('', $file->epilogue?->code);
88 self::assertNull((new GrammarParser())->parse(new TokenStream((new Scanner())->scan('%% a: ;')))->epilogue);
89 }
90
91 public function testParseRejectsARuleBeforeTheSection(): void
92 {
93 $this->expectException(SyntaxException::class);
94 $this->expectExceptionMessage("Expected a declaration or '%%' but found 'a' at 1:1");
95
96 (new GrammarParser())->parse(new TokenStream((new Scanner())->scan('a: ;')));
97 }
98
99 public function testParseRejectsAStrayTokenAmongTheRules(): void
100 {
101 $this->expectException(SyntaxException::class);
102 $this->expectExceptionMessage("Expected a rule or ';' but found '|' at 1:4");
103
104 (new GrammarParser())->parse(new TokenStream((new Scanner())->scan('%% | a: ;')));
105 }
106
107 public function testParseRejectsADeclarationAmongTheRulesWithoutASemicolon(): void
108 {
109 $this->expectException(SyntaxException::class);
110 $this->expectExceptionMessage("Expected ';' after a declaration among the rules but found 'a' at 1:15");
111
112 (new GrammarParser())->parse(new TokenStream((new Scanner())->scan('%% %start a a: ;')));
113 }
114
115 public function testParseRejectsARulesSectionWithoutRules(): void
116 {
117 $this->expectException(SyntaxException::class);
118 $this->expectExceptionMessage('Expected a rule but found end of file at 3:1');
119
120 (new GrammarParser())->parse(new TokenStream((new Scanner())->scan("%debug\n%%\n")));
121 }
122
123 public function testParseRejectsARulesSectionHoldingOnlyDeclarations(): void
124 {
125 $this->expectException(SyntaxException::class);
126 $this->expectExceptionMessage("Expected a rule but found '%%' at 1:13");
127
128 (new GrammarParser())->parse(new TokenStream((new Scanner())->scan('%% %start a;%%')));
129 }
130}
131