Passed
Pull Request — master (#506)
by
unknown
04:51 queued 02:06
created

Expression::build()   B

Complexity

Conditions 10
Paths 18

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 10

Importance

Changes 0
Metric Value
cc 10
eloc 14
nc 18
nop 1
dl 0
loc 26
ccs 14
cts 14
cp 1
crap 10
rs 7.6666
c 0
b 0
f 0

How to fix   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\Context;
9
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
10
use PhpMyAdmin\SqlParser\Parser;
11
use PhpMyAdmin\SqlParser\Token;
12
use PhpMyAdmin\SqlParser\TokensList;
13
14
use function implode;
15
use function rtrim;
16
use function strlen;
17
use function trim;
18
19
/**
20
 * Parses a reference to an expression (column, table or database name, function
21
 * call, mathematical expression, etc.).
22
 */
23
#[\AllowDynamicProperties]
24
final class Expression implements Component
25
{
26
    /**
27
     * List of allowed reserved keywords in expressions.
28
     *
29
     * @var array<string, int>
30
     */
31
    private static $allowedKeywords = [
32
        'AND' => 1,
33
        'AS' => 1,
34
        'BETWEEN' => 1,
35
        'CASE' => 1,
36
        'DUAL' => 1,
37
        'DIV' => 1,
38
        'IS' => 1,
39
        'MOD' => 1,
40
        'NOT' => 1,
41
        'NOT NULL' => 1,
42
        'NULL' => 1,
43
        'OR' => 1,
44
        'OVER' => 1,
45
        'REGEXP' => 1,
46
        'RLIKE' => 1,
47
        'XOR' => 1,
48
    ];
49
50
    /**
51
     * The name of this database.
52
     *
53
     * @var string|null
54
     */
55
    public $database;
56
57
    /**
58
     * The name of this table.
59
     *
60
     * @var string|null
61
     */
62
    public $table;
63
64
    /**
65
     * The name of the column.
66
     *
67
     * @var string|null
68
     */
69
    public $column;
70
71
    /**
72
     * The sub-expression.
73
     *
74
     * @var string|null
75
     */
76
    public $expr = '';
77
78
    /**
79
     * The alias of this expression.
80
     *
81
     * @var string|null
82
     */
83
    public $alias;
84
85
    /**
86
     * The name of the function.
87
     *
88
     * @var mixed
89
     */
90
    public $function;
91
92
    /**
93
     * The type of subquery.
94
     *
95
     * @var string|null
96
     */
97
    public $subquery;
98
99
    /**
100
     * Syntax:
101
     *     new Expression('expr')
102
     *     new Expression('expr', 'alias')
103
     *     new Expression('database', 'table', 'column')
104
     *     new Expression('database', 'table', 'column', 'alias')
105
     *
106
     * If the database, table or column name is not required, pass an empty
107
     * string.
108
     *
109
     * @param string|null $database The name of the database or the expression.
110
     * @param string|null $table    The name of the table or the alias of the expression.
111
     * @param string|null $column   the name of the column
112
     * @param string|null $alias    the name of the alias
113
     */
114 1096
    public function __construct($database = null, $table = null, $column = null, $alias = null)
115
    {
116 1096
        if (($column === null) && ($alias === null)) {
117 1096
            $this->expr = $database; // case 1
118 1096
            $this->alias = $table; // case 2
119
        } else {
120 4
            $this->database = $database; // case 3
121 4
            $this->table = $table; // case 3
122 4
            $this->column = $column; // case 3
123 4
            $this->alias = $alias; // case 4
124
        }
125
    }
126
127
    /**
128
     * Possible options:.
129
     *
130
     *      `field`
131
     *
132
     *          First field to be filled.
133
     *          If this is not specified, it takes the value of `parseField`.
134
     *
135
     *      `parseField`
136
     *
137
     *          Specifies the type of the field parsed. It may be `database`,
138
     *          `table` or `column`. These expressions may not include
139
     *          parentheses.
140
     *
141
     *      `breakOnAlias`
142
     *
143
     *          If not empty, breaks when the alias occurs (it is not included).
144
     *
145
     *      `breakOnParentheses`
146
     *
147
     *          If not empty, breaks when the first parentheses occurs.
148
     *
149
     *      `parenthesesDelimited`
150
     *
151
     *          If not empty, breaks after last parentheses occurred.
152
     *
153
     * @param Parser               $parser  the parser that serves as context
154
     * @param TokensList           $list    the list of tokens that are being parsed
155
     * @param array<string, mixed> $options parameters for parsing
156
     *
157
     * @throws ParserException
158
     */
159 1086
    public static function parse(Parser $parser, TokensList $list, array $options = []): Expression|null
160
    {
161 1086
        $ret = new static();
162
163
        /**
164
         * Whether current tokens make an expression or a table reference.
165
         *
166
         * @var bool
167
         */
168 1086
        $isExpr = false;
169
170
        /**
171
         * Whether a period was previously found.
172
         *
173
         * @var bool
174
         */
175 1086
        $dot = false;
176
177
        /**
178
         * Whether an alias is expected. Is 2 if `AS` keyword was found.
179
         *
180
         * @var bool
181
         */
182 1086
        $alias = false;
183
184
        /**
185
         * Counts brackets.
186
         *
187
         * @var int
188
         */
189 1086
        $brackets = 0;
190
191
        /**
192
         * Keeps track of the last two previous tokens.
193
         *
194
         * @var Token[]
195
         */
196 1086
        $prev = [
197 1086
            null,
198 1086
            null,
199 1086
        ];
200
201
        // When a field is parsed, no parentheses are expected.
202 1086
        if (! empty($options['parseField'])) {
203 624
            $options['breakOnParentheses'] = true;
204 624
            $options['field'] = $options['parseField'];
205
        }
206
207 1086
        for (; $list->idx < $list->count; ++$list->idx) {
208
            /**
209
             * Token parsed at this moment.
210
             */
211 1086
            $token = $list->tokens[$list->idx];
212
213
            // End of statement.
214 1086
            if ($token->type === Token::TYPE_DELIMITER) {
215 424
                break;
216
            }
217
218
            // Skipping whitespaces and comments.
219 1084
            if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
220
                // If the token is a closing C comment from a MySQL command, it must be ignored.
221 926
                if ($isExpr && $token->token !== '*/') {
222 404
                    $ret->expr .= $token->token;
223
                }
224
225 926
                continue;
226
            }
227
228 1084
            if ($token->type === Token::TYPE_KEYWORD) {
229 846
                if (($brackets > 0) && empty($ret->subquery) && ! empty(Parser::$statementParsers[$token->keyword])) {
230
                    // A `(` was previously found and this keyword is the
231
                    // beginning of a statement, so this is a subquery.
232 68
                    $ret->subquery = $token->keyword;
233
                } elseif (
234 844
                    ($token->flags & Token::FLAG_KEYWORD_FUNCTION)
235 844
                    && (empty($options['parseField'])
236 844
                    && ! $alias)
237
                ) {
238 104
                    $isExpr = true;
239 820
                } elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED) && ($brackets === 0)) {
240 758
                    if (empty(self::$allowedKeywords[$token->keyword])) {
241
                        // A reserved keyword that is not allowed in the
242
                        // expression was found so the expression must have
243
                        // ended and a new clause is starting.
244 710
                        break;
245
                    }
246
247 204
                    if ($token->keyword === 'AS') {
248 156
                        if (! empty($options['breakOnAlias'])) {
249 30
                            break;
250
                        }
251
252 142
                        if ($alias) {
253 2
                            $parser->error('An alias was expected.', $token);
254 2
                            break;
255
                        }
256
257 142
                        $alias = true;
258 142
                        continue;
259
                    }
260
261 64
                    if ($token->keyword === 'CASE') {
262
                        // For a use of CASE like
263
                        // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...'
264 10
                        $tempCaseExpr = CaseExpression::parse($parser, $list);
265 10
                        $ret->expr .= CaseExpression::build($tempCaseExpr);
266 10
                        $isExpr = true;
267 10
                        continue;
268
                    }
269
270 54
                    $isExpr = true;
271 234
                } elseif ($brackets === 0 && strlen((string) $ret->expr) > 0 && ! $alias) {
272
                    /* End of expression */
273 160
                    break;
274
                }
275
            }
276
277
            if (
278 1080
                ($token->type === Token::TYPE_NUMBER)
279 1056
                || ($token->type === Token::TYPE_BOOL)
280 1056
                || (($token->type === Token::TYPE_SYMBOL)
281 1056
                && ($token->flags & Token::FLAG_SYMBOL_VARIABLE))
282 1048
                || (($token->type === Token::TYPE_SYMBOL)
283 1048
                && ($token->flags & Token::FLAG_SYMBOL_PARAMETER))
284 1080
                || (($token->type === Token::TYPE_OPERATOR)
285 1080
                && ($token->value !== '.'))
286
            ) {
287 692
                if (! empty($options['parseField'])) {
288 234
                    break;
289
                }
290
291
                // Numbers, booleans and operators (except dot) are usually part
292
                // of expressions.
293 540
                $isExpr = true;
294
            }
295
296 1080
            if ($token->type === Token::TYPE_OPERATOR) {
297 482
                if (! empty($options['breakOnParentheses']) && (($token->value === '(') || ($token->value === ')'))) {
298
                    // No brackets were expected.
299 4
                    break;
300
                }
301
302 480
                if ($token->value === '(') {
303 180
                    ++$brackets;
304
                    if (
305 180
                        empty($ret->function) && ($prev[1] !== null)
306 180
                        && (($prev[1]->type === Token::TYPE_NONE)
307 180
                        || ($prev[1]->type === Token::TYPE_SYMBOL)
308 180
                        || (($prev[1]->type === Token::TYPE_KEYWORD)
309 180
                        && ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)))
310
                    ) {
311 56
                        $ret->function = $prev[1]->value;
312
                    }
313 478
                } elseif ($token->value === ')') {
314 194
                    if ($brackets === 0) {
315
                        // Not our bracket
316 22
                        break;
317
                    }
318
319 178
                    --$brackets;
320 178
                    if ($brackets === 0) {
321 178
                        if (! empty($options['parenthesesDelimited'])) {
322
                            // The current token is the last bracket, the next
323
                            // one will be outside the expression.
324 50
                            $ret->expr .= $token->token;
325 50
                            ++$list->idx;
326 50
                            break;
327
                        }
328 22
                    } elseif ($brackets < 0) {
329
                        // $parser->error('Unexpected closing bracket.', $token);
330
                        // $brackets = 0;
331
                        break;
332
                    }
333 426
                } elseif ($token->value === ',') {
334
                    // Expressions are comma-delimited.
335 274
                    if ($brackets === 0) {
336 240
                        break;
337
                    }
338
                }
339
            }
