Completed
Push — master ( 31bf53...69db6d )
by Maurício
02:21 queued 02:17
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 0
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 AllowDynamicProperties;
0 ignored issues
show
Bug introduced by
The type AllowDynamicProperties was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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