phpmyadmin /
sql-parser
| 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
Loading history...
|
|||||
| 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
Loading history...
|
|||||
| 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
Loading history...
|
|||||
| 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 |