packages/sql-faker/tests/Unit/Compiler/Bison/Lexer/BisonLexerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Compiler\Bison\Lexer;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Compiler\Bison\Lexer\ActionScanner;
11use SqlFaker\Compiler\Bison\Lexer\BisonLexeme;
12use SqlFaker\Compiler\Bison\Lexer\BisonLexer;
13use SqlFaker\Compiler\Bison\Lexer\BisonScannerChain;
14use SqlFaker\Compiler\Bison\Lexer\BisonToken;
15use SqlFaker\Compiler\Bison\Lexer\BisonTrivia;
16use SqlFaker\Compiler\Bison\Lexer\DirectiveScanner;
17use SqlFaker\Compiler\Bison\Lexer\IdentifierScanner;
18use SqlFaker\Compiler\Bison\Lexer\NumberScanner;
19use SqlFaker\Compiler\Bison\Lexer\PunctuationScanner;
20use SqlFaker\Compiler\Bison\Lexer\QuotedLiteralScanner;
21use SqlFaker\Compiler\Bison\Lexer\SourceCursor;
22use SqlFaker\Compiler\Bison\Lexer\TypeTagScanner;
23use SqlFaker\Compiler\GrammarParseException;
24
25#[CoversClass(BisonLexer::class)]
26#[UsesClass(ActionScanner::class)]
27#[UsesClass(BisonLexeme::class)]
28#[UsesClass(BisonScannerChain::class)]
29#[UsesClass(BisonToken::class)]
30#[UsesClass(BisonTrivia::class)]
31#[UsesClass(DirectiveScanner::class)]
32#[UsesClass(GrammarParseException::class)]
33#[UsesClass(IdentifierScanner::class)]
34#[UsesClass(NumberScanner::class)]
35#[UsesClass(PunctuationScanner::class)]
36#[UsesClass(QuotedLiteralScanner::class)]
37#[UsesClass(SourceCursor::class)]
38#[UsesClass(TypeTagScanner::class)]
39final class BisonLexerTest extends TestCase
40{
41 public function testScanReadsAnEmptySourceAsEndOfFile(): void
42 {
43 $token = (new BisonLexer(''))->scan();
44
45 self::assertSame(BisonLexeme::Eof, $token->type);
46 self::assertSame('', $token->value);
47 self::assertSame(0, $token->offset);
48 }
49
50 public function testScanKeepsReportingEndOfFile(): void
51 {
52 $lexer = new BisonLexer('a');
53
54 $lexer->scan();
55
56 self::assertSame(BisonLexeme::Eof, $lexer->scan()->type);
57 self::assertSame(BisonLexeme::Eof, $lexer->scan()->type);
58 }
59
60 public function testScanExcludesLeadingWhitespaceFromTheToken(): void
61 {
62 $token = (new BisonLexer(" \n foo"))->scan();
63
64 self::assertSame(BisonLexeme::Identifier, $token->type);
65 self::assertSame(4, $token->offset);
66 }
67
68 public function testScanSkipsRunsOfMixedTrivia(): void
69 {
70 $token = (new BisonLexer('/* a */ // b' . "\n" . '/* c */ foo'))->scan();
71
72 self::assertSame(BisonLexeme::Identifier, $token->type);
73 self::assertSame('foo', $token->value);
74 }
75
76 public function testScanReadsTrailingTriviaAsEndOfFile(): void
77 {
78 self::assertSame(BisonLexeme::Eof, (new BisonLexer('/* only a comment */'))->scan()->type);
79 }
80
81 public function testScanAdvancesToTheNextTokenEachTime(): void
82 {
83 $lexer = new BisonLexer('a : b ;');
84
85 $lexemes = [
86 $lexer->scan()->type,
87 $lexer->scan()->type,
88 $lexer->scan()->type,
89 $lexer->scan()->type,
90 $lexer->scan()->type,
91 ];
92
93 self::assertSame(
94 [
95 BisonLexeme::Identifier,
96 BisonLexeme::Colon,
97 BisonLexeme::Identifier,
98 BisonLexeme::Semicolon,
99 BisonLexeme::Eof,
100 ],
101 $lexemes,
102 );
103 }
104
105 public function testScanReportsACharacterNoScannerClaims(): void
106 {
107 $lexer = new BisonLexer(' @');
108
109 $this->expectException(GrammarParseException::class);
110 $this->expectExceptionMessage("Unexpected character '@' at offset 2");
111
112 $lexer->scan();
113 }
114
115 public function testScanUsesTheChainItWasGiven(): void
116 {
117 $lexer = new BisonLexer('foo', new BisonScannerChain([new IdentifierScanner()]));
118
119 self::assertSame('foo', $lexer->scan()->value);
120 }
121
122 public function testScanRejectsWhatTheGivenChainDoesNotCover(): void
123 {
124 $lexer = new BisonLexer('123', new BisonScannerChain([new IdentifierScanner()]));
125
126 $this->expectException(GrammarParseException::class);
127 $this->expectExceptionMessage("Unexpected character '1' at offset 0");
128
129 $lexer->scan();
130 }
131
132 public function testConsumeRemainingTakesTheRestWithoutScanningIt(): void
133 {
134 $lexer = new BisonLexer('foo @ % <');
135
136 $lexer->scan();
137
138 self::assertSame(' @ % <', $lexer->consumeRemaining());
139 }
140
141 public function testConsumeRemainingIsEmptyOnceTheSourceRunsOut(): void
142 {
143 $lexer = new BisonLexer('foo');
144
145 $lexer->scan();
146
147 self::assertSame('', $lexer->consumeRemaining());
148 }
149}
150