Passed
Push — master ( 7315fc...e1b21e )
by William
03:29
created

AlterOperation::parse()   D

Complexity

Conditions 48
Paths 36

Size

Total Lines 192
Code Lines 95

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 96
CRAP Score 48.0025

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 48
eloc 95
c 4
b 1
f 0
nc 36
nop 3
dl 0
loc 192
ccs 96
cts 97
cp 0.9897
crap 48.0025
rs 4.1666

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

496
            $ret .= PartitionDefinition::build(/** @scrutinizer ignore-type */ $component->partitions);
Loading history...
497
        }
498
499 12
        return $ret;
500
    }
501
502
    /**
503
     * Check if token's value is one of the common keywords
504
     * between column and table alteration
505
     *
506
     * @param string $tokenValue Value of current token
507
     *
508
     * @return bool
509
     */
510 56
    private static function checkIfColumnDefinitionKeyword($tokenValue)
511
    {
512 56
        $commonOptions = [
513 56
            'AUTO_INCREMENT',
514 56
            'COMMENT',
515 56
            'DEFAULT',
516 56
            'CHARACTER SET',
517 56
            'COLLATE',
518 56
            'PRIMARY',
519 56
            'UNIQUE',
520 56
            'PRIMARY KEY',
521 56
            'UNIQUE KEY',
522 56
        ];
523
524
        // Since these options can be used for
525
        // both table as well as a specific column in the table
526 56
        return in_array($tokenValue, $commonOptions);
527
    }
528
529
    /**
530
     * Check if token is symbol and quoted with backtick
531
     *
532
     * @param Token $token token to check
533
     *
534
     * @return bool
535
     */
536 112
    private static function checkIfTokenQuotedSymbol($token)
537
    {
538 112
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
539
    }
540
}
541