Passed
Push — master ( e9fa3d...22a26f )
by William
03:07
created

AlterOperation   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 477
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 55
eloc 209
c 4
b 1
f 0
dl 0
loc 477
ccs 111
cts 111
cp 1
rs 6

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A checkIfTokenQuotedSymbol() 0 3 2
A build() 0 14 4
A checkIfColumnDefinitionKeyword() 0 17 1
D parse() 0 200 47

How to fix   Complexity   

Complex Class

Complex classes like AlterOperation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AlterOperation, and based on these observations, apply Extract Interface, too.

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
        'TO' => 2,
120
        'BY' => 2,
121
        'FOREIGN' => 2,
122
        'FULLTEXT' => 2,
123
        'KEY' => 2,
124
        'KEYS' => 2,
125
        'PARTITION' => 2,
126
        'PARTITION BY' => 2,
127
        'PARTITIONING' => 2,
128
        'PRIMARY KEY' => 2,
129
        'SPATIAL' => 2,
130
        'TABLESPACE' => 2,
131
        'INDEX' => 2,
132
133
        'CHARACTER SET' => 3,
134
    ];
135
136
    /**
137
     * All user options.
138
     *
139
     * @var array<string, int|array<int, int|string>>
140
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
141
     */
142
    public static $USER_OPTIONS = [
143
        'ATTRIBUTE' => [
144
            1,
145
            'var',
146
        ],
147
        'COMMENT' => [
148
            1,
149
            'var',
150
        ],
151
        'REQUIRE' => [
152
            1,
153
            'var',
154
        ],
155
        'BY' => [
156
            2,
157
            'expr',
158
        ],
159
        'PASSWORD' => [
160
            2,
161
            'var',
162
        ],
163
        'WITH' => [
164
            2,
165
            'var',
166
        ],
167
168
        'ACCOUNT' => 1,
169
        'DEFAULT' => 1,
170
171
        'LOCK' => 2,
172
        'UNLOCK' => 2,
173
174
        'IDENTIFIED' => 3,
175
    ];
176
177
    /**
178
     * All view options.
179
     *
180
     * @var array<string, int|array<int, int|string>>
181
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
182
     */
183
    public static $VIEW_OPTIONS = ['AS' => 1];
184
185
    /**
186
     * Options of this operation.
187
     *
188
     * @var OptionsArray
189
     */
190
    public $options;
191
192
    /**
193
     * The altered field.
194
     *
195
     * @var Expression|string|null
196
     */
197
    public $field;
198
199
    /**
200
     * The partitions.
201
     *
202
     * @var Component[]|ArrayObj|null
203
     */
204
    public $partitions;
205
206
    /**
207
     * Unparsed tokens.
208
     *
209
     * @var Token[]|string
210
     */
211
    public $unknown = [];
212
213
    /**
214
     * @param OptionsArray              $options    options of alter operation
215
     * @param Expression|string|null    $field      altered field
216
     * @param Component[]|ArrayObj|null $partitions partitions definition found in the operation
217
     * @param Token[]                   $unknown    unparsed tokens found at the end of operation
218
     */
219 172
    public function __construct(
220
        $options = null,
221
        $field = null,
222
        $partitions = null,
223
        $unknown = []
224
    ) {
225 172
        $this->partitions = $partitions;
226 172
        $this->options = $options;
227 172
        $this->field = $field;
228 172
        $this->unknown = $unknown;
229 43
    }
230
231
    /**
232
     * @param Parser               $parser  the parser that serves as context
233
     * @param TokensList           $list    the list of tokens that are being parsed
234
     * @param array<string, mixed> $options parameters for parsing
235
     *
236
     * @return AlterOperation
237
     */
238 172
    public static function parse(Parser $parser, TokensList $list, array $options = [])
