TestGenerator   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 79
c 2
b 0
f 0
dl 0
loc 207
ccs 0
cts 86
cp 0
rs 10
wmc 25

3 Methods

Rating   Name   Duplication   Size   Complexity  
B generate() 0 64 6
C buildAll() 0 48 13
B build() 0 62 6
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
It seems like $debugFile can also be of type null; however, parameter $filename of is_dir() 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 ignore-type  annotation

222
                if (($debug !== null) && (! is_dir(/** @scrutinizer ignore-type */ $debugFile))) {
Loading history...
223
                    mkdir($debugFile);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

223
                    mkdir(/** @scrutinizer ignore-type */ $debugFile);
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
Bug introduced by
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 ignore-type  annotation

233
                    $debugFile = substr(/** @scrutinizer ignore-type */ $debugFile, 0, -3) . '.debug';
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