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