Passed
Push — master ( b16987...8b6d77 )
by Maurício
03:49 queued 13s
created

Expressions::parse()   F

Complexity

Conditions 86
Paths 3848

Size

Total Lines 280
Code Lines 153

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 143
CRAP Score 86.0024

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 86
eloc 153
c 1
b 0
f 0
nc 3848
nop 3
dl 0
loc 280
ccs 143
cts 144
cp 0.9931
crap 86.0024
rs 0

How to fix   Long Method    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\Parsers;
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\Components\Expression;
9
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
10
use PhpMyAdmin\SqlParser\Parseable;
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 in_array;
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 Expressions implements Parseable
28
{
29
    /**
30
     * List of allowed reserved keywords in expressions.
31
     */
32
    private const ALLOWED_KEYWORDS = [
33
        'AND',
34
        'AS',
35
        'BETWEEN',
36
        'CASE',
37
        'DUAL',
38
        'DIV',
39
        'IS',
40
        'MOD',
41
        'NOT',
42
        'NOT NULL',
43
        'NULL',
44
        'OR',
45
        'OVER',
46
        'REGEXP',
47
        'RLIKE',
48
        'XOR',
49
    ];
50
51
    /**
52
     * Possible options:.
53
     *
54
     *      `field`
55
     *
56
     *          First field to be filled.
57
     *          If this is not specified, it takes the value of `parseField`.
58
     *
59
     *      `parseField`
60
     *
61
     *          Specifies the type of the field parsed. It may be `database`,
62
     *          `table` or `column`. These expressions may not include
63
     *          parentheses.
64
     *
65
     *      `breakOnAlias`
66
     *
67
     *          If not empty, breaks when the alias occurs (it is not included).
68
     *
69
     *      `breakOnParentheses`
70
     *
71
     *          If not empty, breaks when the first parentheses occurs.
72
     *
73
     *      `parenthesesDelimited`
74
     *
75
     *          If not empty, breaks after last parentheses occurred.
76
     *
77
     * @param Parser               $parser  the parser that serves as context
78
     * @param TokensList           $list    the list of tokens that are being parsed
79
     * @param array<string, mixed> $options parameters for parsing
80
     *
81
     * @throws ParserException
82
     */
83 1108
    public static function parse(Parser $parser, TokensList $list, array $options = []): Expression|null
84
    {
85 1108
        $ret = new Expression();
86
87
        /**
88
         * Whether current tokens make an expression or a table reference.
89
         */
90 1108
        $isExpr = false;
91
92
        /**
93
         * Whether a period was previously found.
94
         */
95 1108
        $dot = false;
96
97
        /**
98
         * Whether an alias is expected. Is 2 if `AS` keyword was found.
99
         */
100 1108
        $alias = false;
101
102
        /**
103
         * Counts brackets.
104
         */
105 1108
        $brackets = 0;
106
107
        /**
108
         * Keeps track of the last two previous tokens.
109
         */
110 1108
        $prev = [
111 1108
            null,
112 1108
            null,
113 1108
        ];
114
115
        // When a field is parsed, no parentheses are expected.
116 1108
        if (! empty($options['parseField'])) {
117 638
            $options['breakOnParentheses'] = true;
118 638
            $options['field'] = $options['parseField'];
119
        }
120
121 1108
        for (; $list->idx < $list->count; ++$list->idx) {
122
            /**
123
             * Token parsed at this moment.
124
             */
125 1108
            $token = $list->tokens[$list->idx];
126
127
            // End of statement.
128 1108
            if ($token->type === TokenType::Delimiter) {
129 430
                break;
130
            }
131
132
            // Skipping whitespaces and comments.
133 1106
            if (($token->type === TokenType::Whitespace) || ($token->type === TokenType::Comment)) {
134
                // If the token is a closing C comment from a MySQL command, it must be ignored.
135 946
                if ($isExpr && $token->token !== '*/') {
136 412
                    $ret->expr .= $token->token;
137
                }
138
139 946
                continue;
140
            }
141
142 1106
            if ($token->type === TokenType::Keyword) {
143 864
                if (($brackets > 0) && empty($ret->subquery) && ! empty(Parser::STATEMENT_PARSERS[$token->keyword])) {
144
                    // A `(` was previously found and this keyword is the
145
                    // beginning of a statement, so this is a subquery.
146 68
                    $ret->subquery = $token->keyword;
147
                } elseif (
148 862
                    ($token->flags & Token::FLAG_KEYWORD_FUNCTION)
149 862
                    && (empty($options['parseField'])
150 862
                    && ! $alias)
151
                ) {
152 106
                    $isExpr = true;
153 838
                } elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED) && ($brackets === 0)) {
154 776
                    if (! in_array($token->keyword, self::ALLOWED_KEYWORDS, true)) {
155
                        // A reserved keyword that is not allowed in the
156
                        // expression was found so the expression must have
157
                        // ended and a new clause is starting.
158 726
                        break;
159
                    }
160
161 208
                    if ($token->keyword === 'AS') {
162 160
                        if (! empty($options['breakOnAlias'])) {
163 30
                            break;
164
                        }
165
166 146
                        if ($alias) {
167 2
                            $parser->error('An alias was expected.', $token);
168 2
                            break;
169
                        }
170
171 146
                        $alias = true;
172 146
                        continue;
173
                    }
174
175 64
                    if ($token->keyword === 'CASE') {
176
                        // For a use of CASE like
177
                        // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...'
178 10
                        $tempCaseExpr = CaseExpressions::parse($parser, $list);
179 10
                        $ret->expr .= $tempCaseExpr->build();
180 10
                        $isExpr = true;
181 10
                        continue;
182
                    }
183
184 54
                    $isExpr = true;
185
                } elseif (
186 240
                    $brackets === 0 && strlen((string) $ret->expr) > 0 && ! $alias
187 240
                    && ($ret->table === null || $ret->table === '')
188
                ) {
189
                    /* End of expression */
190 110
                    break;
191
                }
192
            }
193
194
            if (
195 1102
                ($token->type === TokenType::Number)
196 1078
                || ($token->type === TokenType::Bool)
197 1078
                || (($token->type === TokenType::Symbol)
198 1078
                && ($token->flags & Token::FLAG_SYMBOL_VARIABLE))
199 1070
                || (($token->type === TokenType::Symbol)
200 1070
                && ($token->flags & Token::FLAG_SYMBOL_PARAMETER))
201 1102
                || (($token->type === TokenType::Operator)
202 1102
                && ($token->value !== '.'))
203
            ) {
204 706
                if (! empty($options['parseField'])) {
205 236
                    break;
206
                }
207
208
                // Numbers, booleans and operators (except dot) are usually part
209
                // of expressions.
210 552
                $isExpr = true;
211
            }
212
213 1102
            if ($token->type === TokenType::Operator) {
214 488
                if (! empty($options['breakOnParentheses']) && (($token->value === '(') || ($token->value === ')'))) {
215
                    // No brackets were expected.
216 4
                    break;
217
                }
218
219 486
                if ($token->value === '(') {
220 182
                    ++$brackets;
221
                    if (
222 182
                        empty($ret->function) && ($prev[1] !== null)
223 182
                        && (($prev[1]->type === TokenType::None)
224 182
                        || ($prev[1]->type === TokenType::Symbol)
225 182
                        || (($prev[1]->type === TokenType::Keyword)
226 182
                        && ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)))
227
                    ) {
228 58
                        $ret->function = $prev[1]->value;
229
                    }
230 484
                } elseif ($token->value === ')') {
231 196
                    if ($brackets === 0) {
232
                        // Not our bracket
233 22
                        break;
234
                    }
235
236 180
                    --$brackets;
237 180
                    if ($brackets === 0) {
238 180
                        if (! empty($options['parenthesesDelimited'])) {
239
                            // The current token is the last bracket, the next
240
                            // one will be outside the expression.
241 50
                            $ret->expr .= $token->token;
242 50
                            ++$list->idx;
243 50
                            break;
244
                        }
245 22
                    } elseif ($brackets < 0) {
246
                        // $parser->error('Unexpected closing bracket.', $token);
247
                        // $brackets = 0;
248
                        break;
249
                    }
250 432
                } elseif ($token->value === ',') {
251
                    // Expressions are comma-delimited.
252 278
                    if ($brackets === 0) {
253 244
                        break;
254
                    }
255
                }
256
            }
