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