packages/lemon-parser/tests/Unit/Preprocessor/PreprocessorTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Preprocessor;
6
7use LemonParser\Ast\Location;
8use LemonParser\Preprocessor\Condition;
9use LemonParser\Preprocessor\Exclusion;
10use LemonParser\Preprocessor\Preprocessor;
11use LemonParser\SyntaxException;
12use PHPUnit\Framework\Attributes\CoversClass;
13use PHPUnit\Framework\Attributes\Small;
14use PHPUnit\Framework\Attributes\UsesClass;
15use PHPUnit\Framework\TestCase;
16
17#[CoversClass(Preprocessor::class)]
18#[UsesClass(Condition::class)]
19#[UsesClass(Exclusion::class)]
20#[UsesClass(Location::class)]
21#[UsesClass(SyntaxException::class)]
22#[Small]
23final class PreprocessorTest extends TestCase
24{
25 public function testPreprocess(): void
26 {
27 $preprocessor = new Preprocessor();
28 $source = "a ::= B.\n%ifdef X\nb ::= C.\n%else\nb ::= D.\n%endif\ne ::= F.\n";
29
30 self::assertSame("a ::= B.\n \n \n \nb ::= D.\n \ne ::= F.\n", $preprocessor->preprocess($source, []));
31 self::assertSame("a ::= B.\n \nb ::= C.\n \n \n \ne ::= F.\n", $preprocessor->preprocess($source, ['X']));
32 }
33
34 public function testPreprocessNestsExcludedRegions(): void
35 {
36 $source = "%ifndef X\n%ifdef Y\na ::= B.\n%else\na ::= C.\n%endif\n%endif\nd ::= E.\n";
37
38 self::assertSame(" \n \n \n \n \n \n \nd ::= E.\n", (new Preprocessor())->preprocess($source, ['X']));
39 self::assertSame(" \n \n \n \na ::= C.\n \n \nd ::= E.\n", (new Preprocessor())->preprocess($source, []));
40 }
41
42 public function testPreprocessCountsDirectivesOnlyAtTheStartOfALine(): void
43 {
44 $source = "a ::= B. %ifdef X\n %endif\n%if X\nc ::= D.\n%endif\n";
45
46 self::assertSame("a ::= B. %ifdef X\n %endif\n \n \n \n", (new Preprocessor())->preprocess($source, []));
47 }
48
49 public function testPreprocessSettlesADirectiveOnTheFirstLineOfAFileWithoutAFinalNewline(): void
50 {
51 self::assertSame(" \n \n ", (new Preprocessor())->preprocess("%ifdef X\na.\n%endif ", []));
52 }
53
54 public function testPreprocessRejectsABadExpression(): void
55 {
56 $this->expectException(SyntaxException::class);
57 $this->expectExceptionMessage('%if syntax error: A B <-- syntax error here at 3:1');
58
59 (new Preprocessor())->preprocess("a.\nb.\n%if A B\nc.\n%endif\n", []);
60 }
61
62 public function testPreprocessRejectsAnUnterminatedRegion(): void
63 {
64 $this->expectException(SyntaxException::class);
65 $this->expectExceptionMessage('unterminated %ifdef starting on line 2 at 2:1');
66
67 (new Preprocessor())->preprocess("a ::= B.\n%ifdef X\nb ::= C.\n", []);
68 }
69
70 public function testDirective(): void
71 {
72 $preprocessor = new Preprocessor();
73
74 self::assertSame('endif', $preprocessor->directive("%endif\n", 0));
75 self::assertSame('else', $preprocessor->directive("x\n%else \n", 2));
76 self::assertSame('ifdef', $preprocessor->directive('%ifdef X', 0));
77 self::assertSame('if', $preprocessor->directive('%if X', 0));
78 self::assertSame('ifndef', $preprocessor->directive('%ifndef X', 0));
79 self::assertNull($preprocessor->directive('%endif', 0));
80 self::assertNull($preprocessor->directive('%endifx', 0));
81 self::assertNull($preprocessor->directive("%if\tX", 0));
82 self::assertNull($preprocessor->directive('%left A.', 0));
83 }
84
85 public function testOpens(): void
86 {
87 $preprocessor = new Preprocessor();
88 $condition = new Condition(['X']);
89
90 self::assertFalse($preprocessor->opens($condition, "%ifdef X\n", 0, 'ifdef', new Location(1, 1)));
91 self::assertTrue($preprocessor->opens($condition, "%ifdef Y\n", 0, 'ifdef', new Location(1, 1)));
92 self::assertTrue($preprocessor->opens($condition, "%ifndef X\n", 0, 'ifndef', new Location(1, 1)));
93 self::assertFalse($preprocessor->opens($condition, '%if X || Y', 0, 'if', new Location(1, 1)));
94 }
95
96 public function testSettle(): void
97 {
98 $preprocessor = new Preprocessor();
99 $condition = new Condition([]);
100 $exclusion = new Exclusion();
101 $source = "%ifdef X\na.\n%else\nb.\n%endif\n";
102
103 $afterIf = $preprocessor->settle($source, 0, 'ifdef', $exclusion, $condition, new Location(1, 1));
104 $depthAfterIf = $exclusion->depth();
105 $afterElse = $preprocessor->settle($afterIf, 12, 'else', $exclusion, $condition, new Location(3, 1));
106 $depthAfterElse = $exclusion->depth();
107 $afterEndif = $preprocessor->settle($afterElse, 21, 'endif', $exclusion, $condition, new Location(5, 1));
108
109 self::assertSame([$source, 1], [$afterIf, $depthAfterIf]);
110 self::assertSame([" \n \n%else\nb.\n%endif\n", 0], [$afterElse, $depthAfterElse]);
111 self::assertSame([$afterElse, 0], [$afterEndif, $exclusion->depth()]);
112 }
113
114 public function testSettleNestsInsideAnExcludedRegion(): void
115 {
116 $preprocessor = new Preprocessor();
117 $condition = new Condition([]);
118 $exclusion = new Exclusion();
119 $exclusion->enter(0, 1);
120
121 $preprocessor->settle("%ifdef X\n%ifdef Y\n%else\n%endif\n", 9, 'ifdef', $exclusion, $condition, new Location(2, 1));
122 $preprocessor->settle("%ifdef X\n%ifdef Y\n%else\n%endif\n", 18, 'else', $exclusion, $condition, new Location(3, 1));
123 $blanked = $preprocessor->settle("%ifdef X\n%ifdef Y\n%else\n%endif\n", 24, 'endif', $exclusion, $condition, new Location(4, 1));
124
125 self::assertSame(1, $exclusion->depth());
126 self::assertSame("%ifdef X\n%ifdef Y\n%else\n%endif\n", $blanked);
127 }
128
129 public function testLineEnd(): void
130 {
131 $preprocessor = new Preprocessor();
132
133 self::assertSame(2, $preprocessor->lineEnd("ab\ncd", 0));
134 self::assertSame(5, $preprocessor->lineEnd("ab\ncd", 3));
135 }
136
137 public function testBlank(): void
138 {
139 self::assertSame("ab \n \n", (new Preprocessor())->blank("abcde\nfg\n", 2, 8));
140 }
141}
142