packages/sql-faker/tests/Unit/Generation/Token/TerminalSequenceTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Token;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Generation\Candidate\ChoiceLexemeGenerator;
11use SqlFaker\Generation\Candidate\FixedLexemeGenerator;
12use SqlFaker\Generation\Candidate\MatchingLexemeGenerator;
13use SqlFaker\Generation\Candidate\RegisteredLexemeGenerator;
14use SqlFaker\Generation\Candidate\SequenceLexemeGenerator;
15use SqlFaker\Generation\Candidate\ValueLexemeGenerator;
16use SqlFaker\Generation\Candidate\VersionCase;
17use SqlFaker\Generation\Candidate\VersionedLexemeGenerator;
18use SqlFaker\Generation\Derivation\CompletionCosts;
19use SqlFaker\Generation\Derivation\Derivation;
20use SqlFaker\Generation\Derivation\DerivationTrace;
21use SqlFaker\Generation\Derivation\TerminationAnalyzer;
22use SqlFaker\Generation\Derivation\TerminationCost;
23use SqlFaker\Generation\Derivation\TokenGenerator;
24use SqlFaker\Generation\Exception\GenerationException;
25use SqlFaker\Generation\Exception\LexicalException;
26use SqlFaker\Generation\Lexeme\Lexeme;
27use SqlFaker\Generation\Lexeme\LexemeBoundary;
28use SqlFaker\Generation\Lexeme\LexemeCandidates;
29use SqlFaker\Generation\Lexeme\LexemeGenerator;
30use SqlFaker\Generation\Lexeme\LexemeInput;
31use SqlFaker\Generation\Lexeme\LexemeSequence;
32use SqlFaker\Generation\Lexeme\OutputPart;
33use SqlFaker\Generation\Lexeme\ResolvedOutput;
34use SqlFaker\Generation\Lexeme\SpacingConstraint;
35use SqlFaker\Generation\Lexeme\SpacingRule;
36use SqlFaker\Generation\Output\CandidateResolver;
37use SqlFaker\Generation\Output\CombinedSpacingRule;
38use SqlFaker\Generation\Output\ReverseLexemeGenerator;
39use SqlFaker\Generation\Output\SqlSerializer;
40use SqlFaker\Generation\Plan\GenerationPlan;
41use SqlFaker\Generation\Token\ProductionOccurrence;
42use SqlFaker\Generation\Token\RewriteRule;
43use SqlFaker\Generation\Token\TerminalOccurrence;
44use SqlFaker\Generation\Token\TerminalSequence;
45use SqlFaker\Generation\Token\TokenRewriter;
46use SqlFaker\Grammar\Model\Grammar;
47use SqlFaker\Grammar\Model\NonTerminal;
48use SqlFaker\Grammar\Model\Production;
49use SqlFaker\Grammar\Model\ProductionRule;
50use SqlFaker\Grammar\Model\Terminal;
51use SqlFaker\MySql\Generation\Spacing\KeywordPhraseSpacingRule;
52
53#[CoversClass(TerminalSequence::class)]
54#[UsesClass(RewriteRule::class)]
55#[UsesClass(TokenGenerator::class)]
56#[UsesClass(ProductionOccurrence::class)]
57#[UsesClass(TokenRewriter::class)]
58#[UsesClass(TerminalOccurrence::class)]
59#[UsesClass(CandidateResolver::class)]
60#[UsesClass(ReverseLexemeGenerator::class)]
61#[UsesClass(ResolvedOutput::class)]
62#[UsesClass(OutputPart::class)]
63#[UsesClass(SqlSerializer::class)]
64#[UsesClass(VersionCase::class)]
65#[UsesClass(VersionedLexemeGenerator::class)]
66#[UsesClass(KeywordPhraseSpacingRule::class)]
67#[UsesClass(CombinedSpacingRule::class)]
68#[UsesClass(LexemeBoundary::class)]
69#[UsesClass(SpacingRule::class)]
70#[UsesClass(SpacingConstraint::class)]
71#[UsesClass(Lexeme::class)]
72#[UsesClass(LexemeGenerator::class)]
73#[UsesClass(ValueLexemeGenerator::class)]
74#[UsesClass(LexemeSequence::class)]
75#[UsesClass(RegisteredLexemeGenerator::class)]
76#[UsesClass(LexemeCandidates::class)]
77#[UsesClass(SequenceLexemeGenerator::class)]
78#[UsesClass(ChoiceLexemeGenerator::class)]
79#[UsesClass(FixedLexemeGenerator::class)]
80#[UsesClass(MatchingLexemeGenerator::class)]
81#[UsesClass(LexemeInput::class)]
82#[UsesClass(CompletionCosts::class)]
83#[UsesClass(Derivation::class)]
84#[UsesClass(DerivationTrace::class)]
85#[UsesClass(GenerationPlan::class)]
86#[UsesClass(TerminationAnalyzer::class)]
87#[UsesClass(TerminationCost::class)]
88#[UsesClass(Grammar::class)]
89#[UsesClass(NonTerminal::class)]
90#[UsesClass(Production::class)]
91#[UsesClass(ProductionRule::class)]
92#[UsesClass(Terminal::class)]
93#[UsesClass(GenerationException::class)]
94#[UsesClass(LexicalException::class)]
95#[UsesClass(\SqlFaker\Generation\Plan\ProductionPattern::class)]
96#[UsesClass(\SqlFaker\Generation\Output\BoundaryCompletion::class)]
97#[UsesClass(\SqlFaker\Generation\Derivation\CompletionState::class)]
98#[UsesClass(\SqlFaker\Generation\Derivation\CompletionFrontier::class)]
99#[UsesClass(\SqlFaker\Generation\Derivation\ConstrainedCompletion::class)]
100#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
101#[UsesClass(\SqlFaker\Generation\Derivation\CompletionMemo::class)]
102#[UsesClass(\SqlFaker\Generation\Derivation\CompletionReduction::class)]
103#[UsesClass(\SqlFaker\Generation\Derivation\ConstraintDependencies::class)]
104#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
105#[UsesClass(\SqlFaker\Generation\Derivation\Completion\PatternProductions::class)]
106#[UsesClass(\SqlFaker\Generation\Derivation\Completion\CompletionWitness::class)]
107final class TerminalSequenceTest extends TestCase
108{
109    public function testFromNamesPreservesOccurrenceIdentityForRepeatedTerminalNames(): void
110    {
111        $sequence = TerminalSequence::fromNames(['IDENT', '.', 'IDENT']);
112        self::assertSame([0, 1, 2], array_map(static fn (TerminalOccurrence $terminal): int => $terminal->id, $sequence->terminals));
113        self::assertSame($sequence->terminals, $sequence->original);
114        self::assertSame([], $sequence->rewrites);
115    }
116
117    public function testNamesAndNameAtReadCurrentOutputAndHandleItsEdges(): void
118    {
119        $sequence = TerminalSequence::fromNames(['A', 'B']);
120        self::assertSame(['A', 'B'], $sequence->names());
121        self::assertSame('B', $sequence->nameAt(1));
122        self::assertNull($sequence->nameAt(-1));
123        self::assertNull($sequence->nameAt(2));
124    }
125
126    public function testNameAtHandlesAnEmptySequence(): void
127    {
128        self::assertNull(TerminalSequence::fromNames([])->nameAt(0));
129    }
130
131    public function testReplaceRetainsOriginalDerivationAndRecordsTheRule(): void
132    {
133        $sequence = TerminalSequence::fromNames(['A', 'B']);
134        $replacement = $sequence->terminals[1]->replaced('C', 'rewrite');
135        $result = $sequence->replace(1, 1, [$replacement], 'rewrite');
136        self::assertSame(['A', 'C'], $result->names());
137        self::assertSame(['A', 'B'], $sequence->names());
138        self::assertSame($sequence->original, $result->original);
139        self::assertSame(['rewrite'], $result->rewrites);
140        self::assertSame(1, $result->terminals[1]->id);
141    }
142
143    public function testRangeAndOccurrencesRetainEmptyAndNestedProductions(): void
144    {
145        $trace = new DerivationTrace('root');
146        $trace->expand(0, new Production([new NonTerminal('child'), new NonTerminal('child')]), 0);
147        $trace->expand(0, new Production([]), 1);
148        $trace->expand(0, new Production([new Terminal('X')]), 2);
149        $sequence = $trace->terminals();
150        self::assertSame([2, 1], $sequence->occurrences('child'));
151        self::assertSame([0, 1], $sequence->range(0));
152        self::assertSame([0, 1], $sequence->range(2));
153        self::assertNull($sequence->range(1));
154        self::assertSame([], $sequence->occurrences('absent'));
155    }
156
157    public function testOccurrencesAfterRewritingStillDescribeTheOriginalSelection(): void
158    {
159        $trace = new DerivationTrace('root');
160        $trace->expand(0, new Production([new Terminal('X')]), 3);
161        $sequence = $trace->terminals()->replace(0, 1, [], 'remove');
162        self::assertSame([0], $sequence->occurrences('root'));
163        self::assertNull($sequence->range(0));
164        self::assertCount(1, $sequence->original);
165        self::assertSame(3, $sequence->productions[0]->ordinal);
166    }
167
168    public function testInsertedAllocatesDistinctIdsOutsideTheOriginalDerivation(): void
169    {
170        $sequence = TerminalSequence::fromNames(['X']);
171        $first = $sequence->inserted('A', $sequence->terminals[0], 'insert');
172        $second = $sequence->inserted('B', $sequence->terminals[0], 'insert', 1);
173        $rewritten = $sequence->replace(0, 0, [$first, $second], 'insert');
174        $third = $rewritten->inserted('C', $sequence->terminals[0], 'insert');
175        self::assertSame([-1, -2, -3], [$first->id, $second->id, $third->id]);
176        self::assertSame('insert', $third->rewrite);
177    }
178    public function testChildFindsEmptyDirectChildrenWithoutConfusingNestedNames(): void
179    {
180        $sequence = new TerminalSequence([], [], [], [
181            new ProductionOccurrence(0, null, 'root', 0),
182            new ProductionOccurrence(1, 0, 'child', 0),
183            new ProductionOccurrence(2, 1, 'child', 1),
184        ]);
185        self::assertSame(1, $sequence->child(0, 'child')?->id);
186        self::assertSame(2, $sequence->child(1, 'child')?->id);
187        self::assertNull($sequence->child(0, 'missing'));
188    }
189
190    public function testInsertedForAttachesOutputToAnOriginallyEmptyProduction(): void
191    {
192        $sequence = new TerminalSequence([], [], [], [
193            new ProductionOccurrence(0, null, 'root', 0),
194            new ProductionOccurrence(1, 0, 'optional', 0),
195        ]);
196        $terminal = $sequence->insertedFor('VALUE', 1, 'complete');
197        self::assertSame([0, 1], $terminal->ancestors);
198        self::assertSame(['root', 'optional'], $terminal->rules);
199        self::assertSame(-1, $terminal->id);
200        $output = $sequence->replace(0, 0, [$terminal], 'complete');
201        self::assertSame([0, 1], $output->range(1));
202        self::assertSame([], $output->original);
203        self::assertSame($sequence->productions, $output->productions);
204    }
205
206    public function testRangeIncludesAllRetainedTerminalsAndReplacementPreservesEarlierRewrites(): void
207    {
208        $prefix = new TerminalOccurrence('PREFIX', 1);
209        $first = new TerminalOccurrence('A', 2, [0], ['root']);
210        $second = new TerminalOccurrence('B', 3, [0], ['root']);
211        $tail = new TerminalOccurrence('TAIL', 4);
212        $input = new TerminalSequence([$prefix, $first, $second, $tail], [], ['previous']);
213        self::assertSame([1, 3], $input->range(0));
214        $result = $input->replace(1, 1, [], 'remove');
215        self::assertSame([1, 2], $result->range(0));
216        self::assertSame(['previous', 'remove'], $result->rewrites);
217        self::assertSame(['previous'], $input->rewrites);
218        self::assertSame([$prefix, $second, $tail], $result->terminals);
219    }
220    public function testReplaceRetainsEachIntermediateOperation(): void
221    {
222        $input = TerminalSequence::fromNames(['A', 'B']);
223        $middle = $input->terminals[0]->replaced('C', 'first');
224        $first = $input->replace(0, 1, [$middle], 'first');
225        $result = $first->replace(0, 1, [], 'second');
226        self::assertSame([
227            ['rule' => 'first', 'offset' => 0, 'removed' => [$input->terminals[0]], 'inserted' => [$middle]],
228            ['rule' => 'second', 'offset' => 0, 'removed' => [$middle], 'inserted' => []],
229        ], $result->operations);
230        self::assertSame([], $input->operations);
231    }
232}
233