Passed
Pull Request — master (#520)
by
unknown
02:50
created

Parser::__construct()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.0208

Importance

Changes 0
Metric Value
cc 6
eloc 11
c 0
b 0
f 0
nc 12
nop 2
dl 0
loc 20
rs 9.2222
ccs 11
cts 12
cp 0.9167
crap 6.0208
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
        // formatter.
120
        'PARTITION BY' => [],
121
        'SUBPARTITION BY' => [],
122
123
        // This is not a proper keyword and was added here to help the
124
        // builder.
125
        '_OPTIONS' => [
126
            'class' => Components\OptionsArray::class,
127
            'field' => 'options',
128
        ],
129
        '_END_OPTIONS' => [
130
            'class' => Components\OptionsArray::class,
131
            'field' => 'endOptions',
132
        ],
133
        '_GROUP_OPTIONS' => [
134
            'class' => Components\OptionsArray::class,
135
            'field' => 'groupOptions',
136
        ],
137
138
        'INTERSECT' => [
139
            'class' => Components\UnionKeyword::class,
140
            'field' => 'union',
141
        ],
142
        'EXCEPT' => [
143
            'class' => Components\UnionKeyword::class,
144
            'field' => 'union',
145
        ],
146
        'UNION' => [
147
            'class' => Components\UnionKeyword::class,
148
            'field' => 'union',
149
        ],
150
        'UNION ALL' => [
151
            'class' => Components\UnionKeyword::class,
152
            'field' => 'union',
153
        ],
154
        'UNION DISTINCT' => [
155
            'class' => Components\UnionKeyword::class,
156
            'field' => 'union',
157
        ],
158
159
        // Actual clause parsers.
160
        'ALTER' => [
161
            'class' => Components\Expression::class,
162
            'field' => 'table',
163
            'options' => ['parseField' => 'table'],
164
        ],
165
        'ANALYZE' => [
166
            'class' => Components\ExpressionArray::class,
167
            'field' => 'tables',
168
            'options' => ['parseField' => 'table'],
169
        ],
170
        'BACKUP' => [
171
            'class' => Components\ExpressionArray::class,
172
            'field' => 'tables',
173
            'options' => ['parseField' => 'table'],
174
        ],
175
        'CALL' => [
176
            'class' => Components\FunctionCall::class,
177
            'field' => 'call',
178
        ],
179
        'CHECK' => [
180
            'class' => Components\ExpressionArray::class,
181
            'field' => 'tables',
182
            'options' => ['parseField' => 'table'],
183
        ],
184
        'CHECKSUM' => [
185
            'class' => Components\ExpressionArray::class,
186
            'field' => 'tables',
187
            'options' => ['parseField' => 'table'],
188
        ],
189
        'CROSS JOIN' => [
190
            'class' => Components\JoinKeyword::class,
191
            'field' => 'join',
192
        ],
193
        'DROP' => [
194
            'class' => Components\ExpressionArray::class,
195
            'field' => 'fields',
196
            'options' => ['parseField' => 'table'],
197
        ],
198
        'FORCE' => [
199
            'class' => Components\IndexHint::class,
200
            'field' => 'index_hints',
201
        ],
202
        'FROM' => [
203
            'class' => Components\ExpressionArray::class,
204
            'field' => 'from',
205
            'options' => ['field' => 'table'],
206
        ],
207
        'GROUP BY' => [
208
            'class' => Components\GroupKeyword::class,
209
            'field' => 'group',
210
        ],
211
        'HAVING' => [
212
            'class' => Components\Condition::class,
213
            'field' => 'having',
214
        ],
215
        'IGNORE' => [
216
            'class' => Components\IndexHint::class,
217
            'field' => 'index_hints',
218
        ],
219
        'INTO' => [
220
            'class' => Components\IntoKeyword::class,
221
            'field' => 'into',
222
        ],
223
        'JOIN' => [
224
            'class' => Components\JoinKeyword::class,
225
            'field' => 'join',
226
        ],
227
        'LEFT JOIN' => [
228
            'class' => Components\JoinKeyword::class,
229
            'field' => 'join',
230
        ],
231
        'LEFT OUTER JOIN' => [
232
            'class' => Components\JoinKeyword::class,
233
            'field' => 'join',
234
        ],
235
        'ON' => [
236
            'class' => Components\Expression::class,
237
            'field' => 'table',
238
            'options' => ['parseField' => 'table'],
239
        ],
240
        'RIGHT JOIN' => [
241
            'class' => Components\JoinKeyword::class,
242
            'field' => 'join',
243
        ],
244
        'RIGHT OUTER JOIN' => [
245
            'class' => Components\JoinKeyword::class,
246
            'field' => 'join',
247
        ],
248
        'INNER JOIN' => [
249
            'class' => Components\JoinKeyword::class,
250
            'field' => 'join',
251
        ],
252
        'FULL JOIN' => [
253
            'class' => Components\JoinKeyword::class,
254
            'field' => 'join',
255
        ],
256
        'FULL OUTER JOIN' => [
257
            'class' => Components\JoinKeyword::class,
258
            'field' => 'join',
259
        ],
260
        'NATURAL JOIN' => [
261
            'class' => Components\JoinKeyword::class,
262
            'field' => 'join',
263
        ],
264
        'NATURAL LEFT JOIN' => [
265
            'class' => Components\JoinKeyword::class,
266
            'field' => 'join',
267
        ],
268
        'NATURAL RIGHT JOIN' => [
269
            'class' => Components\JoinKeyword::class,
270
            'field' => 'join',
271
        ],
272
        'NATURAL LEFT OUTER JOIN' => [
273
            'class' => Components\JoinKeyword::class,
274
            'field' => 'join',
275
        ],
276
        'NATURAL RIGHT OUTER JOIN' => [
277
            'class' => Components\JoinKeyword::class,
278
            'field' => 'join',
279
        ],
280
        'STRAIGHT_JOIN' => [
281
            'class' => Components\JoinKeyword::class,
282
            'field' => 'join',
283
        ],
284
        'LIMIT' => [
285
            'class' => Components\Limit::class,
286
            'field' => 'limit',
287
        ],
288
        'OPTIMIZE' => [
289
            'class' => Components\ExpressionArray::class,
290
            'field' => 'tables',
291
            'options' => ['parseField' => 'table'],
292
        ],
293
        'ORDER BY' => [
294
            'class' => Components\OrderKeyword::class,
295
            'field' => 'order',
296
        ],
297
        'PARTITION' => [
298
            'class' => Components\ArrayObj::class,
299
            'field' => 'partition',
300
        ],
301
        'PROCEDURE' => [
302
            'class' => Components\FunctionCall::class,
303
            'field' => 'procedure',
304
        ],
305
        'RENAME' => [
306
            'class' => Components\RenameOperation::class,
307
            'field' => 'renames',
308
        ],
309
        'REPAIR' => [
310
            'class' => Components\ExpressionArray::class,
311
            'field' => 'tables',
312
            'options' => ['parseField' => 'table'],
313
        ],
314
        'RESTORE' => [
315
            'class' => Components\ExpressionArray::class,
316
            'field' => 'tables',
317
            'options' => ['parseField' => 'table'],
318
        ],
319
        'SET' => [
320
            'class' => Components\SetOperation::class,
321
            'field' => 'set',
322
        ],
323
        'SELECT' => [
324
            'class' => Components\ExpressionArray::class,
325
            'field' => 'expr',
326
        ],
327
        'TRUNCATE' => [
328
            'class' => Components\Expression::class,
329
            'field' => 'table',
330
            'options' => ['parseField' => 'table'],
331
        ],
332
        'UPDATE' => [
333
            'class' => Components\ExpressionArray::class,
334
            'field' => 'tables',
335
            'options' => ['parseField' => 'table'],
336
        ],
337
        'USE' => [
338
            'class' => Components\IndexHint::class,
339
            'field' => 'index_hints',
340
        ],
341
        'VALUE' => [
342
            'class' => Components\Array2d::class,
343
            'field' => 'values',
344
        ],
345
        'VALUES' => [
346
            'class' => Components\Array2d::class,
347
            'field' => 'values',
348
        ],
349
        'WHERE' => [
350
            'class' => Components\Condition::class,
351
            'field' => 'where',
352
        ],
353
    ];
