Parser::parse()   D
last analyzed

Complexity

Conditions 30
Paths 29

Size

Total Lines 206
Code Lines 85

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 82
CRAP Score 30

Importance

Changes 0
Metric Value
cc 30
eloc 85
nc 29
nop 0
dl 0
loc 206
ccs 82
cts 82
cp 1
crap 30
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser;
6
7
use Exception;
8
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
9
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
10
use PhpMyAdmin\SqlParser\Statements\TransactionStatement;
11
12
use function is_string;
13
use function strtoupper;
14
15
/**
16
 * Defines the parser of the library.
17
 *
18
 * This is one of the most important components, along with the lexer.
19
 *
20
 * Takes multiple tokens (contained in a Lexer instance) as input and builds a parse tree.
21
 */
22
class Parser
23
{
24
    /**
25
     * Whether errors should throw exceptions or just be stored.
26
     */
27
    private bool $strict = false;
28
29
    /**
30
     * List of errors that occurred during lexing.
31
     *
32
     * Usually, the lexing does not stop once an error occurred because that
33
     * error might be false positive or a partial result (even a bad one)
34
     * might be needed.
35
     *
36
     * @var Exception[]
37
     */
38
    public array $errors = [];
39
40
    /**
41
     * Array of classes that are used in parsing the SQL statements.
42
     *
43
     * @psalm-var array<string, class-string<Statement>|''>
44
     */
45
    public const STATEMENT_PARSERS = [
46
        // MySQL Utility Statements
47
        'DESCRIBE' => Statements\ExplainStatement::class,
48
        'DESC' => Statements\ExplainStatement::class,
49
        'EXPLAIN' => Statements\ExplainStatement::class,
50
        'FLUSH' => '',
51
        'GRANT' => '',
52
        'HELP' => '',
53
        'SET PASSWORD' => '',
54
        'STATUS' => '',
55
        'USE' => '',
56
57
        // Table Maintenance Statements
58
        // https://dev.mysql.com/doc/refman/5.7/en/table-maintenance-sql.html
59
        'ANALYZE' => Statements\AnalyzeStatement::class,
60
        'BACKUP' => Statements\BackupStatement::class,
61
        'CHECK' => Statements\CheckStatement::class,
62
        'CHECKSUM' => Statements\ChecksumStatement::class,
63
        'OPTIMIZE' => Statements\OptimizeStatement::class,
64
        'REPAIR' => Statements\RepairStatement::class,
65
        'RESTORE' => Statements\RestoreStatement::class,
66
67
        // Database Administration Statements
68
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-server-administration.html
69
        'SET' => Statements\SetStatement::class,
70
        'SHOW' => Statements\ShowStatement::class,
71
72
        // Data Definition Statements.
73
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-definition.html
74
        'ALTER' => Statements\AlterStatement::class,
75
        'CREATE' => Statements\CreateStatement::class,
76
        'DROP' => Statements\DropStatement::class,
77
        'RENAME' => Statements\RenameStatement::class,
78
        'TRUNCATE' => Statements\TruncateStatement::class,
79
80
        // Data Manipulation Statements.
81
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-manipulation.html
82
        'CALL' => Statements\CallStatement::class,
83
        'DELETE' => Statements\DeleteStatement::class,
84
        'DO' => '',
85
        'HANDLER' => '',
86
        'INSERT' => Statements\InsertStatement::class,
87
        'LOAD DATA' => Statements\LoadStatement::class,
88
        'REPLACE' => Statements\ReplaceStatement::class,
89
        'SELECT' => Statements\SelectStatement::class,
90
        'UPDATE' => Statements\UpdateStatement::class,
91
        'WITH' => Statements\WithStatement::class,
92
93
        // Prepared Statements.
94
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html
95
        'DEALLOCATE' => '',
96
        'EXECUTE' => '',
97
        'PREPARE' => '',
98
99
        // Transactional and Locking Statements
100
        // https://dev.mysql.com/doc/refman/5.7/en/commit.html
101
        'BEGIN' => Statements\TransactionStatement::class,
102
        'COMMIT' => Statements\TransactionStatement::class,
103
        'ROLLBACK' => Statements\TransactionStatement::class,
104
        'START TRANSACTION' => Statements\TransactionStatement::class,
105
106
        'PURGE' => Statements\PurgeStatement::class,
107
108
        // Lock statements
109
        // https://dev.mysql.com/doc/refman/5.7/en/lock-tables.html
110
        'LOCK' => Statements\LockStatement::class,
111
        'UNLOCK' => Statements\LockStatement::class,
112
    ];
113
114
    /**
115
     * Array of classes that are used in parsing SQL components.
116
     */
117
    public const KEYWORD_PARSERS = [
118
        // This is not a proper keyword and was added here to help the
119
        // builder.
120
        '_OPTIONS' => [
121
            'class' => Parsers\OptionsArrays::class,
122
            'field' => 'options',
123
        ],
124
        '_END_OPTIONS' => [
125
            'class' => Parsers\OptionsArrays::class,
126
            'field' => 'endOptions',
127
        ],
128
        '_GROUP_OPTIONS' => [
129
            'class' => Parsers\OptionsArrays::class,
130
            'field' => 'groupOptions',
131
        ],
132
133
        'INTERSECT' => [
134
            'class' => Parsers\UnionKeywords::class,
135
            'field' => 'union',
136
        ],
137
        'EXCEPT' => [
138
            'class' => Parsers\UnionKeywords::class,
139
            'field' => 'union',
140
        ],
141
        'UNION' => [
142
            'class' => Parsers\UnionKeywords::class,
143
            'field' => 'union',
144
        ],
145
        'UNION ALL' => [
146
            'class' => Parsers\UnionKeywords::class,
147
            'field' => 'union',
148
        ],
149
        'UNION DISTINCT' => [
150
            'class' => Parsers\UnionKeywords::class,
151
            'field' => 'union',
152
        ],
153
154
        // Actual clause parsers.
155
        'ALTER' => [
156
            'class' => Parsers\Expressions::class,
157
            'field' => 'table',
158
            'options' => ['parseField' => 'table'],
159
        ],
160
        'ANALYZE' => [
161
            'class' => Parsers\ExpressionArray::class,
162
            'field' => 'tables',
163
            'options' => ['parseField' => 'table'],
164
        ],
165
        'BACKUP' => [
166
            'class' => Parsers\ExpressionArray::class,
167
            'field' => 'tables',
168
            'options' => ['parseField' => 'table'],
169
        ],
170
        'CALL' => [
171
            'class' => Parsers\FunctionCalls::class,
172
            'field' => 'call',
173
        ],
174
        'CHECK' => [
175
            'class' => Parsers\ExpressionArray::class,
176
            'field' => 'tables',
177
            'options' => ['parseField' => 'table'],
178
        ],
179
        'CHECKSUM' => [
180
            'class' => Parsers\ExpressionArray::class,
181
            'field' => 'tables',
182
            'options' => ['parseField' => 'table'],
183
        ],
184
        'CROSS JOIN' => [
185
            'class' => Parsers\JoinKeywords::class,
186
            'field' => 'join',
187
        ],
188
        'DROP' => [
189
            'class' => Parsers\ExpressionArray::class,
190
            'field' => 'fields',
191
            'options' => ['parseField' => 'table'],
192
        ],
193
        'FORCE' => [
194
            'class' => Parsers\IndexHints::class,
195
            'field' => 'indexHints',
196
        ],
197
        'FROM' => [
198
            'class' => Parsers\ExpressionArray::class,
199
            'field' => 'from',
200
            'options' => ['field' => 'table'],
201
        ],
202
        'GROUP BY' => [
203
            'class' => Parsers\GroupKeywords::class,
204
            'field' => 'group',
205
        ],
206
        'HAVING' => [
207
            'class' => Parsers\Conditions::class,
208
            'field' => 'having',
209
        ],
210
        'IGNORE' => [
211
            'class' => Parsers\IndexHints::class,
212
            'field' => 'indexHints',
213
        ],
214
        'INTO' => [
215
            'class' => Parsers\IntoKeywords::class,
216
            'field' => 'into',
217
        ],
218
        'JOIN' => [
219
            'class' => Parsers\JoinKeywords::class,
220
            'field' => 'join',
221
        ],
222
        'LEFT JOIN' => [
223
            'class' => Parsers\JoinKeywords::class,
224
            'field' => 'join',
225
        ],
226
        'LEFT OUTER JOIN' => [
227
            'class' => Parsers\JoinKeywords::class,
228
            'field' => 'join',
229
        ],
230
        'ON' => [
231
            'class' => Parsers\Expressions::class,
232
            'field' => 'table',
233
            'options' => ['parseField' => 'table'],
234
        ],
235
        'RIGHT JOIN' => [
236
            'class' => Parsers\JoinKeywords::class,
237
            'field' => 'join',
238
        ],
239
        'RIGHT OUTER JOIN' => [
240
            'class' => Parsers\JoinKeywords::class,
241
            'field' => 'join',
242
        ],
243
        'INNER JOIN' => [
244
            'class' => Parsers\JoinKeywords::class,
245
            'field' => 'join',
246
        ],
247
        'FULL JOIN' => [
248
            'class' => Parsers\JoinKeywords::class,
249
            'field' => 'join',
250
        ],
251
        'FULL OUTER JOIN' => [
252
            'class' => Parsers\JoinKeywords::class,
253
            'field' => 'join',
254
        ],
255
        'NATURAL JOIN' => [
256
            'class' => Parsers\JoinKeywords::class,
257
            'field' => 'join',
258
        ],
259
        'NATURAL LEFT JOIN' => [
260
            'class' => Parsers\JoinKeywords::class,
261
            'field' => 'join',
262
        ],
263
        'NATURAL RIGHT JOIN' => [
264
            'class' => Parsers\JoinKeywords::class,
265
            'field' => 'join',
266
        ],
267
        'NATURAL LEFT OUTER JOIN' => [
268
            'class' => Parsers\JoinKeywords::class,
269
            'field' => 'join',
270
        ],
271
        'NATURAL RIGHT OUTER JOIN' => [
272
            'class' => Parsers\JoinKeywords::class,
273
            'field' => 'join',
274
        ],
275
        'STRAIGHT_JOIN' => [
276
            'class' => Parsers\JoinKeywords::class,
277
            'field' => 'join',
278
        ],
279
        'LIMIT' => [
280
            'class' => Parsers\Limits::class,
281
            'field' => 'limit',
282
        ],
283
        'OPTIMIZE' => [
284
            'class' => Parsers\ExpressionArray::class,
285
            'field' => 'tables',
286
            'options' => ['parseField' => 'table'],
287
        ],
288
        'ORDER BY' => [
289
            'class' => Parsers\OrderKeywords::class,
290
            'field' => 'order',
291
        ],
292
        'PARTITION' => [
293
            'class' => Parsers\ArrayObjs::class,
294
            'field' => 'partition',
295
        ],
296
        'PROCEDURE' => [
297
            'class' => Parsers\FunctionCalls::class,
298
            'field' => 'procedure',
299
        ],
300
        'RENAME' => [
301
            'class' => Parsers\RenameOperations::class,
302
            'field' => 'renames',
303
        ],
304
        'REPAIR' => [
305
            'class' => Parsers\ExpressionArray::class,
306
            'field' => 'tables',
307
            'options' => ['parseField' => 'table'],
308
        ],
309
        'RESTORE' => [
310
            'class' => Parsers\ExpressionArray::class,
311
            'field' => 'tables',
312
            'options' => ['parseField' => 'table'],
313
        ],
314
        'SET' => [
315
            'class' => Parsers\SetOperations::class,
316
            'field' => 'set',
317
        ],
318
        'SELECT' => [
319
            'class' => Parsers\ExpressionArray::class,
320
            'field' => 'expr',
321
        ],
322
        'TRUNCATE' => [
323
            'class' => Parsers\Expressions::class,
324
            'field' => 'table',
325
            'options' => ['parseField' => 'table'],
326
        ],
327
        'UPDATE' => [
328
            'class' => Parsers\ExpressionArray::class,
329
            'field' => 'tables',
330
            'options' => ['parseField' => 'table'],
331
        ],
332
        'USE' => [
333
            'class' => Parsers\IndexHints::class,
334
            'field' => 'indexHints',
335
        ],
336
        'VALUE' => [
337
            'class' => Parsers\Array2d::class,
338
            'field' => 'values',
339
        ],
340
        'VALUES' => [
341
            'class' => Parsers\Array2d::class,
342
            'field' => 'values',
343
        ],
344
        'WHERE' => [
345
            'class' => Parsers\Conditions::class,
346
            'field' => 'where',
347
        ],
348
    ];
349
350
    /**
351
     * The list of tokens that are parsed.
352
     */
353
    public TokensList|null $list = null;
354
355
    /**
356
     * List of statements parsed.
357
     *
358
     * @var Statement[]
359
     */
360
    public array $statements = [];
361
362
    /**
363
     * The number of opened brackets.
364
     */
365
    public int $brackets = 0;
366
367
    /**
368
     * @param string|UtfString|TokensList|null $list   the list of tokens to be parsed
369
     * @param bool                             $strict whether strict mode should be enabled or not
370
     */
371 1328
    public function __construct(string|UtfString|TokensList|null $list = null, bool $strict = false)
372
    {
373 1328
        if (Context::$keywords === []) {
374
            Context::load();
375
        }
376
377 1328
        if (is_string($list) || ($list instanceof UtfString)) {
378 326
            $lexer = new Lexer($list, $strict);
379 326
            $this->list = $lexer->list;
380 1010
        } elseif ($list instanceof TokensList) {
381 840
            $this->list = $list;
382
        }
383
384 1328
        $this->strict = $strict;
385
386 1328
        if ($list === null) {
387 170
            return;
388
        }
389
390 1158
        $this->parse();
391
    }
392
393
    /**
394
     * Builds the parse trees.
395
     *
396
     * @throws ParserException
397
     */
398 1158
    public function parse(): void
399
    {
400
        /**
401
         * Last transaction.
402
         */
403 1158
        $lastTransaction = null;
404
405
        /**
406
         * Last parsed statement.
407
         */
408 1158
        $lastStatement = null;
409
410
        /**
411
         * Union's type or false for no union.
412
         */
413 1158
        $unionType = false;
414
415
        /**
416
         * The index of the last token from the last statement.
417
         */
418 1158
        $prevLastIdx = -1;
419
420
        /**
421
         * The list of tokens.
422
         */
423 1158
        $list = &$this->list;
424
425 1158
        for (; $list->idx < $list->count; ++$list->idx) {
426
            /**
427
             * Token parsed at this moment.
428
             */
429 1154
            $token = $list->tokens[$list->idx];
430
431
            // `DELIMITER` is not an actual statement and it requires
432
            // special handling.
433 1154
            if (($token->type === TokenType::None) && (strtoupper($token->token) === 'DELIMITER')) {
434
                // Skipping to the end of this statement.
435 24
                $list->getNextOfType(TokenType::Delimiter);
0 ignored issues
show
Bug introduced by
The method getNextOfType() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

435
                $list->/** @scrutinizer ignore-call */ 
436
                       getNextOfType(TokenType::Delimiter);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
436 24
                $prevLastIdx = $list->idx;
437 24
                continue;
438
            }
439
440
            // Counting the brackets around statements.
441 1154
            if ($token->value === '(') {
442 24
                ++$this->brackets;
443 24
                continue;
444
            }
445
446
            // Statements can start with keywords only.
447
            // Comments, whitespaces, etc. are ignored.
448 1154
            if ($token->type !== TokenType::Keyword) {
449
                if (
450 968
                    ($token->type !== TokenType::Comment)
451 968
                    && ($token->type !== TokenType::Whitespace)
452 968
                    && ($token->type !== TokenType::Operator) // `(` and `)`
453 968
                    && ($token->type !== TokenType::Delimiter)
454
                ) {
455 42
                    $this->error('Unexpected beginning of statement.', $token);
456
                }
457
458 968
                continue;
459
            }
460
461
            if (
462 1150
                ($token->keyword === 'UNION') ||
463 1150
                    ($token->keyword === 'UNION ALL') ||
464 1150
                    ($token->keyword === 'UNION DISTINCT') ||
465 1150
                    ($token->keyword === 'EXCEPT') ||
466 1150
                    ($token->keyword === 'INTERSECT')
467
            ) {
468 38
                $unionType = $token->keyword;
469 38
                continue;
470
            }
471
472 1150
            $lastIdx = $list->idx;
473 1150
            $statementName = null;
474
475 1150
            if ($token->keyword === 'ANALYZE') {
476 16
                ++$list->idx; // Skip ANALYZE
477
478 16
                $first = $list->getNextOfType(TokenType::Keyword);
479 16
                $second = $list->getNextOfType(TokenType::Keyword);
480
481
                // ANALYZE keyword can be an indication of two cases:
482
                // 1 - ANALYZE TABLE statements, in both MariaDB and MySQL
483
                // 2 - Explain statement, in case of MariaDB https://mariadb.com/kb/en/explain-analyze/
484
                // We need to point case 2 to use the EXPLAIN Parser.
485 16
                $statementName = 'EXPLAIN';
486 16
                if (($first && $first->keyword === 'TABLE') || ($second && $second->keyword === 'TABLE')) {
487 6
                    $statementName = 'ANALYZE';
488
                }
489
490 16
                $list->idx = $lastIdx;
491 1140
            } elseif (empty(self::STATEMENT_PARSERS[$token->keyword])) {
492
                // Checking if it is a known statement that can be parsed.
493 80
                if (! isset(self::STATEMENT_PARSERS[$token->keyword])) {
494
                    // A statement is considered recognized if the parser
495
                    // is aware that it is a statement, but it does not have
496
                    // a parser for it yet.
497 80
                    $this->error('Unrecognized statement type.', $token);
498
                }
499
500
                // Skipping to the end of this statement.
501 80
                $list->getNextOfType(TokenType::Delimiter);
502 80
                $prevLastIdx = $list->idx;
503 80
                continue;
504
            }
505
506
            /**
507
             * The name of the class that is used for parsing.
508
             */
509 1146
            $class = self::STATEMENT_PARSERS[$statementName ?? $token->keyword];
510
511
            /**
512
             * Processed statement.
513
             */
514 1146
            $statement = new $class($this, $this->list);
515
516
            // The first token that is a part of this token is the next token
517
            // unprocessed by the previous statement.
518
            // There might be brackets around statements and this shouldn't
519
            // affect the parser
520 1146
            $statement->first = $prevLastIdx + 1;
521
522
            // Storing the index of the last token parsed and updating the old
523
            // index.
524 1146
            $statement->last = $list->idx;
525 1146
            $prevLastIdx = $list->idx;
526
527
            // Handles unions.
528
            if (
529 1146
                ! empty($unionType)
530 1146
                && ($lastStatement instanceof SelectStatement)
531 1146
                && ($statement instanceof SelectStatement)
532
            ) {
533
                /*
534
                 * This SELECT statement.
535
                 *
536
                 * @var SelectStatement $statement
537
                 */
538
539
                /*
540
                 * Last SELECT statement.
541
                 *
542
                 * @var SelectStatement $lastStatement
543
                 */
544 38
                $lastStatement->union[] = [
545 38
                    $unionType,
546 38
                    $statement,
547 38
                ];
548
549
                // if there are no no delimiting brackets, the `ORDER` and
550
                // `LIMIT` keywords actually belong to the first statement.
551 38
                $lastStatement->order = $statement->order;
552 38
                $lastStatement->limit = $statement->limit;
553 38
                $statement->order = [];
554 38
                $statement->limit = null;
555
556
                // The statement actually ends where the last statement in
557
                // union ends.
558 38
                $lastStatement->last = $statement->last;
559
560 38
                $unionType = false;
561
562
                // Validate clause order
563 38
                $statement->validateClauseOrder($this, $list);
0 ignored issues
show
Bug introduced by
It seems like $list can also be of type null; however, parameter $list of PhpMyAdmin\SqlParser\Sta...::validateClauseOrder() does only seem to accept PhpMyAdmin\SqlParser\TokensList, 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

563
                $statement->validateClauseOrder($this, /** @scrutinizer ignore-type */ $list);
Loading history...
564 38
                continue;
565
            }
566
567
            // Handles transactions.
568 1146
            if ($statement instanceof TransactionStatement) {
569
                /*
570
                 * @var TransactionStatement
571
                 */
572 26
                if ($statement->type === TransactionStatement::TYPE_BEGIN) {
573 22
                    $lastTransaction = $statement;
574 22
                    $this->statements[] = $statement;
575 22
                } elseif ($statement->type === TransactionStatement::TYPE_END) {
576 20
                    if ($lastTransaction === null) {
577
                        // Even though an error occurred, the query is being
578
                        // saved.
579 6
                        $this->statements[] = $statement;
580 6
                        $this->error('No transaction was previously started.', $token);
581
                    } else {
582 18
                        $lastTransaction->end = $statement;
583
                    }
584
585 20
                    $lastTransaction = null;
586
                }
587
588
                // Validate clause order
589 26
                $statement->validateClauseOrder($this, $list);
590 26
                continue;
591
            }
592
593
            // Validate clause order
594 1144
            $statement->validateClauseOrder($this, $list);
595
596
            // Finally, storing the statement.
597 1144
            if ($lastTransaction !== null) {
598 22
                $lastTransaction->statements[] = $statement;
599
            } else {
600 1128
                $this->statements[] = $statement;
601
            }
602
603 1144
            $lastStatement = $statement;
604
        }
605
    }
606
607
    /**
608
     * Creates a new error log.
609
     *
610
     * @param string $msg   the error message
611
     * @param Token  $token the token that produced the error
612
     * @param int    $code  the code of the error
613
     *
614
     * @throws ParserException throws the exception, if strict mode is enabled.
615
     */
616 314
    public function error(string $msg, Token|null $token = null, int $code = 0): void
617
    {
618 314
        $error = new ParserException(
619 314
            Translator::gettext($msg),
620 314
            $token,
621 314
            $code,
622 314
        );
623
624 314
        if ($this->strict) {
625 2
            throw $error;
626
        }
627
628 312
        $this->errors[] = $error;
629
    }
630
}
631