Passed
Pull Request — master (#417)
by
unknown
03:34
created

Statement::parse()   D

Complexity

Conditions 46
Paths 30

Size

Total Lines 211
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 86
CRAP Score 46.0032

Importance

Changes 0
Metric Value
cc 46
eloc 92
nc 30
nop 2
dl 0
loc 211
ccs 86
cts 87
cp 0.9885
crap 46.0032
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 PhpMyAdmin\SqlParser\Components\FunctionCall;
8
use PhpMyAdmin\SqlParser\Components\OptionsArray;
9
use Stringable;
10
11
use function array_flip;
12
use function array_keys;
13
use function count;
14
use function in_array;
15
use function stripos;
16
use function trim;
17
18
/**
19
 * The result of the parser is an array of statements are extensions of the class defined here.
20
 *
21
 * A statement represents the result of parsing the lexemes.
22
 *
23
 * Abstract statement definition.
24
 */
25
#[\AllowDynamicProperties]
26
abstract class Statement implements Stringable
27
{
28
    /**
29
     * Options for this statement.
30
     *
31
     * The option would be the key and the value can be an integer or an array.
32
     *
33
     * The integer represents only the index used.
34
     *
35
     * The array may have two keys: `0` is used to represent the index used and
36
     * `1` is the type of the option (which may be 'var' or 'var='). Both
37
     * options mean they expect a value after the option (e.g. `A = B` or `A B`,
38
     * in which case `A` is the key and `B` is the value). The only difference
39
     * is in the building process. `var` options are built as `A B` and  `var=`
40
     * options are built as `A = B`
41
     *
42
     * Two options that can be used together must have different values for
43
     * indexes, else, when they will be used together, an error will occur.
44
     *
45
     * @var array<string, int|array<int, int|string>>
46
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
47
     */
48
    public static $OPTIONS = [];
49
50
    /**
51
     * The clauses of this statement, in order.
52
     *
53
     * The value attributed to each clause is used by the builder and it may
54
     * have one of the following values:
55
     *
56
     *     - 1 = 01 - add the clause only
57
     *     - 2 = 10 - add the keyword
58
     *     - 3 = 11 - add both the keyword and the clause
59
     *
60
     * @var array<string, array<int, int|string>>
61
     * @psalm-var array<string, array{non-empty-string, (1|2|3)}>
62
     */
63
    public static $CLAUSES = [];
64
65
    /**
66
     * @var array<string, int|array<int, int|string>>
67
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
68
     */
69
    public static $END_OPTIONS = [];
70
71
    /**
72
     * The options of this query.
73
     *
74
     * @see static::$OPTIONS
75
     *
76
     * @var OptionsArray|null
77
     */
78
    public $options;
79
80
    /**
81
     * The index of the first token used in this statement.
82
     *
83
     * @var int|null
84
     */
85
    public $first;
86
87
    /**
88
     * The index of the last token used in this statement.
89
     *
90
     * @var int|null
91
     */
92
    public $last;
93
94
    /**
95
     * @param Parser|null     $parser the instance that requests parsing
96
     * @param TokensList|null $list   the list of tokens to be parsed
97
     */
98 1880
    public function __construct(?Parser $parser = null, ?TokensList $list = null)
99
    {
100 1880
        if (($parser === null) || ($list === null)) {
101 12
            return;
102
        }
103
104 1872
        $this->parse($parser, $list);
105
    }
106
107
    /**
108
     * Builds the string representation of this statement.
109
     *
110
     * @return string
111
     */
112 128
    public function build()
113
    {
114
        /**
115
         * Query to be returned.
116
         *
117
         * @var string
118
         */
119 128
        $query = '';
120
121
        /**
122
         * Clauses which were built already.
123
         *
124
         * It is required to keep track of built clauses because some fields,
125
         * for example `join` is used by multiple clauses (`JOIN`, `LEFT JOIN`,
126
         * `LEFT OUTER JOIN`, etc.). The same happens for `VALUE` and `VALUES`.
127
         *
128
         * A clause is considered built just after fields' value
129
         * (`$this->field`) was used in building.
130
         */
131 128
        $built = [];
132
133
        /**
134
         * Statement's clauses.
135
         */
136 128
        $clauses = $this->getClauses();
137
138 128
        foreach ($clauses as $clause) {
139
            /**
140
             * The name of the clause.
141
             *
142
             * @var string
143
             */
144 128
            $name = $clause[0];
145
146
            /**
147
             * The type of the clause.
148
             *
149
             * @see self::$CLAUSES
150
             *
151
             * @var int
152
             */
153 128
            $type = $clause[1];
154
155
            /**
156
             * The builder (parser) of this clause.
157
             *
158
             * @var Component
159
             */
160 128
            $class = Parser::$KEYWORD_PARSERS[$name]['class'];
161
162
            /**
163
             * The name of the field that is used as source for the builder.
164
             * Same field is used to store the result of parsing.
165
             *
166
             * @var string
167
             */
168 128
            $field = Parser::$KEYWORD_PARSERS[$name]['field'];
169
170
            // The field is empty, there is nothing to be built.
171 128
            if (empty($this->$field)) {
172 128
                continue;
173
            }
174
175
            // Checking if this field was already built.
176 128
            if ($type & 1) {
177 128
                if (! empty($built[$field])) {
178 52
                    continue;
179
                }
180
181 128
                $built[$field] = true;
182
            }
183
184
            // Checking if the name of the clause should be added.
185 128
            if ($type & 2) {
186 128
                $query = trim($query) . ' ' . $name;
187
            }
188
189
            // Checking if the result of the builder should be added.
190 128
            if (! ($type & 1)) {
191 128
                continue;
192
            }
193
194 128
            $query = trim($query) . ' ' . $class::build($this->$field);
195
        }
196
197 128
        return $query;
198
    }
199
200
    /**
201
     * Parses the statements defined by the tokens list.
202
     *
203
     * @param Parser     $parser the instance that requests parsing
204
     * @param TokensList $list   the list of tokens to be parsed
205
     *
206
     * @return void
207
     *
208
     * @throws Exceptions\ParserException
209
     */
210 932
    public function parse(Parser $parser, TokensList $list)
211
    {
212
        /**
213
         * Array containing all list of clauses parsed.
214
         * This is used to check for duplicates.
215
         */
216 932
        $parsedClauses = [];
217
218
        // This may be corrected by the parser.
219 932
        $this->first = $list->idx;
220
221
        /**
222
         * Whether options were parsed or not.
223
         * For statements that do not have any options this is set to `true` by
224
         * default.
225
         */
226 932
        $parsedOptions = empty(static::$OPTIONS);
227
228 932
        for (; $list->idx < $list->count; ++$list->idx) {
229
            /**
230
             * Token parsed at this moment.
231
             */
232 932
            $token = $list->tokens[$list->idx];
233
234
            // End of statement.
235 932
            if ($token->type === Token::TYPE_DELIMITER) {
236 888
                break;
237
            }
238
239
            // Checking if this closing bracket is the pair for a bracket
240
            // outside the statement.
241 932
            if (($token->value === ')') && ($parser->brackets > 0)) {
242 28
                --$parser->brackets;
243 28
                continue;
244
            }
245
246
            // Only keywords are relevant here. Other parts of the query are
247
            // processed in the functions below.
248 932
            if ($token->type !== Token::TYPE_KEYWORD) {
249 72
                if (($token->type !== Token::TYPE_COMMENT) && ($token->type !== Token::TYPE_WHITESPACE)) {
250 40
                    $parser->error('Unexpected token.', $token);
251
                }
252
253 72
                continue;
254
            }
255
256
            // Unions are parsed by the parser because they represent more than
257
            // one statement.
258
            if (
259 932
                ($token->keyword === 'UNION') ||
260 932
                ($token->keyword === 'UNION ALL') ||
261 932
                ($token->keyword === 'UNION DISTINCT') ||
262 932
                ($token->keyword === 'EXCEPT') ||
263 932
                ($token->keyword === 'INTERSECT')
264
            ) {
265 80
                break;
266
            }
267
268 932
            $lastIdx = $list->idx;
269
270
            // ON DUPLICATE KEY UPDATE ...
271
            // has to be parsed in parent statement (INSERT or REPLACE)
272
            // so look for it and break
273 932
            if ($this instanceof Statements\SelectStatement && $token->value === 'ON') {
274 12
                ++$list->idx; // Skip ON
275
276
                // look for ON DUPLICATE KEY UPDATE
277 12
                $first = $list->getNextOfType(Token::TYPE_KEYWORD);
278 12
                $second = $list->getNextOfType(Token::TYPE_KEYWORD);
279 12
                $third = $list->getNextOfType(Token::TYPE_KEYWORD);
280
281
                if (
282 12
                    $first && $second && $third
283 12
                    && $first->value === 'DUPLICATE'
284 12
                    && $second->value === 'KEY'
285 12
                    && $third->value === 'UPDATE'
286
                ) {
287 12
                    $list->idx = $lastIdx;
288 12
                    break;
289
                }
290
            }
291
292 932
            $list->idx = $lastIdx;
293
294
            /**
295
             * The name of the class that is used for parsing.
296
             *
297
             * @var Component
298
             */
299 932
            $class = null;
300
301
            /**
302
             * The name of the field where the result of the parsing is stored.
303
             *
304
             * @var string
305
             */
306 932
            $field = null;
307
308
            /**
309
             * Parser's options.
310
             */
311 932
            $options = [];
312
313
            // Looking for duplicated clauses.
314
            if (
315 932
                ! empty(Parser::$KEYWORD_PARSERS[$token->value])
316 932
                || ! empty(Parser::$STATEMENT_PARSERS[$token->value])
317
            ) {
318 932
                if (! empty($parsedClauses[$token->value])) {
319 32
                    $parser->error('This type of clause was previously parsed.', $token);
320 32
                    break;
321
                }
322
323 932
                $parsedClauses[$token->value] = true;
324
            }
325
326
            // Checking if this is the beginning of a clause.
327
            // Fix Issue #221: As `truncate` is not a keyword
328
            // but it might be the beginning of a statement of truncate,
329
            // so let the value use the keyword field for truncate type.
330 932
            $tokenValue = in_array($token->keyword, ['TRUNCATE']) ? $token->keyword : $token->value;
331 932
            if (! empty(Parser::$KEYWORD_PARSERS[$tokenValue]) && $list->idx < $list->count) {
332 928
                $class = Parser::$KEYWORD_PARSERS[$tokenValue]['class'];
333 928
                $field = Parser::$KEYWORD_PARSERS[$tokenValue]['field'];
334 928
                if (! empty(Parser::$KEYWORD_PARSERS[$tokenValue]['options'])) {
335 728
                    $options = Parser::$KEYWORD_PARSERS[$tokenValue]['options'];
336
                }
337
            }
338
339
            // Checking if this is the beginning of the statement.
340 932
            if (! empty(Parser::$STATEMENT_PARSERS[$token->keyword])) {
341
                if (
342 932
                    ! empty(static::$CLAUSES) // Undefined for some statements.
343 932
                    && empty(static::$CLAUSES[$token->value])
344
                ) {
345
                    // Some keywords (e.g. `SET`) may be the beginning of a
346
                    // statement and a clause.
347
                    // If such keyword was found and it cannot be a clause of
348
                    // this statement it means it is a new statement, but no
349
                    // delimiter was found between them.
350 12
                    $parser->error(
351 12
                        'A new statement was found, but no delimiter between it and the previous one.',
352 12
                        $token
353 12
                    );
354 12
                    break;
355
                }
356
357 932
                if (! $parsedOptions) {
358 876
                    if (empty(static::$OPTIONS[$token->value])) {
359
                        // Skipping keyword because if it is not a option.
360 872
                        ++$list->idx;
361
                    }
362
363 876
                    $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
364 932
                    $parsedOptions = true;
365
                }
366 644
            } elseif ($class === null) {
367 60
                if ($this instanceof Statements\SelectStatement && $token->value === 'WITH ROLLUP') {
368
                    // Handle group options in Select statement
369
                    // See Statements\SelectStatement::$GROUP_OPTIONS
370
                    $this->group_options = OptionsArray::parse($parser, $list, static::$GROUP_OPTIONS);
371
                } elseif (
372 60
                    $this instanceof Statements\SelectStatement
373 60
                    && ($token->value === 'FOR UPDATE'
374 60
                        || $token->value === 'LOCK IN SHARE MODE')
375
                ) {
376
                    // Handle special end options in Select statement
377
                    // See Statements\SelectStatement::$END_OPTIONS
378 16
                    $this->end_options = OptionsArray::parse($parser, $list, static::$END_OPTIONS);
379
                } elseif (
380 44
                    $this instanceof Statements\SetStatement
381 44
                    && ($token->value === 'COLLATE'
382 44
                        || $token->value === 'DEFAULT')
383
                ) {
384
                    // Handle special end options in SET statement
385
                    // See Statements\SetStatement::$END_OPTIONS
386 4
                    $this->end_options = OptionsArray::parse($parser, $list, static::$END_OPTIONS);
387
                } else {
388
                    // There is no parser for this keyword and isn't the beginning
389
                    // of a statement (so no options) either.
390 40
                    $parser->error('Unrecognized keyword.', $token);
391 40
                    continue;
392
                }
393
            }
394
395 932
            $this->before($parser, $list, $token);
396
397
            // Parsing this keyword.
398 932
            if ($class !== null) {
399
                // We can't parse keyword at the end of statement
400 928
                if ($list->idx >= $list->count) {
401 4
                    $parser->error('Keyword at end of statement.', $token);
402 4
                    continue;
403
                }
404
405 924
                ++$list->idx; // Skipping keyword or last option.
406 924
                $this->$field = $class::parse($parser, $list, $options);
407
            }
408
409 928
            $this->after($parser, $list, $token);
410
411
            // #223 Here may make a patch, if last is delimiter, back one
412 928
            if ($class !== FunctionCall::class || $list->offsetGet($list->idx)->type !== Token::TYPE_DELIMITER) {
413 924
                continue;
414
            }
415
416 4
            --$list->idx;
417
        }
418
419
        // This may be corrected by the parser.
420 932
        $this->last = --$list->idx; // Go back to last used token.
421
    }
422
423
    /**
424
     * Function called before the token is processed.
425
     *
426
     * @param Parser     $parser the instance that requests parsing
427
     * @param TokensList $list   the list of tokens to be parsed
428
     * @param Token      $token  the token that is being parsed
429
     *
430
     * @return void
431
     */
432 896
    public function before(Parser $parser, TokensList $list, Token $token)
0 ignored issues
show
Unused Code introduced by
The parameter $token is not used and could be removed. ( Ignorable by Annotation )

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

432
    public function before(Parser $parser, TokensList $list, /** @scrutinizer ignore-unused */ Token $token)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
433
    {
434 896
    }
435
436
    /**
437
     * Function called after the token was processed.
438
     *
439
     * @param Parser     $parser the instance that requests parsing
440
     * @param TokensList $list   the list of tokens to be parsed
441
     * @param Token      $token  the token that is being parsed
442
     *
443
     * @return void
444
     */
445 912
    public function after(Parser $parser, TokensList $list, Token $token)
0 ignored issues
show
Unused Code introduced by
The parameter $token is not used and could be removed. ( Ignorable by Annotation )

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

445
    public function after(Parser $parser, TokensList $list, /** @scrutinizer ignore-unused */ Token $token)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
446
    {
447 912
    }
448
449
    /**
450
     * Gets the clauses of this statement.
451
     *
452
     * @return array<string, array<int, int|string>>
453
     * @psalm-return array<string, array{non-empty-string, (1|2|3)}>
454
     */
455 1344
    public function getClauses()
456
    {
457 1344
        return static::$CLAUSES;
458
    }
459
460
    /**
461
     * Builds the string representation of this statement.
462
     *
463
     * @see static::build
464
     *
465
     * @return string
466
     */
467 32
    public function __toString()
468
    {
469 32
        return $this->build();
470
    }
471
472
    /**
473
     * Validates the order of the clauses in parsed statement
474
     * Ideally this should be called after successfully
475
     * completing the parsing of each statement.
476
     *
477
     * @param Parser     $parser the instance that requests parsing
478
     * @param TokensList $list   the list of tokens to be parsed
479
     *
480
     * @return bool
481
     *
482
     * @throws Exceptions\ParserException
483
     */
484 1872
    public function validateClauseOrder($parser, $list)
485
    {
486 1872
        $clauses = array_flip(array_keys($this->getClauses()));
487
488 1872
        if (empty($clauses) || count($clauses) === 0) {
489 1096
            return true;
490
        }
491
492 888
        $minIdx = -1;
493
494
        /**
495
         * For tracking JOIN clauses in a query
496
         *   = 0 - JOIN not found till now
497
         *   > 0 - Index of first JOIN clause in the statement.
498
         *
499
         * @var int
500
         */
501 888
        $minJoin = 0;
502
503
        /**
504
         * For tracking JOIN clauses in a query
505
         *   = 0 - JOIN not found till now
506
         *   > 0 - Index of last JOIN clause
507
         *         (which appears together with other JOINs)
508
         *         in the statement.
509
         *
510
         * @var int
511
         */
512 888
        $maxJoin = 0;
513
514 888
        $error = 0;
515 888
        $lastIdx = 0;
516 888
        foreach ($clauses as $clauseType => $index) {
517 888
            $clauseStartIdx = Utils\Query::getClauseStartOffset($this, $list, $clauseType);
518
519
            if (
520 888
                $clauseStartIdx !== -1
521 888
                && $this instanceof Statements\SelectStatement
522 888
                && ($clauseType === 'FORCE'
523 888
                    || $clauseType === 'IGNORE'
524 888
                    || $clauseType === 'USE')
525
            ) {
526
                // TODO: ordering of clauses in a SELECT statement with
527
                // Index hints is not supported
528 28
                return true;
529
            }
530
531
            // Handle ordering of Multiple Joins in a query
532 888
            if ($clauseStartIdx !== -1) {
533 872
                if ($minJoin === 0 && stripos($clauseType, 'JOIN')) {
534
                    // First JOIN clause is detected
535 88
                    $minJoin = $maxJoin = $clauseStartIdx;
536 872
                } elseif ($minJoin !== 0 && ! stripos($clauseType, 'JOIN')) {
537
                    // After a previous JOIN clause, a non-JOIN clause has been detected
538 48
                    $maxJoin = $lastIdx;
539 872
                } elseif ($maxJoin < $clauseStartIdx && stripos($clauseType, 'JOIN')) {
540 4
                    $error = 1;
541
                }
542
            }
543
544 888
            if ($clauseStartIdx !== -1 && $clauseStartIdx < $minIdx) {
545 24
                if ($minJoin === 0 || $error === 1) {
546 20
                    $token = $list->tokens[$clauseStartIdx];
547 20
                    $parser->error('Unexpected ordering of clauses.', $token);
548
549 20
                    return false;
550
                }
551
552 4
                $minIdx = $clauseStartIdx;
553 888
            } elseif ($clauseStartIdx !== -1) {
554 872
                $minIdx = $clauseStartIdx;
555
            }
556
557 888
            $lastIdx = $clauseStartIdx !== -1 ? $clauseStartIdx : $lastIdx;
558
        }
559
560 844
        return true;
561
    }
562
}
563