340
341
            // Saving the previous tokens.
342 1078
            $prev[0] = $prev[1];
343 1078
            $prev[1] = $token;
344
345 1078
            if ($alias) {
346
                // An alias is expected (the keyword `AS` was previously found).
347 140
                if (! empty($ret->alias)) {
348 2
                    $parser->error('An alias was previously found.', $token);
349 2
                    break;
350
                }
351
352 140
                $ret->alias = $token->value;
353 140
                $alias = false;
354 1078
            } elseif ($isExpr) {
355
                // Handling aliases.
356
                if (
357 496
                    $brackets === 0
358 496
                    && ($prev[0] === null
359 496
                        || (($prev[0]->type !== Token::TYPE_OPERATOR || $prev[0]->token === ')')
360 496
                            && ($prev[0]->type !== Token::TYPE_KEYWORD
361 496
                                || ! ($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED))))
362 496
                    && (($prev[1]->type === Token::TYPE_STRING)
363 496
                        || ($prev[1]->type === Token::TYPE_SYMBOL
364 496
                            && ! ($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE))
365 496
                        || ($prev[1]->type === Token::TYPE_NONE
366 496
                            && $prev[1]->token !== 'OVER'))
367
                ) {
368 14
                    if (! empty($ret->alias)) {
369 4
                        $parser->error('An alias was previously found.', $token);
370 4
                        break;
371
                    }
372
373 12
                    $ret->alias = $prev[1]->value;
374
                } else {
375 496
                    $currIdx = $list->idx;
376 496
                    --$list->idx;
377 496
                    $beforeToken = $list->getPrevious();
378 496
                    $list->idx = $currIdx;
379
                    // columns names tokens are of type NONE, or SYMBOL (`col`), and the columns options
380
                    // would start with a token of type KEYWORD, in that case, we want to have a space
381
                    // between the tokens.
382
                    if (
383 496
                        $ret->expr !== null &&
384
                        $beforeToken &&
385 496
                        ($beforeToken->type === Token::TYPE_NONE ||
386 496
                        $beforeToken->type === Token::TYPE_SYMBOL || $beforeToken->type === Token::TYPE_STRING) &&
387 496
                        $token->type === Token::TYPE_KEYWORD
388
                    ) {
389 76
                        $ret->expr = rtrim($ret->expr, ' ') . ' ';
390
                    }
391
392 496
                    $ret->expr .= $token->token;
393
                }
394
            } else {
395 992
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '.')) {
396
                    // Found a `.` which means we expect a column name and
397
                    // the column name we parsed is actually the table name
398
                    // and the table name is actually a database name.
399 112
                    if (! empty($ret->database) || $dot) {
400 4
                        $parser->error('Unexpected dot.', $token);
401
                    }
402
403 112
                    $ret->database = $ret->table;
404 112
                    $ret->table = $ret->column;
405 112
                    $ret->column = null;
406 112
                    $dot = true;
407 112
                    $ret->expr .= $token->token;
408
                } else {
409 992
                    $field = empty($options['field']) ? 'column' : $options['field'];
410 992
                    if (empty($ret->$field)) {
411 992
                        $ret->$field = $token->value;
412 992
                        $ret->expr .= $token->token;
413 992
                        $dot = false;
414
                    } else {
415
                        // No alias is expected.
416 74
                        if (! empty($options['breakOnAlias'])) {
417 22
                            break;
418
                        }
419
420 52
                        if (! empty($ret->alias)) {
421 8
                            $parser->error('An alias was previously found.', $token);
422 8
                            break;
423
                        }
424
425 48
                        $ret->alias = $token->value;
426
                    }
427
                }
