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