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