239
    {
240 172
        $ret = new static();
241
242
        /**
243
         * Counts brackets.
244
         *
245
         * @var int
246
         */
247 172
        $brackets = 0;
248
249
        /**
250
         * The state of the parser.
251
         *
252
         * Below are the states of the parser.
253
         *
254
         *      0 ---------------------[ options ]---------------------> 1
255
         *
256
         *      1 ----------------------[ field ]----------------------> 2
257
         *
258
         *      1 -------------[ PARTITION / PARTITION BY ]------------> 3
259
         *
260
         *      2 -------------------------[ , ]-----------------------> 0
261
         *
262
         * @var int
263
         */
264 172
        $state = 0;
265
266
        /**
267
         * partition state.
268
         *
269
         * @var int
270
         */
271 172
        $partitionState = 0;
272
273 172
        for (; $list->idx < $list->count; ++$list->idx) {
274
            /**
275
             * Token parsed at this moment.
276
             */
277 172
            $token = $list->tokens[$list->idx];
278
279
            // End of statement.
280 172
            if ($token->type === Token::TYPE_DELIMITER) {
281 148
                break;
282
            }
283
284
            // Skipping comments.
285 172
            if ($token->type === Token::TYPE_COMMENT) {
286 4
                continue;
287
            }
288
289
            // Skipping whitespaces.
290 172
            if ($token->type === Token::TYPE_WHITESPACE) {
291 88
                if ($state === 2) {
292
                    // When parsing the unknown part, the whitespaces are
293
                    // included to not break anything.
294 80
                    $ret->unknown[] = $token;
295 80
                    continue;
296
                }
297
            }
298
299 172
            if ($state === 0) {
300 172
                $ret->options = OptionsArray::parse($parser, $list, $options);
301
302 172
                if ($ret->options->has('AS')) {
303 4
                    for (; $list->idx < $list->count; ++$list->idx) {
304 4
                        if ($list->tokens[$list->idx]->type === Token::TYPE_DELIMITER) {
305 4
                            break;
306
                        }
307
308 4
                        $ret->unknown[] = $list->tokens[$list->idx];
309
                    }
310
311 4
                    break;
312
                }
313
314 168
                $state = 1;
315 168
                if ($ret->options->has('PARTITION') || $token->value === 'PARTITION BY') {
316 8
                    $state = 3;
317 168
                    $list->getPrevious(); // in order to check whether it's partition or partition by.
318
                }
319 140
            } elseif ($state === 1) {
320 132
                $ret->field = Expression::parse(
321 33
                    $parser,
322 33
                    $list,
323
                    [
324 132
                        'breakOnAlias' => true,
325
                        'parseField' => 'column',
326
                    ]
327
                );
328 132
                if ($ret->field === null) {
329
                    // No field was read. We go back one token so the next
330
                    // iteration will parse the same token, but in state 2.
331 36
                    --$list->idx;
332
                }
333
334 132
                $state = 2;
335 120
            } elseif ($state === 2) {
336 112
                if (is_string($token->value) || is_numeric($token->value)) {
337 112
                    $arrayKey = $token->value;
338
                } else {
339 4
                    $arrayKey = $token->token;
340
                }
341
342 112
                if ($token->type === Token::TYPE_OPERATOR) {
343 60
                    if ($token->value === '(') {
344 44
                        ++$brackets;
345 60
                    } elseif ($token->value === ')') {
346 44
                        --$brackets;
347 32
                    } elseif (($token->value === ',') && ($brackets === 0)) {
348 60
                        break;
349
                    }
350 112
                } elseif (! self::checkIfTokenQuotedSymbol($token)) {
351 100
                    if (! empty(Parser::$STATEMENT_PARSERS[$token->value])) {
352
                        // We want to get the next non-comment and non-space token after $token
353
                        // therefore, the first getNext call will start with the current $idx which's $token,
354
                        // will return it and increase $idx by 1, which's not guaranteed to be non-comment
355
                        // and non-space, that's why we're calling getNext again.
356
357 24
                        $list->getNext();
358 24
                        $nextToken = $list->getNext();
359
360 24
                        if ($token->value === 'SET' && $nextToken !== null && $nextToken->value === '(') {
361
                            // To avoid adding the tokens between the SET() parentheses to the unknown tokens
362 8
                            $list->getNextOfTypeAndValue(Token::TYPE_OPERATOR, ')');
363 16
                        } elseif ($token->value === 'SET' && $nextToken !== null && $nextToken->value === 'DEFAULT') {
364
                            // to avoid adding the `DEFAULT` token to the unknown tokens.
365 8
                            ++$list->idx;
366
                        } else {
367
                            // We have reached the end of ALTER operation and suddenly found
368
                            // a start to new statement, but have not find a delimiter between them
369 8
                            $parser->error(
370 2
                                'A new statement was found, but no delimiter between it and the previous one.',
371 2
                                $token
372
                            );
373 24
                            break;
374
                        }
375
                    } elseif (
376 92
                        (array_key_exists($arrayKey, self::$DB_OPTIONS)
377 92
                        || array_key_exists($arrayKey, self::$TABLE_OPTIONS))
378 92
                        && ! self::checkIfColumnDefinitionKeyword($arrayKey)
379
                    ) {
380
                        // This alter operation has finished, which means a comma
381
                        // was missing before start of new alter operation
382 8
                        $parser->error('Missing comma before start of a new alter operation.', $token);
383 8
                        break;
384
                    }
385
                }
386
387 112
                $ret->unknown[] = $token;
388 8
            } elseif ($state === 3) {
389 8
                if ($partitionState === 0) {
390
                        // We want to get the next non-comment and non-space token after $token
391
                        // therefore, the first getNext call will start with the current $idx which's $token,
392
                        // will return it and increase $idx by 1, which's not guaranteed to be non-comment
393
                        // and non-space, that's why we're calling getNext again.
394
395 8
                        $list->getNext();
396 8
                        $nextToken = $list->getNext();
397
                    if (
398 8
                        ($token->type === Token::TYPE_KEYWORD)
399 8
                        && (($token->keyword === 'PARTITION BY')
400 8
                        || ($token->keyword === 'PARTITION' && $nextToken && $nextToken->value !== '('))
401
                    ) {
402 8
                        $partitionState = 1;
403 4
                    } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITION')) {
404 4
                        $partitionState = 2;
405
                    }
406
407 8
                    --$list->idx; // to decrease the idx by one, because the last getNext returned and increased it.
408
409
                    // reverting the effect of the getNext
410 8
                    $list->getPrevious();
411 8
                    $list->getPrevious();
412
413 8
                    ++$list->idx; // to index the idx by one, because the last getPrevious returned and decreased it.
414 8
                } elseif ($partitionState === 1) {
415
                    // Building the expression used for partitioning.
416 8
                    if (empty($ret->field)) {
417 8
                        $ret->field = '';
418
                    }
419
420 8
                    $ret->field .= $token->type === Token::TYPE_WHITESPACE ? ' ' : $token->token;
421 4
                } elseif ($partitionState === 2) {
422 4
                    $ret->partitions = ArrayObj::parse(
423 1
                        $parser,
424 1
                        $list,
425 4
                        ['type' => PartitionDefinition::class]
426
                    );
427
                }
428
            }
429
        }
