Completed
Push — master ( 25a071...fe490d )
by Maurício
24s queued 20s
created

Expression::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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