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