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