packages/bison-parser/tests/Unit/Scanner/CodeReaderTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Scanner;
6
7use BisonParser\Ast\Location;
8use BisonParser\Scanner\CodeReader;
9use BisonParser\Scanner\Cursor;
10use BisonParser\SyntaxException;
11use PHPUnit\Framework\Attributes\CoversClass;
12use PHPUnit\Framework\Attributes\Small;
13use PHPUnit\Framework\Attributes\UsesClass;
14use PHPUnit\Framework\TestCase;
15
16#[CoversClass(CodeReader::class)]
17#[UsesClass(Cursor::class)]
18#[UsesClass(Location::class)]
19#[UsesClass(SyntaxException::class)]
20#[Small]
21final class CodeReaderTest extends TestCase
22{
23 public function testBraced(): void
24 {
25 $reader = new CodeReader();
26 $cursor = new Cursor('{ if (a) { b("}"); } /* } */ <% c %> } tail');
27
28 self::assertSame(' if (a) { b("}"); } /* } */ <% c %> ', $reader->braced($cursor));
29 self::assertSame(' tail', $cursor->take(5));
30 }
31
32 public function testBracedNeverClosesOnADigraph(): void
33 {
34 $this->expectException(SyntaxException::class);
35 $this->expectExceptionMessage("Missing '}' closing the braced code opened at 1:1");
36
37 (new CodeReader())->braced(new Cursor('{ x %> y } z'));
38 }
39
40 public function testBracedClosesOnTheBraceThatBalancesADigraph(): void
41 {
42 $cursor = new Cursor('{ x %> { y } } z');
43
44 self::assertSame(' x %> { y ', (new CodeReader())->braced($cursor));
45 self::assertSame(' } z', $cursor->take(4));
46 }
47
48 public function testBracedSplicesBackslashNewlines(): void
49 {
50 $cursor = new Cursor("{ a /\\\n* } *\\\n\\\n/ b <\\\n% x %\\\n> c <% } } tail");
51
52 self::assertSame(" a /\\\n* } *\\\n\\\n/ b <\\\n% x %\\\n> c <% } ", (new CodeReader())->braced($cursor));
53 self::assertSame(' tail', $cursor->take(5));
54 }
55
56 public function testBracedRejectsAnUnterminatedBlock(): void
57 {
58 $this->expectException(SyntaxException::class);
59 $this->expectExceptionMessage("Missing '}' closing the braced code opened at 1:1");
60
61 (new CodeReader())->braced(new Cursor("{ a { b }\n"));
62 }
63
64 public function testPrologue(): void
65 {
66 $cursor = new Cursor("%{\n#define X \"%}\" // %}\n%} rest");
67
68 self::assertSame("\n#define X \"%}\" // %}\n", (new CodeReader())->prologue($cursor));
69 self::assertSame(' rest', $cursor->take(5));
70 }
71
72 public function testPrologueRejectsAnUnterminatedBlock(): void
73 {
74 $this->expectException(SyntaxException::class);
75 $this->expectExceptionMessage("Missing '%}' closing the prologue opened at 1:1");
76
77 (new CodeReader())->prologue(new Cursor("%{ int x;\n"));
78 }
79
80 public function testUnit(): void
81 {
82 $reader = new CodeReader();
83
84 self::assertSame('/* } */', $reader->unit(new Cursor('/* } */ x')));
85 self::assertSame('// } to the end', $reader->unit(new Cursor("// } to the end\n}")));
86 self::assertSame('"a\"}"', $reader->unit(new Cursor('"a\"}" tail')));
87 self::assertSame("'}'", $reader->unit(new Cursor("'}' tail")));
88 self::assertSame('<%', $reader->unit(new Cursor('<% x')));
89 self::assertSame('%>', $reader->unit(new Cursor('%> x')));
90 self::assertSame('<<', $reader->unit(new Cursor('<<% x')));
91 self::assertSame("<\\\n%", $reader->unit(new Cursor("<\\\n% x")));
92 self::assertSame("// a \\\n still the comment", $reader->unit(new Cursor("// a \\\n still the comment\n}")));
93 self::assertSame('"a\\[b"', $reader->unit(new Cursor('"a\\[b" tail')));
94 self::assertSame("\"a\\\n\"", $reader->unit(new Cursor("\"a\\\n\"b\" tail")));
95 self::assertSame("\"a\\\\\n\"b\"", $reader->unit(new Cursor("\"a\\\\\n\"b\" tail")));
96 self::assertSame('a', $reader->unit(new Cursor('abc')));
97 }
98
99 public function testOpens(): void
100 {
101 $reader = new CodeReader();
102
103 self::assertTrue($reader->opens('{'));
104 self::assertTrue($reader->opens('<%'));
105 self::assertTrue($reader->opens("<\\\n%"));
106 self::assertFalse($reader->opens('}'));
107 self::assertFalse($reader->opens('<<'));
108 }
109
110 public function testCloses(): void
111 {
112 $reader = new CodeReader();
113
114 self::assertTrue($reader->closes('}'));
115 self::assertTrue($reader->closes('%>'));
116 self::assertTrue($reader->closes("%\\\n>"));
117 self::assertFalse($reader->closes('{'));
118 }
119
120 public function testUnitRejectsAnUnterminatedComment(): void
121 {
122 $this->expectException(SyntaxException::class);
123 $this->expectExceptionMessage("Missing '*/' closing the comment opened at 1:1");
124
125 (new CodeReader())->unit(new Cursor('/* never closed'));
126 }
127
128 public function testUnitRejectsAnUnterminatedString(): void
129 {
130 $this->expectException(SyntaxException::class);
131 $this->expectExceptionMessage("Missing '\"' closing the string opened at 1:1");
132
133 (new CodeReader())->unit(new Cursor("\"broken\n\""));
134 }
135
136 public function testUnitRejectsAnUnterminatedCharacterLiteral(): void
137 {
138 $this->expectException(SyntaxException::class);
139 $this->expectExceptionMessage("Missing ''' closing the character literal opened at 1:1");
140
141 (new CodeReader())->unit(new Cursor("'"));
142 }
143}
144