Completed
Push — master ( 20e749...a87ad6 )
by Michal
04:38
created

Parser::parse()   D

Complexity

Conditions 23
Paths 15

Size

Total Lines 201
Code Lines 74

Duplication

Lines 16
Ratio 7.96 %

Code Coverage

Tests 70
CRAP Score 23

Importance

Changes 0
Metric Value
dl 16
loc 201
ccs 70
cts 70
cp 1
rs 4.6303
c 0
b 0
f 0
cc 23
eloc 74
nc 15
nop 0
crap 23

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
/**
4
 * Defines the parser of the library.
5
 *
6
 * This is one of the most important components, along with the lexer.
7
 */
8
9
namespace PhpMyAdmin\SqlParser;
10
11
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
12
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
13
use PhpMyAdmin\SqlParser\Statements\TransactionStatement;
14
15
/**
16
 * Takes multiple tokens (contained in a Lexer instance) as input and builds a
17
 * parse tree.
18
 *
19
 * @category Parser
20
 *
21
 * @license  https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
22
 */
23
class Parser extends Core
24
{
25
    /**
26
     * Array of classes that are used in parsing the SQL statements.
27
     *
28
     * @var array
29
     */
30
    public static $STATEMENT_PARSERS = array(
31
        // MySQL Utility Statements
32
        'DESCRIBE' => 'PhpMyAdmin\\SqlParser\\Statements\\ExplainStatement',
33
        'DESC' => 'PhpMyAdmin\\SqlParser\\Statements\\ExplainStatement',
34
        'EXPLAIN' => 'PhpMyAdmin\\SqlParser\\Statements\\ExplainStatement',
35
        'FLUSH' => '',
36
        'GRANT' => '',
37
        'HELP' => '',
38
        'SET PASSWORD' => '',
39
        'STATUS' => '',
40
        'USE' => '',
41
42
        // Table Maintenance Statements
43
        // https://dev.mysql.com/doc/refman/5.7/en/table-maintenance-sql.html
44
        'ANALYZE' => 'PhpMyAdmin\\SqlParser\\Statements\\AnalyzeStatement',
45
        'BACKUP' => 'PhpMyAdmin\\SqlParser\\Statements\\BackupStatement',
46
        'CHECK' => 'PhpMyAdmin\\SqlParser\\Statements\\CheckStatement',
47
        'CHECKSUM' => 'PhpMyAdmin\\SqlParser\\Statements\\ChecksumStatement',
48
        'OPTIMIZE' => 'PhpMyAdmin\\SqlParser\\Statements\\OptimizeStatement',
49
        'REPAIR' => 'PhpMyAdmin\\SqlParser\\Statements\\RepairStatement',
50
        'RESTORE' => 'PhpMyAdmin\\SqlParser\\Statements\\RestoreStatement',
51
52
        // Database Administration Statements
53
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-server-administration.html
54
        'SET' => 'PhpMyAdmin\\SqlParser\\Statements\\SetStatement',
55
        'SHOW' => 'PhpMyAdmin\\SqlParser\\Statements\\ShowStatement',
56
57
        // Data Definition Statements.
58
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-definition.html
59
        'ALTER' => 'PhpMyAdmin\\SqlParser\\Statements\\AlterStatement',
60
        'CREATE' => 'PhpMyAdmin\\SqlParser\\Statements\\CreateStatement',
61
        'DROP' => 'PhpMyAdmin\\SqlParser\\Statements\\DropStatement',
62
        'RENAME' => 'PhpMyAdmin\\SqlParser\\Statements\\RenameStatement',
63
        'TRUNCATE' => 'PhpMyAdmin\\SqlParser\\Statements\\TruncateStatement',
64
65
        // Data Manipulation Statements.
66
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-manipulation.html
67
        'CALL' => 'PhpMyAdmin\\SqlParser\\Statements\\CallStatement',
68
        'DELETE' => 'PhpMyAdmin\\SqlParser\\Statements\\DeleteStatement',
69
        'DO' => '',
70
        'HANDLER' => '',
71
        'INSERT' => 'PhpMyAdmin\\SqlParser\\Statements\\InsertStatement',
72
        'LOAD' => '',
73
        'REPLACE' => 'PhpMyAdmin\\SqlParser\\Statements\\ReplaceStatement',
74
        'SELECT' => 'PhpMyAdmin\\SqlParser\\Statements\\SelectStatement',
75
        'UPDATE' => 'PhpMyAdmin\\SqlParser\\Statements\\UpdateStatement',
76
77
        // Prepared Statements.
78
        // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html
79
        'DEALLOCATE' => '',
80
        'EXECUTE' => '',
81
        'PREPARE' => '',
82
83
        // Transactional and Locking Statements
84
        // https://dev.mysql.com/doc/refman/5.7/en/commit.html
85
        'BEGIN' => 'PhpMyAdmin\\SqlParser\\Statements\\TransactionStatement',
86
        'COMMIT' => 'PhpMyAdmin\\SqlParser\\Statements\\TransactionStatement',
87
        'ROLLBACK' => 'PhpMyAdmin\\SqlParser\\Statements\\TransactionStatement',
88
        'START TRANSACTION' => 'PhpMyAdmin\\SqlParser\\Statements\\TransactionStatement',
89
    );
90
91
    /**
92
     * Array of classes that are used in parsing SQL components.
93
     *
94
     * @var array
95
     */
96
    public static $KEYWORD_PARSERS = array(
97
        // This is not a proper keyword and was added here to help the
98
        // formatter.
99
        'PARTITION BY' => array(),
100
        'SUBPARTITION BY' => array(),
101
102
        // This is not a proper keyword and was added here to help the
103
        // builder.
104
        '_OPTIONS' => array(
105
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\OptionsArray',
106
            'field' => 'options',
107
        ),
108
        '_END_OPTIONS' => array(
109
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\OptionsArray',
110
            'field' => 'end_options',
111
        ),
112
113
        'UNION' => array(
114
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\UnionKeyword',
115
            'field' => 'union',
116
        ),
117
        'UNION ALL' => array(
118
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\UnionKeyword',
119
            'field' => 'union',
120
        ),
121
        'UNION DISTINCT' => array(
122
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\UnionKeyword',
123
            'field' => 'union',
124
        ),
125
126
        // Actual clause parsers.
127
        'ALTER' => array(
128
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Expression',
129
            'field' => 'table',
130
            'options' => array('parseField' => 'table'),
131
        ),
132
        'ANALYZE' => array(
133
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
134
            'field' => 'tables',
135
            'options' => array('parseField' => 'table'),
136
        ),
137
        'BACKUP' => array(
138
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
139
            'field' => 'tables',
140
            'options' => array('parseField' => 'table'),
141
        ),
142
        'CALL' => array(
143
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\FunctionCall',
144
            'field' => 'call',
145
        ),
146
        'CHECK' => array(
147
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
148
            'field' => 'tables',
149
            'options' => array('parseField' => 'table'),
150
        ),
151
        'CHECKSUM' => array(
152
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
153
            'field' => 'tables',
154
            'options' => array('parseField' => 'table'),
155
        ),
156
        'CROSS JOIN' => array(
157
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
158
            'field' => 'join',
159
        ),
160
        'DROP' => array(
161
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
162
            'field' => 'fields',
163
            'options' => array('parseField' => 'table'),
164
        ),
165
        'FROM' => array(
166
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
167
            'field' => 'from',
168
            'options' => array('field' => 'table'),
169
        ),
170
        'GROUP BY' => array(
171
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\OrderKeyword',
172
            'field' => 'group',
173
        ),
174
        'HAVING' => array(
175
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Condition',
176
            'field' => 'having',
177
        ),
178
        'INTO' => array(
179
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\IntoKeyword',
180
            'field' => 'into',
181
        ),
182
        'JOIN' => array(
183
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
184
            'field' => 'join',
185
        ),
186
        'LEFT JOIN' => array(
187
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
188
            'field' => 'join',
189
        ),
190
        'LEFT OUTER JOIN' => array(
191
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
192
            'field' => 'join',
193
        ),
194
        'ON' => array(
195
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Expression',
196
            'field' => 'table',
197
            'options' => array('parseField' => 'table'),
198
        ),
199
        'RIGHT JOIN' => array(
200
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
201
            'field' => 'join',
202
        ),
203
        'RIGHT OUTER JOIN' => array(
204
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
205
            'field' => 'join',
206
        ),
207
        'INNER JOIN' => array(
208
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
209
            'field' => 'join',
210
        ),
211
        'FULL JOIN' => array(
212
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
213
            'field' => 'join',
214
        ),
215
        'FULL OUTER JOIN' => array(
216
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
217
            'field' => 'join',
218
        ),
219
        'NATURAL JOIN' => array(
220
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
221
            'field' => 'join',
222
        ),
223
        'NATURAL LEFT JOIN' => array(
224
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
225
            'field' => 'join',
226
        ),
227
        'NATURAL RIGHT JOIN' => array(
228
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
229
            'field' => 'join',
230
        ),
231
        'NATURAL LEFT OUTER JOIN' => array(
232
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
233
            'field' => 'join',
234
        ),
235
        'NATURAL RIGHT OUTER JOIN' => array(
236
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
237
            'field' => 'join',
238
        ),
239
        'LIMIT' => array(
240
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Limit',
241
            'field' => 'limit',
242
        ),
243
        'OPTIMIZE' => array(
244
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
245
            'field' => 'tables',
246
            'options' => array('parseField' => 'table'),
247
        ),
248
        'ORDER BY' => array(
249
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\OrderKeyword',
250
            'field' => 'order',
251
        ),
252
        'PARTITION' => array(
253
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ArrayObj',
254
            'field' => 'partition',
255
        ),
256
        'PROCEDURE' => array(
257
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\FunctionCall',
258
            'field' => 'procedure',
259
        ),
260
        'RENAME' => array(
261
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\RenameOperation',
262
            'field' => 'renames',
263
        ),
264
        'REPAIR' => array(
265
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
266
            'field' => 'tables',
267
            'options' => array('parseField' => 'table'),
268
        ),
269
        'RESTORE' => array(
270
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
271
            'field' => 'tables',
272
            'options' => array('parseField' => 'table'),
273
        ),
274
        'SET' => array(
275
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\SetOperation',
276
            'field' => 'set',
277
        ),
278
        'SELECT' => array(
279
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
280
            'field' => 'expr',
281
        ),
282
        'TRUNCATE' => array(
283
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Expression',
284
            'field' => 'table',
285
            'options' => array('parseField' => 'table'),
286
        ),
287
        'UPDATE' => array(
288
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\ExpressionArray',
289
            'field' => 'tables',
290
            'options' => array('parseField' => 'table'),
291
        ),
292
        'VALUE' => array(
293
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Array2d',
294
            'field' => 'values',
295
        ),
296
        'VALUES' => array(
297
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Array2d',
298
            'field' => 'values',
299
        ),
300
        'WHERE' => array(
301
            'class' => 'PhpMyAdmin\\SqlParser\\Components\\Condition',
302
            'field' => 'where',
303
        ),
304
    );
305
306
    /**
307
     * The list of tokens that are parsed.
308
     *
309
     * @var TokensList
310
     */
311
    public $list;
312
313
    /**
314
     * List of statements parsed.
315
     *
316
     * @var Statement[]
317
     */
318
    public $statements = array();
319
320
    /**
321
     * The number of opened brackets.
322
     *
323
     * @var int
324
     */
325
    public $brackets = 0;
326
327
    /**
328
     * Constructor.
329
     *
330
     * @param string|UtfString|TokensList $list   the list of tokens to be parsed
0 ignored issues
show
Documentation introduced by
Should the type for parameter $list not be string|UtfString|TokensList|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
331
     * @param bool                        $strict whether strict mode should be enabled or not
332
     */
333 309
    public function __construct($list = null, $strict = false)
334
    {
335 309
        if ((is_string($list)) || ($list instanceof UtfString)) {
336 87
            $lexer = new Lexer($list, $strict);
337 87
            $this->list = $lexer->list;
338
        } elseif ($list instanceof TokensList) {
339 160
            $this->list = $list;
340
        }
341
342 309
        $this->strict = $strict;
343
344 309
        if ($list !== null) {
345 247
            $this->parse();
346
        }
347 309
    }
348
349
    /**
350
     * Builds the parse trees.
351
     */
352 247
    public function parse()
353
    {
354
        /**
355
         * Last transaction.
356
         *
357
         * @var TransactionStatement
358
         */
359 247
        $lastTransaction = null;
360
361
        /**
362
         * Last parsed statement.
363
         *
364
         * @var Statement
365
         */
366 247
        $lastStatement = null;
367
368
        /**
369
         * Union's type or false for no union.
370
         *
371
         * @var bool|string
372
         */
373 247
        $unionType = false;
374
375
        /**
376
         * The index of the last token from the last statement.
377
         *
378
         * @var int
379
         */
380 247
        $prevLastIdx = -1;
381
382
        /**
383
         * The list of tokens.
384
         *
385
         * @var TokensList
386
         */
387 247
        $list = &$this->list;
388
389 247
        for (; $list->idx < $list->count; ++$list->idx) {
390
            /**
391
             * Token parsed at this moment.
392
             *
393
             * @var Token
394
             */
395 245
            $token = $list->tokens[$list->idx];
396
397
            // `DELIMITER` is not an actual statement and it requires
398
            // special handling.
399 245
            if (($token->type === Token::TYPE_NONE)
400 245
                && (strtoupper($token->token) === 'DELIMITER')
401
            ) {
402
                // Skipping to the end of this statement.
403 1
                $list->getNextOfType(Token::TYPE_DELIMITER);
404 1
                $prevLastIdx = $list->idx;
405 1
                continue;
406
            }
407
408
            // Counting the brackets around statements.
409 245
            if ($token->value === '(') {
410 5
                ++$this->brackets;
411 5
                continue;
412
            }
413
414
            // Statements can start with keywords only.
415
            // Comments, whitespaces, etc. are ignored.
416 245
            if ($token->type !== Token::TYPE_KEYWORD) {
417 200
                if (($token->type !== TOKEN::TYPE_COMMENT)
418 200
                    && ($token->type !== Token::TYPE_WHITESPACE)
419 200
                    && ($token->type !== Token::TYPE_OPERATOR) // `(` and `)`
420 200
                    && ($token->type !== Token::TYPE_DELIMITER)
421
                ) {
422 10
                    $this->error(
423 10
                        'Unexpected beginning of statement.',
424
                        $token
425
                    );
426
                }
427 200
                continue;
428
            }
429
430 244
            if (($token->keyword === 'UNION') || ($token->keyword === 'UNION ALL') || ($token->keyword === 'UNION DISTINCT')) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
431 5
                $unionType = $token->keyword;
432 5
                continue;
433
            }
434
435
            // Checking if it is a known statement that can be parsed.
436 244
            if (empty(static::$STATEMENT_PARSERS[$token->keyword])) {
437 14
                if (!isset(static::$STATEMENT_PARSERS[$token->keyword])) {
438
                    // A statement is considered recognized if the parser
439
                    // is aware that it is a statement, but it does not have
440
                    // a parser for it yet.
441 14
                    $this->error(
442 14
                        'Unrecognized statement type.',
443
                        $token
444
                    );
445
                }
446
                // Skipping to the end of this statement.
447 14
                $list->getNextOfType(Token::TYPE_DELIMITER);
448 14
                $prevLastIdx = $list->idx;
449 14
                continue;
450
            }
451
452
            /**
453
             * The name of the class that is used for parsing.
454
             *
455
             * @var string
456
             */
457 244
            $class = static::$STATEMENT_PARSERS[$token->keyword];
458
459
            /**
460
             * Processed statement.
461
             *
462
             * @var Statement
463
             */
464 244
            $statement = new $class($this, $this->list);
465
466
            // The first token that is a part of this token is the next token
467
            // unprocessed by the previous statement.
468
            // There might be brackets around statements and this shouldn't
469
            // affect the parser
470 244
            $statement->first = $prevLastIdx + 1;
471
472
            // Storing the index of the last token parsed and updating the old
473
            // index.
474 244
            $statement->last = $list->idx;
475 244
            $prevLastIdx = $list->idx;
476
477
            // Handles unions.
478 244
            if ((!empty($unionType))
479 244
                && ($lastStatement instanceof SelectStatement)
480 244
                && ($statement instanceof SelectStatement)
481
            ) {
482
                /*
483
                 * This SELECT statement.
484
                 *
485
                 * @var SelectStatement $statement
486
                 */
487
488
                /*
489
                 * Last SELECT statement.
490
                 *
491
                 * @var SelectStatement $lastStatement
492
                 */
493 5
                $lastStatement->union[] = array($unionType, $statement);
494
495
                // if there are no no delimiting brackets, the `ORDER` and
496
                // `LIMIT` keywords actually belong to the first statement.
497 5
                $lastStatement->order = $statement->order;
498 5
                $lastStatement->limit = $statement->limit;
499 5
                $statement->order = array();
500 5
                $statement->limit = null;
501
502
                // The statement actually ends where the last statement in
503
                // union ends.
504 5
                $lastStatement->last = $statement->last;
505
506 5
                $unionType = false;
507
508
                // Validate clause order
509 5
                $statement->validateClauseOrder($this, $list);
510 5
                continue;
511
            }
512
513
            // Handles transactions.
514 244
            if ($statement instanceof TransactionStatement) {
515
                /*
516
                 * @var TransactionStatement
517
                 */
518 5
                if ($statement->type === TransactionStatement::TYPE_BEGIN) {
519 4
                    $lastTransaction = $statement;
520 4
                    $this->statements[] = $statement;
521 5
                } elseif ($statement->type === TransactionStatement::TYPE_END) {
522 5 View Code Duplication
                    if ($lastTransaction === null) {
523
                        // Even though an error occurred, the query is being
524
                        // saved.
525 1
                        $this->statements[] = $statement;
526 1
                        $this->error(
527 1
                            'No transaction was previously started.',
528
                            $token
529
                        );
530
                    } else {
531 4
                        $lastTransaction->end = $statement;
532
                    }
533 5
                    $lastTransaction = null;
534
                }
535
536
                // Validate clause order
537 5
                $statement->validateClauseOrder($this, $list);
538 5
                continue;
539
            }
540
541
            // Validate clause order
542 243
            $statement->validateClauseOrder($this, $list);
543
544
            // Finally, storing the statement.
545 243 View Code Duplication
            if ($lastTransaction !== null) {
546 4
                $lastTransaction->statements[] = $statement;
547
            } else {
548 240
                $this->statements[] = $statement;
549
            }
550 243
            $lastStatement = $statement;
551
        }
552 247
    }
553
554
    /**
555
     * Creates a new error log.
556
     *
557
     * @param string $msg   the error message
558
     * @param Token  $token the token that produced the error
0 ignored issues
show
Documentation introduced by
Should the type for parameter $token not be null|Token?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
559
     * @param int    $code  the code of the error
560
     *
561
     * @throws ParserException throws the exception, if strict mode is enabled
562
     */
563 73
    public function error($msg, Token $token = null, $code = 0)
564
    {
565 73
        $error = new ParserException(
566 73
            Translator::gettext($msg),
567
            $token, $code
568
        );
569 73
        parent::error($error);
570 72
    }
571
}
572