430
431 172
        if ($ret->options->isEmpty()) {
432 4
            $parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
433
        }
434
435 172
        --$list->idx;
436
437 172
        return $ret;
438
    }
439
440
    /**
441
     * @param AlterOperation       $component the component to be built
442
     * @param array<string, mixed> $options   parameters for building
443
     *
444
     * @return string
445
     */
446 12
    public static function build($component, array $options = [])
447
    {
448 12
        $ret = $component->options . ' ';
449 12
        if (isset($component->field) && ($component->field !== '')) {
450 12
            $ret .= $component->field . ' ';
451
        }
452
453 12
        $ret .= TokensList::build($component->unknown);
454
455 12
        if (isset($component->partitions)) {
456 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

456
            $ret .= PartitionDefinition::build(/** @scrutinizer ignore-type */ $component->partitions);
Loading history...
457
        }
458
459 12
        return $ret;
460
    }
461
462
    /**
463
     * Check if token's value is one of the common keywords
464
     * between column and table alteration
465
     *
466
     * @param string $tokenValue Value of current token
467
     *
468
     * @return bool
469
     */
470 56
    private static function checkIfColumnDefinitionKeyword($tokenValue)
471
    {
472 28
        $commonOptions = [
473 14
            'AUTO_INCREMENT',
474
            'COMMENT',
475
            'DEFAULT',
476
            'CHARACTER SET',
477
            'COLLATE',
478
            'PRIMARY',
479
            'UNIQUE',
480
            'PRIMARY KEY',
481
            'UNIQUE KEY',
482
        ];
483
484
        // Since these options can be used for
485
        // both table as well as a specific column in the table
486 56
        return in_array($tokenValue, $commonOptions);
487
    }
488
489
    /**
490
     * Check if token is symbol and quoted with backtick
491
     *
492
     * @param Token $token token to check
493
     *
494
     * @return bool
495
     */
496 112
    private static function checkIfTokenQuotedSymbol($token)
497
    {
498 112
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
499
    }
500
}
501