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