packages/bison-parser/tests/Unit/Scanner/EscapesTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Scanner;
6
7use BisonParser\Ast\Location;
8use BisonParser\Scanner\Escapes;
9use BisonParser\SyntaxException;
10use PHPUnit\Framework\Attributes\CoversClass;
11use PHPUnit\Framework\Attributes\Small;
12use PHPUnit\Framework\Attributes\UsesClass;
13use PHPUnit\Framework\TestCase;
14
15#[CoversClass(Escapes::class)]
16#[UsesClass(Location::class)]
17#[UsesClass(SyntaxException::class)]
18#[Small]
19final class EscapesTest extends TestCase
20{
21    public function testDecode(): void
22    {
23        $escapes = new Escapes();
24
25        self::assertSame("a\nb\t\\\"'?", $escapes->decode('a\nb\t\\\\\"\'\?', new Location(1, 1)));
26        self::assertSame("\x07\x08\f\r\v", $escapes->decode('\a\b\f\r\v', new Location(1, 1)));
27        self::assertSame('plain', $escapes->decode('plain', new Location(1, 1)));
28    }
29
30    public function testDecodeRejectsAnUnknownEscape(): void
31    {
32        $this->expectException(SyntaxException::class);
33        $this->expectExceptionMessage('Invalid character after \-escape: q at 2:4');
34
35        (new Escapes())->decode('a\q', new Location(2, 4));
36    }
37
38    public function testDecodeRejectsATrailingBackslash(): void
39    {
40        $this->expectException(SyntaxException::class);
41        $this->expectExceptionMessage('Invalid character after \-escape: end of literal at 2:4');
42
43        (new Escapes())->decode('ab\\', new Location(2, 4));
44    }
45
46    public function testEscape(): void
47    {
48        $escapes = new Escapes();
49
50        self::assertSame(["\x01", 1], $escapes->escape('1', new Location(1, 1)));
51        self::assertSame(["\x0A", 2], $escapes->escape('12x', new Location(1, 1)));
52        self::assertSame(["\xFF", 3], $escapes->escape('3778', new Location(1, 1)));
53        self::assertSame(["\x02", 21], $escapes->escape('x00000000000000000002', new Location(1, 1)));
54        self::assertSame(["\x41", 3], $escapes->escape('x41g', new Location(1, 1)));
55        self::assertSame(["\xE9", 5], $escapes->escape('u00E9', new Location(1, 1)));
56        self::assertSame(["\x7E", 9], $escapes->escape('U0000007E', new Location(1, 1)));
57        self::assertSame(["\xFF", 5], $escapes->escape('u00FF', new Location(1, 1)));
58        self::assertSame(["\n", 1], $escapes->escape('n', new Location(1, 1)));
59        self::assertSame(["\n", 1], $escapes->escape('nx41', new Location(1, 1)));
60        self::assertSame(["\t", 1], $escapes->escape('tu00E9', new Location(1, 1)));
61    }
62
63    public function testEscapeRejectsANumberAboveOneByte(): void
64    {
65        $this->expectException(SyntaxException::class);
66        $this->expectExceptionMessage('Invalid number after \\-escape: \\x100 at 1:1');
67
68        (new Escapes())->escape('x100', new Location(1, 1));
69    }
70
71    public function testByte(): void
72    {
73        $escapes = new Escapes();
74
75        self::assertSame("\xFF", $escapes->byte(255, '\\377', new Location(1, 1)));
76        $this->expectException(SyntaxException::class);
77        $this->expectExceptionMessage('Invalid number after \\-escape: \\777 at 2:3');
78        $escapes->byte(511, '\\777', new Location(2, 3));
79    }
80
81    public function testEscapeRejectsAWideUniversalCharacter(): void
82    {
83        $this->expectException(SyntaxException::class);
84        $this->expectExceptionMessage('Invalid number after \\-escape: \\u3042 at 1:1');
85
86        (new Escapes())->escape('u3042', new Location(1, 1));
87    }
88
89    public function testEncode(): void
90    {
91        $escapes = new Escapes();
92
93        self::assertSame('a\\nb\\t\\r\\\\\\"\'', $escapes->encode("a\nb\t\r\\\"'", '"'));
94        self::assertSame('\\\'"', $escapes->encode("'\"", "'"));
95        self::assertSame('\\001\\177 ~', $escapes->encode("\x01\x7F ~", '"'));
96        self::assertSame("\xC3\xA9", $escapes->encode("\xC3\xA9", '"'));
97    }
98
99    public function testEscapeRejectsANullOctalEscape(): void
100    {
101        $this->expectException(SyntaxException::class);
102        $this->expectExceptionMessage('Invalid number after \\-escape: \\0 at 3:4');
103
104        (new Escapes())->escape('0', new Location(3, 4));
105    }
106
107    public function testEscapeRejectsANullHexadecimalEscape(): void
108    {
109        $this->expectException(SyntaxException::class);
110        $this->expectExceptionMessage('Invalid number after \\-escape: \\x000 at 3:4');
111
112        (new Escapes())->escape('x000', new Location(3, 4));
113    }
114
115    public function testEscapeRejectsANullUniversalCharacter(): void
116    {
117        $this->expectException(SyntaxException::class);
118        $this->expectExceptionMessage('Invalid number after \\-escape: \\u0000 at 3:4');
119
120        (new Escapes())->escape('u0000', new Location(3, 4));
121    }
122
123    public function testEscapeRejectsANullWideUniversalCharacter(): void
124    {
125        $this->expectException(SyntaxException::class);
126        $this->expectExceptionMessage('Invalid number after \\-escape: \\U00000000 at 3:4');
127
128        (new Escapes())->escape('U00000000', new Location(3, 4));
129    }
130
131    public function testByteRejectsZero(): void
132    {
133        $this->expectException(SyntaxException::class);
134        $this->expectExceptionMessage('Invalid number after \\-escape: \\0 at 1:1');
135
136        (new Escapes())->byte(0, '\\0', new Location(1, 1));
137    }
138}
139