257
258
            // Saving the previous tokens.
259 1100
            $prev[0] = $prev[1];
260 1100
            $prev[1] = $token;
261
262 1100
            if ($alias) {
263
                // An alias is expected (the keyword `AS` was previously found).
264 144
                if (! empty($ret->alias)) {
265 2
                    $parser->error('An alias was previously found.', $token);
266 2
                    break;
267
                }
268
269 144
                $ret->alias = $token->value;
270 144
                $alias = false;
271 1100
            } elseif ($isExpr) {
272
                // Handling aliases.
273
                if (
274 508
                    $brackets === 0
275 508
                    && ($prev[0] === null
276 508
                        || (($prev[0]->type !== TokenType::Operator || $prev[0]->token === ')')
277 508
                            && ($prev[0]->type !== TokenType::Keyword
278 508
                                || ! ($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED))))
279 508
                    && (($prev[1]->type === TokenType::String)
280 508
                        || ($prev[1]->type === TokenType::Symbol
281 508
                            && ! ($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE)
282 508
                            && ! ($prev[1]->flags & Token::FLAG_SYMBOL_PARAMETER))
283 508
                        || ($prev[1]->type === TokenType::None
284 508
                            && $prev[1]->token !== 'OVER'))
285
                ) {
286 14
                    if (! empty($ret->alias)) {
287 4
                        $parser->error('An alias was previously found.', $token);
288 4
                        break;
289
                    }
290
291 12
                    $ret->alias = $prev[1]->value;
292
                } else {
293 508
                    $currIdx = $list->idx;
294 508
                    --$list->idx;
295 508
                    $beforeToken = $list->getPrevious();
296 508
                    $list->idx = $currIdx;
297
                    // columns names tokens are of type NONE, or SYMBOL (`col`), and the columns options
298
                    // would start with a token of type KEYWORD, in that case, we want to have a space
299
                    // between the tokens.
300
                    if (
301 508
                        $ret->expr !== null &&
302
                        $beforeToken &&
303 508
                        ($beforeToken->type === TokenType::None ||
304 508
                        $beforeToken->type === TokenType::Symbol || $beforeToken->type === TokenType::String) &&
305 508
                        $token->type === TokenType::Keyword
306
                    ) {
307 76
                        $ret->expr = rtrim($ret->expr, ' ') . ' ';
308
                    }
309
310 508
                    $ret->expr .= $token->token;
311
                }
312
            } else {
313 1010
                if (($token->type === TokenType::Operator) && ($token->value === '.')) {
314
                    // Found a `.` which means we expect a column name and
315
                    // the column name we parsed is actually the table name
316
                    // and the table name is actually a database name.
317 112
                    if (! empty($ret->database) || $dot) {
318 4
                        $parser->error('Unexpected dot.', $token);
319
                    }
320
321 112
                    $ret->database = $ret->table;
322 112
                    $ret->table = $ret->column;
323 112
                    $ret->column = null;
324 112
                    $dot = true;
325 112
                    $ret->expr .= $token->token;
326
                } else {
327 1010
                    $field = empty($options['field']) ? 'column' : $options['field'];
328 1010
                    if (empty($ret->$field)) {
329 1010
                        $ret->$field = $token->value;
330 1010
                        $ret->expr .= $token->token;
331 1010
                        $dot = false;
332
                    } else {
333
                        // No alias is expected.
334 160
                        if (! empty($options['breakOnAlias'])) {
335 100
                            break;
336
                        }
337
338 60
                        if (! empty($ret->alias)) {
339 12
                            $parser->error('An alias was previously found.', $token);
340 12
                            break;
341
                        }
342
343 52
                        $ret->alias = $token->value;
344
                    }
345
                }
346
            }
347
        }
348
349 1108
        if ($alias) {
350 6
            $parser->error('An alias was expected.', $list->tokens[$list->idx - 1]);
351
        }
352
353
        // White-spaces might be added at the end.
354 1108
        $ret->expr = trim((string) $ret->expr);
355
356 1108
        if ($ret->expr === '') {
357 58
            return null;
358
        }
359
360 1100
        --$list->idx;
361
362 1100
        return $ret;
363
    }
364
365
    /** @param Expression[] $component the component to be built */
366 14
    public static function buildAll(array $component): string
367
    {
368 14
        return implode(', ', $component);
369
    }
370
}
371