428
            }
429
        }
430
431 1086
        if ($alias) {
432 6
            $parser->error('An alias was expected.', $list->tokens[$list->idx - 1]);
433
        }
434
435
        // White-spaces might be added at the end.
436 1086
        $ret->expr = trim((string) $ret->expr);
437
438 1086
        if ($ret->expr === '') {
439 58
            return null;
440
        }
441
442 1078
        --$list->idx;
443
444 1078
        return $ret;
445
    }
446
447
    /**
448
     * @param Expression $component the component to be built
449
     */
450 260
    public static function build($component): string
451
    {
452 260
        if ($component->expr !== '' && $component->expr !== null) {
453 242
            $ret = $component->expr;
454
        } else {
455 22
            $fields = [];
456 22
            if (isset($component->database) && ($component->database !== '')) {
457 2
                $fields[] = $component->database;
458
            }
459
460 22
            if (isset($component->table) && ($component->table !== '')) {
461 22
                $fields[] = $component->table;
462
            }
463
464 22
            if (isset($component->column) && ($component->column !== '')) {
465 2
                $fields[] = $component->column;
466
            }
467
468 22
            $ret = implode('.', Context::escapeAll($fields));
469
        }
470
471 260
        if (! empty($component->alias)) {
472 32
            $ret .= ' AS ' . Context::escape($component->alias);
473
        }
474
475 260
        return $ret;
476
    }
477
478
    /**
479
     * @param Expression[] $component the component to be built
480
     */
481 14
    public static function buildAll(array $component): string
482
    {
483 14
        return implode(', ', $component);
484
    }
485
486 206
    public function __toString(): string
487
    {
488 206
        return static::build($this);
489
    }
490
}
491