packages/sql-faker/tests/Unit/Generation/Output/CandidateResolverTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Output;
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(CandidateResolver::class)]
54#[UsesClass(RewriteRule::class)]
55#[UsesClass(TokenGenerator::class)]
56#[UsesClass(ProductionOccurrence::class)]
57#[UsesClass(TerminalSequence::class)]
58#[UsesClass(TokenRewriter::class)]
59#[UsesClass(TerminalOccurrence::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 CandidateResolverTest extends TestCase
108{
109 public function testResolvePropagatesConditionsAcrossAnEmptyMarkerWithoutWritingSpaces(): void
110 {
111 $terminal = new TerminalOccurrence('NAME', 2);
112 $right = new ResolvedOutput([new OutputPart(new Lexeme('name', 'identifier', $terminal, 'name'), '', 'name', [])], new SpacingConstraint(SpacingConstraint::JOIN, ['variable']));
113 $resolver = new CandidateResolver(new CombinedSpacingRule());
114 $input = new LexemeInput(TerminalSequence::fromNames(['@', 'EOF', 'NAME']), 1, $right);
115 $output = $resolver->resolve(new LexemeSequence([], 'eof'), $input);
116 self::assertInstanceOf(ResolvedOutput::class, $output);
117 self::assertSame($right->parts, $output->parts);
118 self::assertSame($right->left, $output->left);
119 $joined = $resolver->resolve(new LexemeSequence([new Lexeme('@', 'symbol', new TerminalOccurrence('@', 0), 'at')], 'at'), new LexemeInput($input->terminals, 0, $output));
120 self::assertInstanceOf(ResolvedOutput::class, $joined);
121 self::assertSame('@name', (new SqlSerializer())->serialize($joined->pieces()));
122 self::assertSame(' ', (new SpacingConstraint())->separator());
123 }
124
125 public function testResolveIntersectsInternalAndExternalConstraintsWithoutChangingTheRightState(): void
126 {
127 $terminals = TerminalSequence::fromNames(['PHRASE', ')']);
128 $right = new ResolvedOutput([new OutputPart(new Lexeme(')', 'symbol', $terminals->terminals[1], 'close'), '', 'close', [])]);
129 $candidate = new LexemeSequence([
130 new Lexeme('A', 'keyword', $terminals->terminals[0], 'a'),
131 new Lexeme('B', 'keyword', $terminals->terminals[0], 'b'),
132 ], 'phrase', null, [1 => new SpacingConstraint(SpacingConstraint::SPACE, ['internal']), 2 => new SpacingConstraint(SpacingConstraint::JOIN, ['external'])]);
133 $result = (new CandidateResolver(new CombinedSpacingRule()))->resolve($candidate, new LexemeInput($terminals, 0, $right));
134 self::assertInstanceOf(ResolvedOutput::class, $result);
135 self::assertSame('A B)', (new SqlSerializer())->serialize($result->pieces()));
136 self::assertSame([')', ''], $right->pieces());
137 self::assertNull($right->left);
138 }
139
140 public function testResolveReportsContradictionsWithBothRuleSources(): void
141 {
142 $terminals = TerminalSequence::fromNames(['X', 'Y']);
143 $right = new ResolvedOutput([new OutputPart(new Lexeme('Y', 'symbol', $terminals->terminals[1], 'y'), '', 'y', [])], new SpacingConstraint(SpacingConstraint::JOIN, ['right-join']));
144 $spacing = $this->createMock(SpacingRule::class);
145 $spacing->method('apply')->willReturn(new SpacingConstraint(SpacingConstraint::SPACE, ['word-space']));
146 $result = (new CandidateResolver($spacing))->resolve(new LexemeSequence([new Lexeme('X', 'symbol', $terminals->terminals[0], 'x')], 'x'), new LexemeInput($terminals, 0, $right));
147 self::assertInstanceOf(SpacingConstraint::class, $result);
148 self::assertNull($result->separator());
149 self::assertSame(['right-join', 'word-space'], $result->rules);
150 self::assertSame(SpacingConstraint::JOIN, $right->left?->allowed);
151 }
152
153 public function testResolveRejectsAnOwedNeighborAtEitherEndOfTheInput(): void
154 {
155 $input = new LexemeInput(TerminalSequence::fromNames(['X']), 0, new ResolvedOutput());
156 $lexeme = new Lexeme('X', 'symbol', $input->terminal(), 'x');
157 $resolver = new CandidateResolver(new CombinedSpacingRule());
158 $left = $resolver->resolve(new LexemeSequence([$lexeme], 'x', new SpacingConstraint(SpacingConstraint::JOIN, ['left'])), $input);
159 $right = $resolver->resolve(new LexemeSequence([$lexeme], 'x', null, [1 => new SpacingConstraint(SpacingConstraint::SPACE, ['right'])]), $input);
160 self::assertInstanceOf(SpacingConstraint::class, $left);
161 self::assertInstanceOf(SpacingConstraint::class, $right);
162 self::assertSame(['left', 'missing-left-neighbor'], $left->rules);
163 self::assertSame(['right', 'missing-right-neighbor'], $right->rules);
164 }
165}
166