packages/sql-parser/tests/Unit/PostgreSql/Lexer/NumberScannerTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\PostgreSql\Lexer;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Small;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlParser\Lexer\Cursor;
12use SqlParser\Lexer\Lexeme;
13use SqlParser\Lexer\LexicalException;
14use SqlParser\PostgreSql\Lexer\KeywordTable;
15use SqlParser\PostgreSql\Lexer\NumberScanner;
16use SqlParser\PostgreSql\Lexer\Scan;
17
18#[CoversClass(NumberScanner::class)]
19#[UsesClass(Cursor::class)]
20#[UsesClass(Lexeme::class)]
21#[UsesClass(LexicalException::class)]
22#[UsesClass(KeywordTable::class)]
23#[UsesClass(Scan::class)]
24#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
25#[UsesClass(\SqlParser\Lexer\SourceException::class)]
26#[Small]
27final class NumberScannerTest extends TestCase
28{
29    public function testScan(): void
30    {
31        $scanner = new NumberScanner();
32        $scan = static fn (string $sql): Scan => new Scan(new Cursor($sql), new KeywordTable([]));
33
34        self::assertSame('ICONST', $scanner->scan($scan('42 '))?->name);
35        self::assertSame('FCONST', $scanner->scan($scan('2147483648'))?->name);
36        self::assertSame('ICONST', $scanner->scan($scan('1_000'))?->name);
37        self::assertSame('ICONST', $scanner->scan($scan('0x1F'))?->name);
38        self::assertSame('FCONST', $scanner->scan($scan('0o777_7777_7777'))?->name);
39        self::assertSame('FCONST', $scanner->scan($scan('1.5'))?->name);
40        self::assertSame('1.', $scanner->scan($scan('1. '))?->text);
41        self::assertSame('.5', $scanner->scan($scan('.5'))?->text);
42        self::assertSame('FCONST', $scanner->scan($scan('1e5'))?->name);
43        self::assertSame('1', $scanner->scan($scan('1..2'))?->text);
44        self::assertSame('PARAM', $scanner->scan($scan('$12 '))?->name);
45        self::assertNull($scanner->scan($scan('abc')));
46        self::assertNull($scanner->scan($scan('.')));
47    }
48
49    public function testScanRejectsTrailingJunk(): void
50    {
51        $this->expectException(LexicalException::class);
52
53        (new NumberScanner())->scan(new Scan(new Cursor('123abc'), new KeywordTable([])));
54    }
55
56    public function testScanRejectsAnIncompleteExponent(): void
57    {
58        $this->expectException(LexicalException::class);
59
60        (new NumberScanner())->scan(new Scan(new Cursor('1e+'), new KeywordTable([])));
61    }
62
63    public function testScanRejectsAnEmptyHexadecimalLiteral(): void
64    {
65        $this->expectException(LexicalException::class);
66
67        (new NumberScanner())->scan(new Scan(new Cursor('0x_'), new KeywordTable([])));
68    }
69
70    public function testRejectJunk(): void
71    {
72        $scanner = new NumberScanner();
73        $scanner->rejectJunk(new Scan(new Cursor(' '), new KeywordTable([])), 0);
74
75        $this->expectException(LexicalException::class);
76
77        $scanner->rejectJunk(new Scan(new Cursor('x'), new KeywordTable([])), 0);
78    }
79
80    public function testFitsInt32(): void
81    {
82        self::assertTrue(NumberScanner::fitsInt32('2147483647'));
83        self::assertFalse(NumberScanner::fitsInt32('2147483648'));
84        self::assertTrue(NumberScanner::fitsInt32('0x7fff_ffff'));
85        self::assertFalse(NumberScanner::fitsInt32('0x80000000'));
86        self::assertTrue(NumberScanner::fitsInt32('0o17777777777'));
87        self::assertFalse(NumberScanner::fitsInt32('0o20000000000'));
88        self::assertTrue(NumberScanner::fitsInt32('0b' . str_repeat('1', 31)));
89        self::assertFalse(NumberScanner::fitsInt32('0b1' . str_repeat('0', 31)));
90        self::assertTrue(NumberScanner::fitsInt32('0000'));
91    }
92}
93