354
355
    /**
356
     * The list of tokens that are parsed.
357
     *
358
     * @var TokensList|null
359
     */
360
    public $list;
361
362
    /**
363
     * List of statements parsed.
364
     *
365
     * @var Statement[]
366
     */
367
    public $statements = [];
368
369
    /**
370
     * The number of opened brackets.
371
     *
372
     * @var int
373
     */
374
    public $brackets = 0;
375
376
    /**
377
     * @param string|UtfString|TokensList|null $list   the list of tokens to be parsed
378
     * @param bool                             $strict whether strict mode should be enabled or not
379
     */
380 1322
    public function __construct($list = null, $strict = false)
381
    {
382 1322
        if (Context::$keywords === []) {
383
            Context::load();
384
        }
385
386 1322
        if (is_string($list) || ($list instanceof UtfString)) {
387 324
            $lexer = new Lexer($list, $strict);
388 324
            $this->list = $lexer->list;
389 1006
        } elseif ($list instanceof TokensList) {
390 836
            $this->list = $list;
391
        }
392
393 1322
        $this->strict = $strict;
394
395 1322
        if ($list === null) {
396 170
            return;
397
        }
398
399 1152
        $this->parse();
400
    }
401
402
    /**
403
     * Builds the parse trees.
404
     *
405
     * @throws ParserException
406
     */
