Passed
Pull Request — master (#536)
by Maurício
02:59
created

Expression   F

Complexity

Total Complexity 99

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 199
c 1
b 0
f 0
dl 0
loc 454
ccs 169
cts 169
cp 1
rs 2
wmc 99

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
F parse() 0 277 84
B build() 0 26 10
A __toString() 0 3 1
A buildAll() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Expression often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Expression, and based on these observations, apply Extract Interface, too.

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