Completed
Pull Request — master (#173)
by Madhura
07:06
created

Expression::parse()   F

Complexity

Conditions 77
Paths 3848

Size

Total Lines 263
Code Lines 142

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 137
CRAP Score 77.0022

Importance

Changes 0
Metric Value
dl 0
loc 263
ccs 137
cts 138
cp 0.9928
rs 2
c 0
b 0
f 0
cc 77
eloc 142
nc 3848
nop 3
crap 77.0022

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
/**
4
 * Parses a reference to an expression (column, table or database name, function
5
 * call, mathematical expression, etc.).
6
 */
7
8
namespace PhpMyAdmin\SqlParser\Components;
9
10
use PhpMyAdmin\SqlParser\Component;
11
use PhpMyAdmin\SqlParser\Context;
12
use PhpMyAdmin\SqlParser\Parser;
13
use PhpMyAdmin\SqlParser\Token;
14
use PhpMyAdmin\SqlParser\TokensList;
15
16
/**
17
 * Parses a reference to an expression (column, table or database name, function
18
 * call, mathematical expression, etc.).
19
 *
20
 * @category   Components
21
 *
22
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
23
 */
24
class Expression extends Component
25
{
26
    /**
27
     * List of allowed reserved keywords in expressions.
28
     *
29
     * @var array
30
     */
31
    private static $ALLOWED_KEYWORDS = array(
32
        'AS' => 1, 'DUAL' => 1, 'NULL' => 1, 'REGEXP' => 1, 'CASE' => 1,
33
        'DIV' => 1, 'AND' => 1, 'OR' => 1, 'XOR' => 1, 'NOT' => 1, 'MOD' => 1,
34
    );
35
36
    /**
37
     * The name of this database.
38
     *
39
     * @var string
40
     */
41
    public $database;
42
43
    /**
44
     * The name of this table.
45
     *
46
     * @var string
47
     */
48
    public $table;
49
50
    /**
51
     * The name of the column.
52
     *
53
     * @var string
54
     */
55
    public $column;
56
57
    /**
58
     * The sub-expression.
59
     *
60
     * @var string
61
     */
62
    public $expr = '';
63
64
    /**
65
     * The alias of this expression.
66
     *
67
     * @var string
68
     */
69
    public $alias;
70
71
    /**
72
     * The name of the function.
73
     *
74
     * @var mixed
75
     */
76
    public $function;
77
78
    /**
79
     * The type of subquery.
80
     *
81
     * @var string
82
     */
83
    public $subquery;
84
85
    /**
86
     * Constructor.
87
     *
88
     * Syntax:
89
     *     new Expression('expr')
90
     *     new Expression('expr', 'alias')
91
     *     new Expression('database', 'table', 'column')
92
     *     new Expression('database', 'table', 'column', 'alias')
93
     *
94
     * If the database, table or column name is not required, pass an empty
95
     * string.
96
     *
97
     * @param string $database The name of the database or the the expression.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $database not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
98
     *                         the the expression.
99
     * @param string $table    The name of the table or the alias of the expression.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $table not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
100
     *                         the alias of the expression.
101
     * @param string $column   the name of the column
0 ignored issues
show
Documentation introduced by
Should the type for parameter $column not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
102
     * @param string $alias    the name of the alias
0 ignored issues
show
Documentation introduced by
Should the type for parameter $alias not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
103
     */
104 270
    public function __construct($database = null, $table = null, $column = null, $alias = null)
105
    {
106 270
        if (($column === null) && ($alias === null)) {
107 270
            $this->expr = $database; // case 1
108 270
            $this->alias = $table; // case 2
109
        } else {
110 2
            $this->database = $database; // case 3
111 2
            $this->table = $table; // case 3
112 2
            $this->column = $column; // case 3
113 2
            $this->alias = $alias; // case 4
114
        }
115 270
    }
116
117
    /**
118
     * Possible options:.
119
     *
120
     *      `field`
121
     *
122
     *          First field to be filled.
123
     *          If this is not specified, it takes the value of `parseField`.
124
     *
125
     *      `parseField`
126
     *
127
     *          Specifies the type of the field parsed. It may be `database`,
128
     *          `table` or `column`. These expressions may not include
129
     *          parentheses.
130
     *
131
     *      `breakOnAlias`
132
     *
133
     *          If not empty, breaks when the alias occurs (it is not included).
134
     *
135
     *      `breakOnParentheses`
136
     *
137
     *          If not empty, breaks when the first parentheses occurs.
138
     *
139
     *      `parenthesesDelimited`
140
     *
141
     *          If not empty, breaks after last parentheses occurred.
142
     *
143
     * @param Parser     $parser  the parser that serves as context
144
     * @param TokensList $list    the list of tokens that are being parsed
145
     * @param array      $options parameters for parsing
146
     *
147
     * @return Expression
0 ignored issues
show
Documentation introduced by
Should the return type not be Expression|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
148
     */
149 264
    public static function parse(Parser $parser, TokensList $list, array $options = array())
150
    {
151 264
        $ret = new self();
152
153
        /**
154
         * Whether current tokens make an expression or a table reference.
155
         *
156
         * @var bool
157
         */
158 264
        $isExpr = false;
159
160
        /**
161
         * Whether a period was previously found.
162
         *
163
         * @var bool
164
         */
165 264
        $dot = false;
166
167
        /**
168
         * Whether an alias is expected. Is 2 if `AS` keyword was found.
169
         *
170
         * @var bool
171
         */
172 264
        $alias = false;
173
174
        /**
175
         * Counts brackets.
176
         *
177
         * @var int
178
         */
179 264
        $brackets = 0;
180
181
        /**
182
         * Keeps track of the last two previous tokens.
183
         *
184
         * @var Token[]
185
         */
186 264
        $prev = array(null, null);
187
188
        // When a field is parsed, no parentheses are expected.
189 264
        if (!empty($options['parseField'])) {
190 127
            $options['breakOnParentheses'] = true;
191 127
            $options['field'] = $options['parseField'];
192
        }
193
194 264
        for (; $list->idx < $list->count; ++$list->idx) {
195
            /**
196
             * Token parsed at this moment.
197
             *
198
             * @var Token
199
             */
200 264
            $token = $list->tokens[$list->idx];
201
202
            // End of statement.
203 264
            if ($token->type === Token::TYPE_DELIMITER) {
204 105
                break;
205
            }
206
207
            // Skipping whitespaces and comments.
208 263
            if (($token->type === Token::TYPE_WHITESPACE)
209 263
                || ($token->type === Token::TYPE_COMMENT)
210
            ) {
211 214
                if ($isExpr) {
212 104
                    $ret->expr .= $token->token;
213
                }
214 214
                continue;
215
            }
216
217 263
            if ($token->type === Token::TYPE_KEYWORD) {
218 197
                if (($brackets > 0) && (empty($ret->subquery))
219 25
                    && (!empty(Parser::$STATEMENT_PARSERS[$token->keyword]))
220
                ) {
221
                    // A `(` was previously found and this keyword is the
222
                    // beginning of a statement, so this is a subquery.
223 20
                    $ret->subquery = $token->keyword;
224 196
                } elseif (($token->flags & Token::FLAG_KEYWORD_FUNCTION)
225 28
                    && (empty($options['parseField'])
226 19
                    && !$alias)
227
                ) {
228 17
                    $isExpr = true;
229 191
                } elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED)
230 184
                    && ($brackets === 0)
231
                ) {
232 184
                    if (empty(self::$ALLOWED_KEYWORDS[$token->keyword])) {
233
                        // A reserved keyword that is not allowed in the
234
                        // expression was found so the expression must have
235
                        // ended and a new clause is starting.
236 173
                        break;
237
                    }
238 46
                    if ($token->keyword === 'AS') {
239 37
                        if (!empty($options['breakOnAlias'])) {
240 5
                            break;
241
                        }
242 32
                        if ($alias) {
243 1
                            $parser->error(
244 1
                                'An alias was expected.',
245 1
                                $token
246
                            );
247 1
                            break;
248
                        }
249 32
                        $alias = true;
250 32
                        continue;
251 10
                    } elseif ($token->keyword === 'CASE') {
252
                        // For a use of CASE like
253
                        // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...'
254 1
                        $tempCaseExpr = CaseExpression::parse($parser, $list);
255 1
                        $ret->expr .= CaseExpression::build($tempCaseExpr);
256 1
                        $isExpr = true;
257 1
                        continue;
258
                    }
259 9
                    $isExpr = true;
260 47
                } elseif ($brackets === 0 && strlen($ret->expr) > 0 && !$alias) {
261
                    /* End of expression */
262 28
                    break;
263
                }
264
            }
265
266 261
            if (($token->type === Token::TYPE_NUMBER)
267 254
                || ($token->type === Token::TYPE_BOOL)
268 254
                || (($token->type === Token::TYPE_SYMBOL)
269 63
                && ($token->flags & Token::FLAG_SYMBOL_VARIABLE))
270 254
                || (($token->type === Token::TYPE_SYMBOL)
271 58
                && ($token->flags & Token::FLAG_SYMBOL_PARAMETER))
272 254
                || (($token->type === Token::TYPE_OPERATOR)
273 173
                && ($token->value !== '.'))
274
            ) {
275 187
                if (!empty($options['parseField'])) {
276 61
                    break;
277
                }
278
279
                // Numbers, booleans and operators (except dot) are usually part
280
                // of expressions.
281 147
                $isExpr = true;
282
            }
283
284 261
            if ($token->type === Token::TYPE_OPERATOR) {
285 136
                if ((!empty($options['breakOnParentheses']))
286 4
                    && (($token->value === '(') || ($token->value === ')'))
287
                ) {
288
                    // No brackets were expected.
289 2
                    break;
290
                }
291 135
                if ($token->value === '(') {
292 45
                    ++$brackets;
293 45
                    if ((empty($ret->function)) && ($prev[1] !== null)
294 11
                        && (($prev[1]->type === Token::TYPE_NONE)
295 10
                        || ($prev[1]->type === Token::TYPE_SYMBOL)
296 10
                        || (($prev[1]->type === Token::TYPE_KEYWORD)
297 10
                        && ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)))
298
                    ) {
299 11
                        $ret->function = $prev[1]->value;
300
                    }
301 135
                } elseif ($token->value === ')' && $brackets == 0) {
302
                    // Not our bracket
303 6
                    break;
304 131
                } elseif ($token->value === ')') {
305 45
                    --$brackets;
306 45
                    if ($brackets === 0) {
307 45
                        if (!empty($options['parenthesesDelimited'])) {
308
                            // The current token is the last bracket, the next
309
                            // one will be outside the expression.
310 7
                            $ret->expr .= $token->token;
311 7
                            ++$list->idx;
312 7
                            break;
313
                        }
314 2
                    } elseif ($brackets < 0) {
315
                        // $parser->error('Unexpected closing bracket.', $token);
316
                        // $brackets = 0;
317
                        break;
318
                    }
319 124
                } elseif ($token->value === ',') {
320
                    // Expressions are comma-delimited.
321 79
                    if ($brackets === 0) {
322 70
                        break;
323
                    }
324
                }
325
            }
326
327
            // Saving the previous tokens.
328 260
            $prev[0] = $prev[1];
329 260
            $prev[1] = $token;
330
331 260
            if ($alias) {
332
                // An alias is expected (the keyword `AS` was previously found).
333 31
                if (!empty($ret->alias)) {
334 1
                    $parser->error('An alias was previously found.', $token);
335 1
                    break;
336
                }
337 31
                $ret->alias = $token->value;
338 31
                $alias = false;
339 260
            } elseif ($isExpr) {
340
                // Handling aliases.
341 126
                if (/* (empty($ret->alias)) && */ ($brackets === 0)
342 120
                    && (($prev[0] === null)
343 58
                    || ((($prev[0]->type !== Token::TYPE_OPERATOR)
344 25
                    || ($prev[0]->token === ')'))
345 49
                    && (($prev[0]->type !== Token::TYPE_KEYWORD)
346 2
                    || (!($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED)))))
347 112
                    && (($prev[1]->type === Token::TYPE_STRING)
348 112
                    || (($prev[1]->type === Token::TYPE_SYMBOL)
349 7
                    && (!($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE)))
350 112
                    || ($prev[1]->type === Token::TYPE_NONE))
351
                ) {
352 6
                    if (!empty($ret->alias)) {
353 2
                        $parser->error('An alias was previously found.', $token);
354 2
                        break;
355
                    }
356 5
                    $ret->alias = $prev[1]->value;
357
                } else {
358 126
                    $ret->expr .= $token->token;
359
                }
360
            } elseif (!$isExpr) {
361 240
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '.')) {
362
                    // Found a `.` which means we expect a column name and
363
                    // the column name we parsed is actually the table name
364
                    // and the table name is actually a database name.
365 29
                    if ((!empty($ret->database)) || ($dot)) {
366 2
                        $parser->error('Unexpected dot.', $token);
367
                    }
368 29
                    $ret->database = $ret->table;
369 29
                    $ret->table = $ret->column;
370 29
                    $ret->column = null;
371 29
                    $dot = true;
372 29
                    $ret->expr .= $token->token;
373
                } else {
374 240
                    $field = empty($options['field']) ? 'column' : $options['field'];
375 240
                    if (empty($ret->$field)) {
376 240
                        $ret->$field = $token->value;
377 240
                        $ret->expr .= $token->token;
378 240
                        $dot = false;
379
                    } else {
380
                        // No alias is expected.
381 12
                        if (!empty($options['breakOnAlias'])) {
382 3
                            break;
383
                        }
384 9
                        if (!empty($ret->alias)) {
385 2
                            $parser->error('An alias was previously found.', $token);
386 2
                            break;
387
                        }
388 8
                        $ret->alias = $token->value;
389
                    }
390
                }