407 1152
    public function parse(): void
408
    {
409
        /**
410
         * Last transaction.
411
         *
412
         * @var TransactionStatement
413
         */
414 1152
        $lastTransaction = null;
415
416
        /**
417
         * Last parsed statement.
418
         *
419
         * @var Statement
420
         */
421 1152
        $lastStatement = null;
422
423
        /**
424
         * Union's type or false for no union.
425
         *
426
         * @var bool|string
427
         */
428 1152
        $unionType = false;
429
430
        /**
431
         * The index of the last token from the last statement.
432
         *
433
         * @var int
434
         */
435 1152
        $prevLastIdx = -1;
436
437
        /**
438
         * The list of tokens.
439
         */
440 1152
        $list = &$this->list;
441
442 1152
        for (; $list->idx < $list->count; ++$list->idx) {
443
            /**
444
             * Token parsed at this moment.
445
             */
446 1148
            $token = $list->tokens[$list->idx];
447
448
            // `DELIMITER` is not an actual statement and it requires
449
            // special handling.
450 1148
            if (($token->type === TokenType::None) && (strtoupper($token->token) === 'DELIMITER')) {
451
                // Skipping to the end of this statement.
452 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

452
                $list->/** @scrutinizer ignore-call */ 
453
                       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...
453 24
                $prevLastIdx = $list->idx;
454 24
                continue;
455
            }
456
457
            // Counting the brackets around statements.
458 1148
            if ($token->value === '(') {
459 24
                ++$this->brackets;
460 24
                continue;
461
            }
462
463
            // Statements can start with keywords only.
464
            // Comments, whitespaces, etc. are ignored.
465 1148
            if ($token->type !== TokenType::Keyword) {
466
                if (
467 964
                    ($token->type !== TokenType::Comment)
468 964
                    && ($token->type !== TokenType::Whitespace)
469 964
                    && ($token->type !== TokenType::Operator) // `(` and `)`
470 964
                    && ($token->type !== TokenType::Delimiter)
471
                ) {
472 42
                    $this->error('Unexpected beginning of statement.', $token);
473
                }
474
475 964
                continue;
476
            }
477
478
            if (
479 1146
                ($token->keyword === 'UNION') ||
480 1146
                    ($token->keyword === 'UNION ALL') ||
481 1146
                    ($token->keyword === 'UNION DISTINCT') ||
482 1146
                    ($token->keyword === 'EXCEPT') ||
483 1146
                    ($token->keyword === 'INTERSECT')
484
            ) {
485 38
                $unionType = $token->keyword;
486 38
                continue;
487
            }
488
489 1146
            $lastIdx = $list->idx;
490 1146
            $statementName = null;
491
492 1146
            if ($token->keyword === 'ANALYZE') {
493 16
                ++$list->idx; // Skip ANALYZE
494
495 16
                $first = $list->getNextOfType(TokenType::Keyword);
496 16
                $second = $list->getNextOfType(TokenType::Keyword);
497
498
                // ANALYZE keyword can be an indication of two cases:
499
                // 1 - ANALYZE TABLE statements, in both MariaDB and MySQL
500
                // 2 - Explain statement, in case of MariaDB https://mariadb.com/kb/en/explain-analyze/
501
                // We need to point case 2 to use the EXPLAIN Parser.
502 16
                $statementName = 'EXPLAIN';
503 16
                if (($first && $first->keyword === 'TABLE') || ($second && $second->keyword === 'TABLE')) {
504 6
                    $statementName = 'ANALYZE';
505
                }
506
507 16
                $list->idx = $lastIdx;
508 1136
            } elseif (empty(self::STATEMENT_PARSERS[$token->keyword])) {
509
                // Checking if it is a known statement that can be parsed.
510 80
                if (! isset(self::STATEMENT_PARSERS[$token->keyword])) {
511
                    // A statement is considered recognized if the parser
512
                    // is aware that it is a statement, but it does not have
513
                    // a parser for it yet.
514 80
                    $this->error('Unrecognized statement type.', $token);
515
                }
516
517
                // Skipping to the end of this statement.
518 80
                $list->getNextOfType(TokenType::Delimiter);
519 80
                $prevLastIdx = $list->idx;
520 80
                continue;
521
            }
522
523
            /**
524
             * The name of the class that is used for parsing.
525
             *
526
             * @var string
527
             */
528 1142
            $class = self::STATEMENT_PARSERS[$statementName ?? $token->keyword];
529
530
            /**
531
             * Processed statement.
532
             *
533
             * @var Statement
534
             */
535 1142
            $statement = new $class($this, $this->list);
536
537
            // The first token that is a part of this token is the next token
538
            // unprocessed by the previous statement.
539
            // There might be brackets around statements and this shouldn't
540
            // affect the parser
541 1142
            $statement->first = $prevLastIdx + 1;
542
543
            // Storing the index of the last token parsed and updating the old
544
            // index.
545 1142
            $statement->last = $list->idx;
546 1142
            $prevLastIdx = $list->idx;
547
548
            // Handles unions.
549
            if (
550 1142
                ! empty($unionType)
551 1142
                && ($lastStatement instanceof SelectStatement)
552 1142
                && ($statement instanceof SelectStatement)
553
            ) {
554
                /*
555
                 * This SELECT statement.
556
                 *
557
                 * @var SelectStatement $statement
558
                 */
559
560
                /*
561
                 * Last SELECT statement.
562
                 *
563
                 * @var SelectStatement $lastStatement
564
                 */
565 38
                $lastStatement->union[] = [
566 38
                    $unionType,
567 38
                    $statement,
568 38
                ];
569
570
                // if there are no no delimiting brackets, the `ORDER` and
571
                // `LIMIT` keywords actually belong to the first statement.
572 38
                $lastStatement->order = $statement->order;
573 38
                $lastStatement->limit = $statement->limit;
574 38
                $statement->order = [];
575 38
                $statement->limit = null;
576
577
                // The statement actually ends where the last statement in
578
                // union ends.
579 38
                $lastStatement->last = $statement->last;
580
581 38
                $unionType = false;
582
583
                // Validate clause order
584 38
                $statement->validateClauseOrder($this, $list);
585 38
                continue;
586
            }
587
588
            // Handles transactions.
589 1142
            if ($statement instanceof TransactionStatement) {
590
                /*
591
                 * @var TransactionStatement
592
                 */
593 26
                if ($statement->type === TransactionStatement::TYPE_BEGIN) {
594 22
                    $lastTransaction = $statement;
595 22
                    $this->statements[] = $statement;
596 22
                } elseif ($statement->type === TransactionStatement::TYPE_END) {
597 20
                    if ($lastTransaction === null) {
598
                        // Even though an error occurred, the query is being
599
                        // saved.
600 6
                        $this->statements[] = $statement;
601 6
                        $this->error('No transaction was previously started.', $token);
602
                    } else {
603 18
                        $lastTransaction->end = $statement;
604
                    }
605
606 20
                    $lastTransaction = null;
607
                }
608
609
                // Validate clause order
610 26
                $statement->validateClauseOrder($this, $list);
611 26
                continue;
612
            }
613
614
            // Validate clause order
615 1140
            $statement->validateClauseOrder($this, $list);
616
617
            // Finally, storing the statement.
618 1140
            if ($lastTransaction !== null) {
619 22
                $lastTransaction->statements[] = $statement;
620
            } else {
621 1124
                $this->statements[] = $statement;
622
            }
623
624 1140
            $lastStatement = $statement;
625
        }
626
    }
627
628
    /**
629
     * Creates a new error log.
630
     *
631
     * @param string $msg   the error message
632
     * @param Token  $token the token that produced the error
633
     * @param int    $code  the code of the error
634
     *
635
     * @throws ParserException throws the exception, if strict mode is enabled.
636
     */
637 314
    public function error($msg, Token|null $token = null, $code = 0): void
638
    {
639 314
        $error = new ParserException(
640 314
            Translator::gettext($msg),
641 314
            $token,
642 314
            $code
643 314
        );
644
645 314
        if ($this->strict) {
646 2
            throw $error;
647
        }
648
649 312
        $this->errors[] = $error;
650
    }
651
}
652