Passed
Push — master ( 94652f...ff593e )
by William
03:47
created

Statement::before()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

427
    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...
428
    {
429
    }
430
431
    /**
432
     * Function called after the token was processed.
433
     *
434
     * @param Parser     $parser the instance that requests parsing
435
     * @param TokensList $list   the list of tokens to be parsed
436
     * @param Token      $token  the token that is being parsed
437
     *
438
     * @return void
439
     */
440
    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

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