391
            }
392
        }
393
394 264
        if ($alias) {
395 3
            $parser->error(
396 3
                'An alias was expected.',
397 3
                $list->tokens[$list->idx - 1]
398
            );
399
        }
400
401
        // White-spaces might be added at the end.
402 264
        $ret->expr = trim($ret->expr);
403
404 264
        if ($ret->expr === '') {
405 10
            return null;
406
        }
407
408 260
        --$list->idx;
409
410 260
        return $ret;
411
    }
412
413
    /**
414
     * @param Expression|Expression[] $component the component to be built
415
     * @param array                   $options   parameters for building
416
     *
417
     * @return string
418
     */
419 54
    public static function build($component, array $options = array())
420
    {
421 54
        if (is_array($component)) {
422 1
            return implode($component, ', ');
423
        }
424
425 54
        if ($component->expr !== '' && !is_null($component->expr)) {
426 48
            $ret = $component->expr;
427
        } else {
428 8
            $fields = array();
429 8
            if ((isset($component->database)) && ($component->database !== '')) {
430 1
                $fields[] = $component->database;
431
            }
432 8
            if ((isset($component->table)) && ($component->table !== '')) {
433 8
                $fields[] = $component->table;
434
            }
435 8
            if ((isset($component->column)) && ($component->column !== '')) {
436 1
                $fields[] = $component->column;
437
            }
438 8
            $ret = implode('.', Context::escape($fields));
439
        }
440
441 54
        if (!empty($component->alias)) {
442 4
            $ret .= ' AS ' . Context::escape($component->alias);
443
        }
444
445 54
        return $ret;
446
    }
447
}
448