Passed
Pull Request — master (#485)
by
unknown
14:36
created

AlterOperation::parse()   F

Complexity

Conditions 55
Paths 36

Size

Total Lines 221
Code Lines 113

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 110
CRAP Score 55.0173

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 55
eloc 113
c 1
b 0
f 0
nc 36
nop 3
dl 0
loc 221
ccs 110
cts 112
cp 0.9821
crap 55.0173
rs 3.3333

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\Components;
6
7
use PhpMyAdmin\SqlParser\Component;
8
use PhpMyAdmin\SqlParser\Parser;
9
use PhpMyAdmin\SqlParser\Token;
10
use PhpMyAdmin\SqlParser\TokensList;
11
12
use function array_key_exists;
13
use function in_array;
14
use function is_numeric;
15
use function is_string;
16
use function trim;
17
18
/**
19
 * Parses an alter operation.
20
 */
21
final class AlterOperation implements Component
22
{
23
    /**
24
     * All database options.
25
     *
26
     * @var array<string, int|array<int, int|string>>
27
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
28
     */
29
    public static $databaseOptions = [
30
        'CHARACTER SET' => [
31
            1,
32
            'var',
33
        ],
34
        'CHARSET' => [
35
            1,
36
            'var',
37
        ],
38
        'DEFAULT CHARACTER SET' => [
39
            1,
40
            'var',
41
        ],
42
        'DEFAULT CHARSET' => [
43
            1,
44
            'var',
45
        ],
46
        'UPGRADE' => [
47
            1,
48
            'var',
49
        ],
50
        'COLLATE' => [
51
            2,
52
            'var',
53
        ],
54
        'DEFAULT COLLATE' => [
55
            2,
56
            'var',
57
        ],
58
    ];
59
60
    /**
61
     * All table options.
62
     *
63
     * @var array<string, int|array<int, int|string>>
64
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
65
     */
66
    public static $tableOptions = [
67
        'ENGINE' => [
68
            1,
69
            'var=',
70
        ],
71
        'ALGORITHM' => [
72
            1,
73
            'var=',
74
        ],
75
        'AUTO_INCREMENT' => [
76
            1,
77
            'var=',
78
        ],
79
        'AVG_ROW_LENGTH' => [
80
            1,
81
            'var',
82
        ],
83
        'COALESCE PARTITION' => [
84
            1,
85
            'var',
86
        ],
87
        'LOCK' => [
88
            1,
89
            'var=',
90
        ],
91
        'MAX_ROWS' => [
92
            1,
93
            'var',
94
        ],
95
        'ROW_FORMAT' => [
96
            1,
97
            'var',
98
        ],
99
        'COMMENT' => [
100
            1,
101
            'var',
102
        ],
103
        'ADD' => 1,
104
        'ALTER' => 1,
105
        'ANALYZE' => 1,
106
        'CHANGE' => 1,
107
        'CHARSET' => 1,
108
        'CHECK' => 1,
109
        'CONVERT' => 1,
110
        'DEFAULT CHARSET' => 1,
111
        'DISABLE' => 1,
112
        'DISCARD' => 1,
113
        'DROP' => 1,
114
        'ENABLE' => 1,
115
        'IMPORT' => 1,
116
        'MODIFY' => 1,
117
        'OPTIMIZE' => 1,
118
        'ORDER' => 1,
119
        'REBUILD' => 1,
120
        'REMOVE' => 1,
121
        'RENAME' => 1,
122
        'REORGANIZE' => 1,
123
        'REPAIR' => 1,
124
        'UPGRADE' => 1,
125
126
        'COLUMN' => 2,
127
        'CONSTRAINT' => 2,
128
        'DEFAULT' => 2,
129
        'BY' => 2,
130
        'FOREIGN' => 2,
131
        'FULLTEXT' => 2,
132
        'KEY' => 2,
133
        'KEYS' => 2,
134
        'PARTITION' => 2,
135
        'PARTITION BY' => 2,
136
        'PARTITIONING' => 2,
137
        'PRIMARY KEY' => 2,
138
        'SPATIAL' => 2,
139
        'TABLESPACE' => 2,
140
        'INDEX' => [
141
            2,
142
            'var',
143
        ],
144
145
        'CHARACTER SET' => 3,
146
        'TO' => [
147
            3,
148
            'var',
149
        ],
150
    ];
151
152
    /**
153
     * All user options.
154
     *
155
     * @var array<string, int|array<int, int|string>>
156
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
157
     */
158
    public static $userOptions = [
159
        'ATTRIBUTE' => [
160
            1,
161
            'var',
162
        ],
163
        'COMMENT' => [
164
            1,
165
            'var',
166
        ],
167
        'REQUIRE' => [
168
            1,
169
            'var',
170
        ],
171
172
        'IDENTIFIED VIA' => [
173
            2,
174
            'var',
175
        ],
176
        'IDENTIFIED WITH' => [
177
            2,
178
            'var',
179
        ],
180
        'PASSWORD' => [
181
            2,
182
            'var',
183
        ],
184
        'WITH' => [
185
            2,
186
            'var',
187
        ],
188
189
        'BY' => [
190
            4,
191
            'expr',
192
        ],
193
194
        'ACCOUNT' => 1,
195
        'DEFAULT' => 1,
196
197
        'LOCK' => 2,
198
        'UNLOCK' => 2,
199
200
        'IDENTIFIED' => 3,
201
    ];
202
203
    /**
204
     * All view options.
205
     *
206
     * @var array<string, int|array<int, int|string>>
207
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
208
     */
209
    public static $viewOptions = ['AS' => 1];
210
211
    /**
212
     * All event options.
213
     *
214
     * @var array<string, int|array<int, int|string>>
215
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
216
     */
217
    public static $eventOptions = [
218
        'ON SCHEDULE' => 1,
219
        'EVERY' => [
220
            2,
221
            'expr',
222
        ],
223
        'AT' => [
224
            2,
225
            'expr',
226
        ],
227
        'STARTS' => [
228
            3,
229
            'expr',
230
        ],
231
        'ENDS' => [
232
            4,
233
            'expr',
234
        ],
235
        'ON COMPLETION PRESERVE' => 5,
236
        'ON COMPLETION NOT PRESERVE' => 5,
237
        'RENAME' => 6,
238
        'TO' => [
239
            7,
240
            'expr',
241
            ['parseField' => 'table'],
242
        ],
243
        'ENABLE' => 8,
244
        'DISABLE' => 8,
245
        'DISABLE ON SLAVE' => 8,
246
        'COMMENT' => [
247
            9,
248
            'var',
249
        ],
250
        'DO' => 10,
251
    ];
252
253
    /**
254
     * Options of this operation.
255
     *
256
     * @var OptionsArray
257
     */
258
    public $options;
259
260
    /**
261
     * The altered field.
262
     *
263
     * @var Expression|string|null
264
     */
265
    public $field;
266
267
    /**
268
     * The partitions.
269
     *
270
     * @var Component[]|ArrayObj|null
271
     */
272
    public $partitions;
273
274
    /**
275
     * Unparsed tokens.
276
     *
277
     * @var Token[]|string
278
     */
279
    public $unknown = [];
280
281
    /**
282
     * @param OptionsArray              $options    options of alter operation
283
     * @param Expression|string|null    $field      altered field
284
     * @param Component[]|ArrayObj|null $partitions partitions definition found in the operation
285
     * @param Token[]                   $unknown    unparsed tokens found at the end of operation
286
     */
287 174
    public function __construct(
288
        $options = null,
289
        $field = null,
290
        $partitions = null,
291
        $unknown = []
292
    ) {
293 174
        $this->partitions = $partitions;
294 174
        $this->options = $options;
295 174
        $this->field = $field;
296 174
        $this->unknown = $unknown;
297
    }
298
299
    /**
300
     * @param Parser               $parser  the parser that serves as context
301
     * @param TokensList           $list    the list of tokens that are being parsed
302
     * @param array<string, mixed> $options parameters for parsing
303
     *
304
     * @return AlterOperation
305
     */
306 174
    public static function parse(Parser $parser, TokensList $list, array $options = [])
307
    {
308 174
        $ret = new static();
309
310
        /**
311
         * Counts brackets.
312
         *
313
         * @var int
314
         */
315 174
        $brackets = 0;
316
317
        /**
318
         * The state of the parser.
319
         *
320
         * Below are the states of the parser.
321
         *
322
         *      0 ---------------------[ options ]---------------------> 1
323
         *
324
         *      1 ----------------------[ field ]----------------------> 2
325
         *
326
         *      1 -------------[ PARTITION / PARTITION BY ]------------> 3
327
         *
328
         *      2 -------------------------[ , ]-----------------------> 0
329
         *
330
         * @var int
331
         */
332 174
        $state = 0;
333
334
        /**
335
         * partition state.
336
         *
337
         * @var int
338
         */
339 174
        $partitionState = 0;
340
341 174
        for (; $list->idx < $list->count; ++$list->idx) {
342
            /**
343
             * Token parsed at this moment.
344
             */
345 174
            $token = $list->tokens[$list->idx];
346
347
            // End of statement.
348 174
            if ($token->type === Token::TYPE_DELIMITER) {
349 158
                break;
350
            }
351
352
            // Skipping comments.
353 174
            if ($token->type === Token::TYPE_COMMENT) {
354 2
                continue;
355
            }
356
357
            // Skipping whitespaces.
358 174
            if ($token->type === Token::TYPE_WHITESPACE) {
359 64
                if ($state === 2) {
360
                    // When parsing the unknown part, the whitespaces are
361
                    // included to not break anything.
362 56
                    $ret->unknown[] = $token;
363 56
                    continue;
364
                }
365
            }
366
367 174
            if ($state === 0) {
368 174
                $ret->options = OptionsArray::parse($parser, $list, $options);
369
370
                // Not only when aliasing but also when parsing the body of an event, we just list the tokens of the
371
                // body in the unknown tokens list, as they define their own statements.
372 174
                if ($ret->options->has('AS') || $ret->options->has('DO')) {
373 6
                    for (; $list->idx < $list->count; ++$list->idx) {
374 6
                        if ($list->tokens[$list->idx]->type === Token::TYPE_DELIMITER) {
375 6
                            break;
376
                        }
377
378 6
                        $ret->unknown[] = $list->tokens[$list->idx];
379
                    }
380
381 6
                    break;
382
                }
383
384 168
                $state = 1;
385 168
                if ($ret->options->has('PARTITION') || $token->value === 'PARTITION BY') {
386 8
                    $state = 3;
387 168
                    $list->getPrevious(); // in order to check whether it's partition or partition by.
388
                }
389 110
            } elseif ($state === 1) {
390 102
                $ret->field = Expression::parse(
391 102
                    $parser,
392 102
                    $list,
393 102
                    [
394 102
                        'breakOnAlias' => true,
395 102
                        'parseField' => 'column',
396 102
                    ]
397 102
                );
398 102
                if ($ret->field === null) {
399
                    // No field was read. We go back one token so the next
400
                    // iteration will parse the same token, but in state 2.
401 44
                    --$list->idx;
402
                }
403
404
                // If the operation is a RENAME COLUMN, now we have detected the field to rename, we need to parse
405
                // again the options to get the new name of the column.
406 102
                if ($ret->options->has('RENAME') && $ret->options->has('COLUMN')) {
407 12
                    $nextOptions = OptionsArray::parse($parser, $list, $options);
408 12
                    $ret->options->merge($nextOptions);
409
                }
410
411 102
                $state = 2;
412 96
            } elseif ($state === 2) {
413 88
                if (is_string($token->value) || is_numeric($token->value)) {
414 88
                    $arrayKey = $token->value;
415
                } else {
416 2
                    $arrayKey = $token->token;
417
                }
418
419 88
                if ($token->type === Token::TYPE_OPERATOR) {
420 62
                    if ($token->value === '(') {
421 42
                        ++$brackets;
422 62
                    } elseif ($token->value === ')') {
423 42
                        --$brackets;
424 44
                    } elseif (($token->value === ',') && ($brackets === 0)) {
425 62
                        break;
426
                    }
427 76
                } elseif (! self::checkIfTokenQuotedSymbol($token)) {
428 68
                    if (! empty(Parser::$statementParsers[$token->value])) {
429 12
                        $list->idx++; // Ignore the current token
430 12
                        $nextToken = $list->getNext();
431
432 12
                        if ($token->type === Token::TYPE_STRING) {
433
                            // To avoid string options, like in ENUMs, to be interpreted as statements when they
434
                            // actually are just strings.
435
                            $ret->unknown[] = $token;
436
                            continue;
437
                        }
438
439 12
                        if ($token->value === 'SET' && $nextToken !== null && $nextToken->value === '(') {
440
                            // To avoid adding the tokens between the SET() parentheses to the unknown tokens
441 4
                            $list->getNextOfTypeAndValue(Token::TYPE_OPERATOR, ')');
442 8
                        } elseif ($token->value === 'SET' && $nextToken !== null && $nextToken->value === 'DEFAULT') {
443
                            // to avoid adding the `DEFAULT` token to the unknown tokens.
444 4
                            ++$list->idx;
445
                        } else {
446
                            // We have reached the end of ALTER operation and suddenly found
447
                            // a start to new statement, but have not find a delimiter between them
448 4
                            $parser->error(
449 4
                                'A new statement was found, but no delimiter between it and the previous one.',
450 4
                                $token
451 4
                            );
452 12
                            break;
453
                        }
454
                    } elseif (
455 62
                        (array_key_exists($arrayKey, self::$databaseOptions)
456 62
                        || array_key_exists($arrayKey, self::$tableOptions))
457 62
                        && ! self::checkIfColumnDefinitionKeyword($arrayKey)
458
                    ) {
459
                        // This alter operation has finished, which means a comma
460
                        // was missing before start of new alter operation
461 4
                        $parser->error('Missing comma before start of a new alter operation.', $token);
462 4
                        break;
463
                    }
464
                }
465
466 74
                $ret->unknown[] = $token;
467 8
            } elseif ($state === 3) {
468 8
                if ($partitionState === 0) {
469 8
                    $list->idx++; // Ignore the current token
470 8
                    $nextToken = $list->getNext();
471
                    if (
472 8
                        ($token->type === Token::TYPE_KEYWORD)
473 8
                        && (($token->keyword === 'PARTITION BY')
474 8
                        || ($token->keyword === 'PARTITION' && $nextToken && $nextToken->value !== '('))
475
                    ) {
476 8
                        $partitionState = 1;
477 2
                    } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITION')) {
478 2
                        $partitionState = 2;
479
                    }
480
481 8
                    --$list->idx; // to decrease the idx by one, because the last getNext returned and increased it.
482
483
                    // reverting the effect of the getNext
484 8
                    $list->getPrevious();
485 8
                    $list->getPrevious();
486
487 8
                    ++$list->idx; // to index the idx by one, because the last getPrevious returned and decreased it.
488 8
                } elseif ($partitionState === 1) {
489
                    // Fetch the next token in a way the current index is reset to manage whitespaces in "field".
490 8
                    $currIdx = $list->idx;
491 8
                    ++$list->idx;
492 8
                    $nextToken = $list->getNext();
493 8
                    $list->idx = $currIdx;
494
                    // Building the expression used for partitioning.
495 8
                    if (empty($ret->field)) {
496 8
                        $ret->field = '';
497
                    }
498
499
                    if (
500 8
                        $token->type === Token::TYPE_OPERATOR
501 8
                        && $token->value === '('
502
                        && $nextToken
503 8
                        && $nextToken->keyword === 'PARTITION'
504
                    ) {
505 6
                        $partitionState = 2;
506 6
                        --$list->idx; // Current idx is on "(". We need a step back for ArrayObj::parse incoming.
507
                    } else {
508 8
                        $ret->field .= $token->type === Token::TYPE_WHITESPACE ? ' ' : $token->token;
509
                    }
510 6
                } elseif ($partitionState === 2) {
511 6
                    $ret->partitions = ArrayObj::parse(
512 6
                        $parser,
513 6
                        $list,
514 6
                        ['type' => PartitionDefinition::class]
515 6
                    );
516
                }
517
            }
518
        }
519
520 174
        if ($ret->options->isEmpty()) {
521 2
            $parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
522
        }
523
524 174
        --$list->idx;
525
526 174
        return $ret;
527
    }
528
529
    /**
530
     * @param AlterOperation $component the component to be built
531
     */
532 24
    public static function build($component): string
533
    {
534
        // Specific case of RENAME COLUMN that insert the field between 2 options.
535 24
        $afterFieldsOptions = new OptionsArray();
536 24
        if ($component->options->has('RENAME') && $component->options->has('COLUMN')) {
537 8
            $afterFieldsOptions = clone $component->options;
538 8
            $afterFieldsOptions->remove('RENAME');
539 8
            $afterFieldsOptions->remove('COLUMN');
540 8
            $component->options->remove('TO');
541
        }
542
543 24
        $ret = $component->options . ' ';
544 24
        if (isset($component->field) && ($component->field !== '')) {
545 18
            $ret .= $component->field . ' ';
546
        }
547
548 24
        $ret .= $afterFieldsOptions . TokensList::build($component->unknown);
549
550 24
        if (isset($component->partitions)) {
551 2
            $ret .= PartitionDefinition::build($component->partitions);
0 ignored issues
show
Bug introduced by
It seems like $component->partitions can also be of type PhpMyAdmin\SqlParser\Components\ArrayObj; however, parameter $component of PhpMyAdmin\SqlParser\Com...tionDefinition::build() does only seem to accept PhpMyAdmin\SqlParser\Com...nts\PartitionDefinition, maybe add an additional type check? ( Ignorable by Annotation )

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

551
            $ret .= PartitionDefinition::build(/** @scrutinizer ignore-type */ $component->partitions);
Loading history...
552
        }
553
554 24
        return trim($ret);
555
    }
556
557
    /**
558
     * Check if token's value is one of the common keywords
559
     * between column and table alteration
560
     *
561
     * @param string $tokenValue Value of current token
562
     */
563 32
    private static function checkIfColumnDefinitionKeyword($tokenValue): bool
564
    {
565 32
        $commonOptions = [
566 32
            'AUTO_INCREMENT',
567 32
            'COMMENT',
568 32
            'DEFAULT',
569 32
            'CHARACTER SET',
570 32
            'COLLATE',
571 32
            'PRIMARY',
572 32
            'UNIQUE',
573 32
            'PRIMARY KEY',
574 32
            'UNIQUE KEY',
575 32
        ];
576
577
        // Since these options can be used for
578
        // both table as well as a specific column in the table
579 32
        return in_array($tokenValue, $commonOptions);
580
    }
581
582
    /**
583
     * Check if token is symbol and quoted with backtick
584
     *
585
     * @param Token $token token to check
586
     */
587 76
    private static function checkIfTokenQuotedSymbol($token): bool
588
    {
589 76
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
590
    }
591
592
    public function __toString(): string
593
    {
594
        return static::build($this);
595
    }
596
}
597