1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace PhpMyAdmin\SqlParser\Tools; |
||||
6 | |||||
7 | use Exception; |
||||
8 | use PhpMyAdmin\SqlParser\Context; |
||||
9 | use PhpMyAdmin\SqlParser\Exceptions\LexerException; |
||||
10 | use PhpMyAdmin\SqlParser\Exceptions\ParserException; |
||||
11 | use PhpMyAdmin\SqlParser\Lexer; |
||||
12 | use PhpMyAdmin\SqlParser\Parser; |
||||
13 | use PhpMyAdmin\SqlParser\Token; |
||||
14 | |||||
15 | use function dirname; |
||||
16 | use function file_exists; |
||||
17 | use function file_get_contents; |
||||
18 | use function file_put_contents; |
||||
19 | use function in_array; |
||||
20 | use function is_dir; |
||||
21 | use function json_decode; |
||||
22 | use function json_encode; |
||||
23 | use function mkdir; |
||||
24 | use function print_r; |
||||
25 | use function scandir; |
||||
26 | use function sprintf; |
||||
27 | use function str_contains; |
||||
28 | use function str_ends_with; |
||||
29 | use function str_replace; |
||||
30 | use function strpos; |
||||
31 | use function substr; |
||||
32 | |||||
33 | use const JSON_PRESERVE_ZERO_FRACTION; |
||||
34 | use const JSON_PRETTY_PRINT; |
||||
35 | use const JSON_UNESCAPED_SLASHES; |
||||
36 | use const JSON_UNESCAPED_UNICODE; |
||||
37 | |||||
38 | /** |
||||
39 | * Used for test generation. |
||||
40 | */ |
||||
41 | class TestGenerator |
||||
42 | { |
||||
43 | /** |
||||
44 | * Generates a test's data. |
||||
45 | * |
||||
46 | * @param string $query the query to be analyzed |
||||
47 | * @param string $type test's type (may be `lexer` or `parser`) |
||||
48 | * |
||||
49 | * @return array<string, string|Lexer|Parser|array<string, array<int, array<int, int|string|Token|null>>>|null> |
||||
50 | */ |
||||
51 | public static function generate(string $query, string $type = 'parser'): array |
||||
52 | { |
||||
53 | /** |
||||
54 | * Lexer used for tokenizing the query. |
||||
55 | */ |
||||
56 | $lexer = new Lexer($query); |
||||
57 | |||||
58 | /** |
||||
59 | * Parsed used for analyzing the query. |
||||
60 | * A new instance of parser is generated only if the test requires. |
||||
61 | */ |
||||
62 | $parser = $type === 'parser' ? new Parser($lexer->list) : null; |
||||
63 | |||||
64 | /** |
||||
65 | * Lexer's errors. |
||||
66 | */ |
||||
67 | $lexerErrors = []; |
||||
68 | |||||
69 | /** |
||||
70 | * Parser's errors. |
||||
71 | */ |
||||
72 | $parserErrors = []; |
||||
73 | |||||
74 | // Both the lexer and the parser construct exception for errors. |
||||
75 | // Usually, exceptions contain a full stack trace and other details that |
||||
76 | // are not required. |
||||
77 | // The code below extracts only the relevant information. |
||||
78 | |||||
79 | // Extracting lexer's errors. |
||||
80 | if (! empty($lexer->errors)) { |
||||
81 | /** @var LexerException $err */ |
||||
82 | foreach ($lexer->errors as $err) { |
||||
83 | $lexerErrors[] = [ |
||||
84 | $err->getMessage(), |
||||
85 | $err->ch, |
||||
86 | $err->pos, |
||||
87 | $err->getCode(), |
||||
88 | ]; |
||||
89 | } |
||||
90 | |||||
91 | $lexer->errors = []; |
||||
92 | } |
||||
93 | |||||
94 | // Extracting parser's errors. |
||||
95 | if (! empty($parser->errors)) { |
||||
96 | /** @var ParserException $err */ |
||||
97 | foreach ($parser->errors as $err) { |
||||
98 | $parserErrors[] = [ |
||||
99 | $err->getMessage(), |
||||
100 | $err->token, |
||||
101 | $err->getCode(), |
||||
102 | ]; |
||||
103 | } |
||||
104 | |||||
105 | $parser->errors = []; |
||||
106 | } |
||||
107 | |||||
108 | return [ |
||||
109 | 'query' => $query, |
||||
110 | 'lexer' => $lexer, |
||||
111 | 'parser' => $parser, |
||||
112 | 'errors' => [ |
||||
113 | 'lexer' => $lexerErrors, |
||||
114 | 'parser' => $parserErrors, |
||||
115 | ], |
||||
116 | ]; |
||||
117 | } |
||||
118 | |||||
119 | /** |
||||
120 | * Builds a test. |
||||
121 | * |
||||
122 | * Reads the input file, generates the data and writes it back. |
||||
123 | * |
||||
124 | * @param string $type the type of this test |
||||
125 | * @param string $input the input file |
||||
126 | * @param string $output the output file |
||||
127 | * @param string $debug the debug file |
||||
128 | * @param bool $ansi activate quotes ANSI mode |
||||
129 | */ |
||||
130 | public static function build( |
||||
131 | string $type, |
||||
132 | string $input, |
||||
133 | string $output, |
||||
134 | string|null $debug = null, |
||||
135 | bool $ansi = false, |
||||
136 | ): void { |
||||
137 | // Support query types: `lexer` / `parser`. |
||||
138 | if (! in_array($type, ['lexer', 'parser'])) { |
||||
139 | throw new Exception('Unknown test type (expected `lexer` or `parser`).'); |
||||
140 | } |
||||
141 | |||||
142 | /** |
||||
143 | * The query that is used to generate the test. |
||||
144 | */ |
||||
145 | $query = file_get_contents($input); |
||||
146 | |||||
147 | // There is no point in generating a test without a query. |
||||
148 | if (empty($query)) { |
||||
149 | throw new Exception('No input query specified.'); |
||||
150 | } |
||||
151 | |||||
152 | if ($ansi === true) { |
||||
153 | // set ANSI_QUOTES for ansi tests |
||||
154 | Context::setMode(Context::SQL_MODE_ANSI_QUOTES); |
||||
155 | } |
||||
156 | |||||
157 | $mariaDbPos = strpos($input, '_mariadb_'); |
||||
158 | if ($mariaDbPos !== false) {// Keep in sync with TestCase.php |
||||
159 | // set context |
||||
160 | $mariaDbVersion = (int) substr($input, $mariaDbPos + 9, 6); |
||||
161 | Context::load('MariaDb' . $mariaDbVersion); |
||||
162 | } else { |
||||
163 | // Load the default context to be sure there is no side effects |
||||
164 | Context::load(); |
||||
165 | } |
||||
166 | |||||
167 | $test = static::generate($query, $type); |
||||
168 | |||||
169 | // unset mode, reset to default every time, to be sure |
||||
170 | Context::setMode(); |
||||
171 | $serializer = new CustomJsonSerializer(); |
||||
172 | // Writing test's data. |
||||
173 | $encoded = $serializer->serialize($test); |
||||
174 | |||||
175 | $encoded = (string) json_encode( |
||||
176 | json_decode($encoded), |
||||
177 | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_SLASHES, |
||||
178 | ); |
||||
179 | |||||
180 | // Remove the project path from .out file, it changes for each dev |
||||
181 | $projectFolder = dirname(__DIR__, 2);// Jump to root |
||||
182 | $encoded = str_replace($projectFolder, '<project-root>', $encoded); |
||||
183 | |||||
184 | file_put_contents($output, $encoded); |
||||
185 | |||||
186 | // Dumping test's data in human readable format too (if required). |
||||
187 | if (empty($debug)) { |
||||
188 | return; |
||||
189 | } |
||||
190 | |||||
191 | file_put_contents($debug, print_r($test, true)); |
||||
192 | } |
||||
193 | |||||
194 | /** |
||||
195 | * Generates recursively all tests preserving the directory structure. |
||||
196 | * |
||||
197 | * @param string $input the input directory |
||||
198 | * @param string $output the output directory |
||||
199 | */ |
||||
200 | public static function buildAll(string $input, string $output, mixed $debug = null): void |
||||
201 | { |
||||
202 | $files = scandir($input); |
||||
203 | |||||
204 | foreach ($files as $file) { |
||||
205 | // Skipping current and parent directories. |
||||
206 | if (($file === '.') || ($file === '..')) { |
||||
207 | continue; |
||||
208 | } |
||||
209 | |||||
210 | // Appending the filename to directories. |
||||
211 | $inputFile = $input . '/' . $file; |
||||
212 | $outputFile = $output . '/' . $file; |
||||
213 | $debugFile = $debug !== null ? $debug . '/' . $file : null; |
||||
214 | |||||
215 | if (is_dir($inputFile)) { |
||||
216 | // Creating required directories to maintain the structure. |
||||
217 | // Ignoring errors if the folder structure exists already. |
||||
218 | if (! is_dir($outputFile)) { |
||||
219 | mkdir($outputFile); |
||||
220 | } |
||||
221 | |||||
222 | if (($debug !== null) && (! is_dir($debugFile))) { |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
223 | mkdir($debugFile); |
||||
0 ignored issues
–
show
It seems like
$debugFile can also be of type null ; however, parameter $directory of mkdir() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
224 | } |
||||
225 | |||||
226 | // Generating tests recursively. |
||||
227 | static::buildAll($inputFile, $outputFile, $debugFile); |
||||
228 | } elseif (str_ends_with($inputFile, '.in')) { |
||||
229 | // Generating file names by replacing `.in` with `.out` and |
||||
230 | // `.debug`. |
||||
231 | $outputFile = substr($outputFile, 0, -3) . '.out'; |
||||
232 | if ($debug !== null) { |
||||
233 | $debugFile = substr($debugFile, 0, -3) . '.debug'; |
||||
0 ignored issues
–
show
It seems like
$debugFile can also be of type null ; however, parameter $string of substr() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
234 | } |
||||
235 | |||||
236 | // Building the test. |
||||
237 | if (! file_exists($outputFile)) { |
||||
238 | echo sprintf("Building test for %s...\n", $inputFile); |
||||
239 | static::build( |
||||
240 | str_contains($inputFile, 'lex') ? 'lexer' : 'parser', |
||||
241 | $inputFile, |
||||
242 | $outputFile, |
||||
243 | $debugFile, |
||||
244 | str_contains($inputFile, 'ansi'), |
||||
245 | ); |
||||
246 | } else { |
||||
247 | echo sprintf("Test for %s already built!\n", $inputFile); |
||||
248 | } |
||||
249 | } |
||||
250 | } |
||||
251 | } |
||||
252 | } |
||||
253 |