packages/sql-faker/tests/Unit/Compiler/Bison/Lexer/BisonScannerChainTest.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\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Compiler\Bison\Lexer\ActionScanner;
12use SqlFaker\Compiler\Bison\Lexer\BisonScanner;
13use SqlFaker\Compiler\Bison\Lexer\BisonScannerChain;
14use SqlFaker\Compiler\Bison\Lexer\DirectiveScanner;
15use SqlFaker\Compiler\Bison\Lexer\IdentifierScanner;
16use SqlFaker\Compiler\Bison\Lexer\NumberScanner;
17use SqlFaker\Compiler\Bison\Lexer\PunctuationScanner;
18use SqlFaker\Compiler\Bison\Lexer\QuotedLiteralScanner;
19use SqlFaker\Compiler\Bison\Lexer\TypeTagScanner;
20
21#[CoversClass(BisonScannerChain::class)]
22#[UsesClass(ActionScanner::class)]
23#[UsesClass(DirectiveScanner::class)]
24#[UsesClass(IdentifierScanner::class)]
25#[UsesClass(NumberScanner::class)]
26#[UsesClass(PunctuationScanner::class)]
27#[UsesClass(QuotedLiteralScanner::class)]
28#[UsesClass(TypeTagScanner::class)]
29#[UsesClass(\SqlFaker\Compiler\Bison\Lexer\BisonLexeme::class)]
30#[UsesClass(\SqlFaker\Compiler\Bison\Lexer\BisonToken::class)]
31#[UsesClass(\SqlFaker\Compiler\Bison\Lexer\BisonTrivia::class)]
32#[UsesClass(\SqlFaker\Compiler\GrammarParseException::class)]
33#[UsesClass(\SqlFaker\Compiler\Bison\Lexer\SourceCursor::class)]
34final class BisonScannerChainTest extends TestCase
35{
36 /**
37 * @param class-string<BisonScanner> $expected
38 */
39 #[DataProvider('providerClaimedCharacter')]
40 public function testScannerForReachesTheScannerThatOwnsEachOpeningCharacter(string $character, string $expected): void
41 {
42 self::assertInstanceOf($expected, BisonScannerChain::forBisonGrammar()->scannerFor($character));
43 }
44
45 /**
46 * @return iterable<string, array{string, class-string<BisonScanner>}>
47 */
48 public static function providerClaimedCharacter(): iterable
49 {
50 yield 'directive' => ['%', DirectiveScanner::class];
51 yield 'action' => ['{', ActionScanner::class];
52 yield 'type tag' => ['<', TypeTagScanner::class];
53 yield 'character literal' => ["'", QuotedLiteralScanner::class];
54 yield 'string literal' => ['"', QuotedLiteralScanner::class];
55 yield 'number' => ['7', NumberScanner::class];
56 yield 'identifier' => ['x', IdentifierScanner::class];
57 yield 'punctuation' => [':', PunctuationScanner::class];
58 }
59
60 public function testForBisonGrammarCoversEveryLexemeOfTheGrammarLanguage(): void
61 {
62 $chain = BisonScannerChain::forBisonGrammar();
63
64 $unclaimed = array_values(array_filter(
65 ['%', '{', '<', "'", '"', '7', 'x', ':', ';', '|', '=', ',', '(', ')', '[', ']'],
66 static fn (string $character): bool => $chain->scannerFor($character) === null,
67 ));
68
69 self::assertSame([], $unclaimed);
70 }
71
72 public function testScannerForReportsNothingForAnUnclaimedCharacter(): void
73 {
74 self::assertNull(BisonScannerChain::forBisonGrammar()->scannerFor('@'));
75 }
76
77 public function testScannerForFindsNothingInAnEmptyChain(): void
78 {
79 self::assertNull((new BisonScannerChain([]))->scannerFor('x'));
80 }
81
82 public function testScannerForReturnsTheFirstScannerThatClaimsTheCharacter(): void
83 {
84 $chain = new BisonScannerChain([new IdentifierScanner(), new NumberScanner()]);
85
86 self::assertInstanceOf(IdentifierScanner::class, $chain->scannerFor('x'));
87 self::assertInstanceOf(NumberScanner::class, $chain->scannerFor('1'));
88 }
89}
90