Completed
Push — master ( 65f66e...428edc )
by Michal
04:14
created

Statement   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 481
Duplicated Lines 1.66 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.24%

Importance

Changes 0
Metric Value
dl 8
loc 481
ccs 167
cts 170
cp 0.9824
rs 5.7894
c 0
b 0
f 0
wmc 65
lcom 1
cbo 5

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 3
C build() 0 88 7
D parse() 8 191 34
A before() 0 3 1
A after() 0 3 1
A getClauses() 0 4 1
A __toString() 0 4 1
C validateClauseOrder() 0 61 17

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Statement 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Statement, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * The result of the parser is an array of statements are extensions of the
5
 * class defined here.
6
 *
7
 * A statement represents the result of parsing the lexemes.
8
 */
9
10
namespace PhpMyAdmin\SqlParser;
11
12
use PhpMyAdmin\SqlParser\Components\OptionsArray;
13
14
/**
15
 * Abstract statement definition.
16
 *
17
 * @category Statements
18
 *
19
 * @license  https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
20
 */
21
abstract class Statement
22
{
23
    /**
24
     * Options for this statement.
25
     *
26
     * The option would be the key and the value can be an integer or an array.
27
     *
28
     * The integer represents only the index used.
29
     *
30
     * The array may have two keys: `0` is used to represent the index used and
31
     * `1` is the type of the option (which may be 'var' or 'var='). Both
32
     * options mean they expect a value after the option (e.g. `A = B` or `A B`,
33
     * in which case `A` is the key and `B` is the value). The only difference
34
     * is in the building process. `var` options are built as `A B` and  `var=`
35
     * options are built as `A = B`
36
     *
37
     * Two options that can be used together must have different values for
38
     * indexes, else, when they will be used together, an error will occur.
39
     *
40
     * @var array
41
     */
42
    public static $OPTIONS = array();
43
44
    /**
45
     * The clauses of this statement, in order.
46
     *
47
     * The value attributed to each clause is used by the builder and it may
48
     * have one of the following values:
49
     *
50
     *     - 1 = 01 - add the clause only
51
     *     - 2 = 10 - add the keyword
52
     *     - 3 = 11 - add both the keyword and the clause
53
     *
54
     * @var array
55
     */
56
    public static $CLAUSES = array();
57
58
    /**
59
     * The options of this query.
60
     *
61
     * @var OptionsArray
62
     *
63
     * @see static::$OPTIONS
64
     */
65
    public $options;
66
67
    /**
68
     * The index of the first token used in this statement.
69
     *
70
     * @var int
71
     */
72
    public $first;
73
74
    /**
75
     * The index of the last token used in this statement.
76
     *
77
     * @var int
78
     */
79
    public $last;
80
81
    /**
82
     * Constructor.
83
     *
84
     * @param Parser     $parser the instance that requests parsing
0 ignored issues
show
Documentation introduced by
Should the type for parameter $parser not be null|Parser?

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...
85
     * @param TokensList $list   the list of tokens to be parsed
0 ignored issues
show
Documentation introduced by
Should the type for parameter $list not be null|TokensList?

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...
86
     */
87 246
    public function __construct(Parser $parser = null, TokensList $list = null)
88
    {
89 246
        if (($parser !== null) && ($list !== null)) {
90 244
            $this->parse($parser, $list);
91 244
        }
92 246
    }
93
94
    /**
95
     * Builds the string representation of this statement.
96
     *
97
     * @return string
98
     */
99 11
    public function build()
100
    {
101
        /**
102
         * Query to be returned.
103
         *
104
         * @var string
105
         */
106 11
        $query = '';
107
108
        /**
109
         * Clauses which were built already.
110
         *
111
         * It is required to keep track of built clauses because some fields,
112
         * for example `join` is used by multiple clauses (`JOIN`, `LEFT JOIN`,
113
         * `LEFT OUTER JOIN`, etc.). The same happens for `VALUE` and `VALUES`.
114
         *
115
         * A clause is considered built just after fields' value
116
         * (`$this->field`) was used in building.
117
         *
118
         * @var array
119
         */
120 11
        $built = array();
121
122
        /**
123
         * Statement's clauses.
124
         *
125
         * @var array
126
         */
127 11
        $clauses = $this->getClauses();
128
129 11
        foreach ($clauses as $clause) {
130
            /**
131
             * The name of the clause.
132
             *
133
             * @var string
134
             */
135 10
            $name = $clause[0];
136
137
            /**
138
             * The type of the clause.
139
             *
140
             * @see self::$CLAUSES
141
             *
142
             * @var int
143
             */
144 10
            $type = $clause[1];
145
146
            /**
147
             * The builder (parser) of this clause.
148
             *
149
             * @var Component
150
             */
151 10
            $class = Parser::$KEYWORD_PARSERS[$name]['class'];
152
153
            /**
154
             * The name of the field that is used as source for the builder.
155
             * Same field is used to store the result of parsing.
156
             *
157
             * @var string
158
             */
159 10
            $field = Parser::$KEYWORD_PARSERS[$name]['field'];
160
161
            // The field is empty, there is nothing to be built.
162 10
            if (empty($this->$field)) {
163 10
                continue;
164
            }
165
166
            // Checking if this field was already built.
167 10
            if ($type & 1) {
168 10
                if (!empty($built[$field])) {
169 2
                    continue;
170
                }
171 10
                $built[$field] = true;
172 10
            }
173
174
            // Checking if the name of the clause should be added.
175 10
            if ($type & 2) {
176 10
                $query .= $name . ' ';
177 10
            }
178
179
            // Checking if the result of the builder should be added.
180 10
            if ($type & 1) {
181 10
                $query .= $class::build($this->$field) . ' ';
182 10
            }
183 11
        }
184
185 11
        return $query;
186
    }
187
188
    /**
189
     * Parses the statements defined by the tokens list.
190
     *
191
     * @param Parser     $parser the instance that requests parsing
192
     * @param TokensList $list   the list of tokens to be parsed
193
     */
194 136
    public function parse(Parser $parser, TokensList $list)
195
    {
196
        /**
197
         * Array containing all list of clauses parsed.
198
         * This is used to check for duplicates.
199
         *
200
         * @var array
201
         */
202 135
        $parsedClauses = array();
203
204
        // This may be corrected by the parser.
205 135
        $this->first = $list->idx;
206
207
        /**
208
         * Whether options were parsed or not.
209
         * For statements that do not have any options this is set to `true` by
210
         * default.
211
         *
212
         * @var bool
213
         */
214 135
        $parsedOptions = empty(static::$OPTIONS);
215
216 135
        for (; $list->idx < $list->count; ++$list->idx) {
217
            /**
218
             * Token parsed at this moment.
219
             *
220
             * @var Token
221
             */
222 135
            $token = $list->tokens[$list->idx];
223
224
            // End of statement.
225 135
            if ($token->type === Token::TYPE_DELIMITER) {
226 134
                break;
227
            }
228
229
            // Checking if this closing bracket is the pair for a bracket
230
            // outside the statement.
231 135
            if (($token->value === ')') && ($parser->brackets > 0)) {
232 3
                --$parser->brackets;
233 3
                continue;
234
            }
235
236
            // Only keywords are relevant here. Other parts of the query are
237
            // processed in the functions below.
238 135
            if ($token->type !== Token::TYPE_KEYWORD) {
239 11 View Code Duplication
                if (($token->type !== TOKEN::TYPE_COMMENT)
240 11
                    && ($token->type !== Token::TYPE_WHITESPACE)
241 11
                ) {
242 6
                    $parser->error(__('Unexpected token.'), $token);
243 6
                }
244 11
                continue;
245
            }
246
247
            // Unions are parsed by the parser because they represent more than
248
            // one statement.
249 135 View Code Duplication
            if (($token->value === 'UNION') || ($token->value === 'UNION ALL') || ($token->value === 'UNION DISTINCT')) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
250 5
                break;
251
            }
252
253 135
            $lastIdx = $list->idx;
254
255
            // ON DUPLICATE KEY UPDATE ...
256
            // has to be parsed in parent statement (INSERT or REPLACE)
257
            // so look for it and break
258 135
            if (get_class($this) === 'PhpMyAdmin\SqlParser\Statements\SelectStatement'
259 135
                && $token->value === 'ON'
260 135
            ) {
261 2
                ++$list->idx; // Skip ON
262
263
                // look for ON DUPLICATE KEY UPDATE
264 2
                $first = $list->getNextOfType(Token::TYPE_KEYWORD);
265 2
                $second = $list->getNextOfType(Token::TYPE_KEYWORD);
266 2
                $third = $list->getNextOfType(Token::TYPE_KEYWORD);
267
268 2
                if ($first && $second && $third
269 2
                    && $first->value === 'DUPLICATE'
270 2
                    && $second->value === 'KEY'
271 2
                    && $third->value === 'UPDATE'
272 2
                ) {
273 2
                    $list->idx = $lastIdx;
274 2
                    break;
275
                }
276
            }
277 135
            $list->idx = $lastIdx;
278
279
            /**
280
             * The name of the class that is used for parsing.
281
             *
282
             * @var Component
283
             */
284 135
            $class = null;
285
286
            /**
287
             * The name of the field where the result of the parsing is stored.
288
             *
289
             * @var string
290
             */
291 135
            $field = null;
292
293
            /**
294
             * Parser's options.
295
             *
296
             * @var array
297
             */
298 135
            $options = array();
299
300
            // Looking for duplicated clauses.
301 135
            if ((!empty(Parser::$KEYWORD_PARSERS[$token->value]))
302 16
                || (!empty(Parser::$STATEMENT_PARSERS[$token->value]))
303 135
            ) {
304 136
                if (!empty($parsedClauses[$token->value])) {
305 2
                    $parser->error(
306 2
                        __('This type of clause was previously parsed.'),
307
                        $token
308 2
                    );
309 2
                    break;
310
                }
311 135
                $parsedClauses[$token->value] = true;
312 135
            }
313
314
            // Checking if this is the beginning of a clause.
315 135
            if (!empty(Parser::$KEYWORD_PARSERS[$token->value])) {
316 134
                $class = Parser::$KEYWORD_PARSERS[$token->value]['class'];
317 134
                $field = Parser::$KEYWORD_PARSERS[$token->value]['field'];
318 134
                if (!empty(Parser::$KEYWORD_PARSERS[$token->value]['options'])) {
319 101
                    $options = Parser::$KEYWORD_PARSERS[$token->value]['options'];
320 101
                }
321 135
            }
322
323
            // Checking if this is the beginning of the statement.
324 135
            if (!empty(Parser::$STATEMENT_PARSERS[$token->value])) {
325 135
                if ((!empty(static::$CLAUSES)) // Undefined for some statements.
326 135
                    && (empty(static::$CLAUSES[$token->value]))
327 135
                ) {
328
                    // Some keywords (e.g. `SET`) may be the beginning of a
329
                    // statement and a clause.
330
                    // If such keyword was found and it cannot be a clause of
331
                    // this statement it means it is a new statement, but no
332
                    // delimiter was found between them.
333 1
                    $parser->error(
334 1
                        __('A new statement was found, but no delimiter between it and the previous one.'),
335
                        $token
336 1
                    );
337 1
                    break;
338
                }
339 135
                if (!$parsedOptions) {
340 124
                    if (empty(static::$OPTIONS[$token->value])) {
341
                        // Skipping keyword because if it is not a option.
342 123
                        ++$list->idx;
343 123
                    }
344 124
                    $this->options = OptionsArray::parse(
345 124
                        $parser,
346 124
                        $list,
347
                        static::$OPTIONS
348 124
                    );
349 124
                    $parsedOptions = true;
350 124
                }
351 135
            } elseif ($class === null) {
352
                // Handle special end options in Select statement
353
                // See Statements\SelectStatement::$END_OPTIONS
354 11
                if (get_class($this) === 'PhpMyAdmin\SqlParser\Statements\SelectStatement'
355 11
                    && ($token->value === 'FOR UPDATE'
356 10
                    || $token->value === 'LOCK IN SHARE MODE')
357 11
                ) {
358 4
                    $this->end_options = OptionsArray::parse(
0 ignored issues
show
Bug introduced by
The property end_options does not seem to exist. Did you mean OPTIONS?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
359 4
                        $parser,
360 4
                        $list,
361
                        static::$END_OPTIONS
362 4
                    );
363 4
                } else {
364
                    // There is no parser for this keyword and isn't the beginning
365
                    // of a statement (so no options) either.
366 7
                    $parser->error(__('Unrecognized keyword.'), $token);
367 7
                    continue;
368
                }
369 4
            }
370
371 135
            $this->before($parser, $list, $token);
372
373
            // Parsing this keyword.
374 135
            if ($class !== null) {
375 134
                ++$list->idx; // Skipping keyword or last option.
376 134
                $this->$field = $class::parse($parser, $list, $options);
377 134
            }
378
379 135
            $this->after($parser, $list, $token);
380 135
        }
381
382
        // This may be corrected by the parser.
383 135
        $this->last = --$list->idx; // Go back to last used token.
384 135
    }
385
386
    /**
387
     * Function called before the token is processed.
388
     *
389
     * @param Parser     $parser the instance that requests parsing
390
     * @param TokensList $list   the list of tokens to be parsed
391
     * @param Token      $token  the token that is being parsed
392
     */
393 128
    public function before(Parser $parser, TokensList $list, Token $token)
394
    {
395 128
    }
396
397
    /**
398
     * Function called after the token was processed.
399
     *
400
     * @param Parser     $parser the instance that requests parsing
401
     * @param TokensList $list   the list of tokens to be parsed
402
     * @param Token      $token  the token that is being parsed
403
     */
404 131
    public function after(Parser $parser, TokensList $list, Token $token)
405
    {
406 131
    }
407
408
    /**
409
     * Gets the clauses of this statement.
410
     *
411
     * @return array
412
     */
413 159
    public function getClauses()
414
    {
415 159
        return static::$CLAUSES;
416
    }
417
418
    /**
419
     * Builds the string representation of this statement.
420
     *
421
     * @see static::build
422
     *
423
     * @return string
424
     */
425 4
    public function __toString()
426
    {
427 4
        return $this->build();
428
    }
429
430
    /**
431
     * Validates the order of the clauses in parsed statement
432
     * Ideally this should be called after successfully
433
     * completing the parsing of each statement.
434
     *
435
     * @param Parser     $parser the instance that requests parsing
436
     * @param TokensList $list   the list of tokens to be parsed
437
     *
438
     * @return bool
439
     */
440 244
    public function validateClauseOrder($parser, $list)
441
    {
442 244
        $clauses = array_flip(array_keys($this->getClauses()));
443
444 244
        if (empty($clauses)
445 136
            || count($clauses) == 0
446 244
        ) {
447 114
            return true;
448
        }
449
450 136
        $minIdx = -1;
451
452
        /**
453
         * For tracking JOIN clauses in a query
454
         *   0 - JOIN not found till now
455
         *   1 - JOIN has been found
456
         *   2 - A Non-JOIN clause has been found
457
         *       after a previously found JOIN clause.
458
         *
459
         * @var int
460
         */
461 136
        $joinStart = 0;
462
463 136
        $error = 0;
0 ignored issues
show
Unused Code introduced by
$error is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
464 136
        foreach ($clauses as $clauseType => $index) {
465 136
            $clauseStartIdx = Utils\Query::getClauseStartOffset(
466 136
                $this,
467 136
                $list,
468
                $clauseType
469 136
            );
470
471
            // Handle ordering of Multiple Joins in a query
472 136
            if ($clauseStartIdx != -1) {
473 135
                if ($joinStart == 0 && stripos($clauseType, 'JOIN') !== false) {
474 15
                    $joinStart = 1;
475 135
                } elseif ($joinStart == 1 && stripos($clauseType, 'JOIN') === false) {
476 5
                    $joinStart = 2;
477 135
                } elseif ($joinStart == 2 && stripos($clauseType, 'JOIN') !== false) {
478
                    $error = 1;
0 ignored issues
show
Unused Code introduced by
$error is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
479
                }
480 135
            }
481
482 136
            if ($clauseStartIdx != -1 && $clauseStartIdx < $minIdx) {
483 5
                if ($joinStart == 0 || ($joinStart == 2 && $error = 1)) {
484 4
                    $token = $list->tokens[$clauseStartIdx];
485 4
                    $parser->error(
486 4
                        __('Unexpected ordering of clauses.'),
487
                        $token
488 4
                    );
489
490 4
                    return false;
491
                } else {
492 1
                    $minIdx = $clauseStartIdx;
493
                }
494 136
            } elseif ($clauseStartIdx != -1) {
495 135
                $minIdx = $clauseStartIdx;
496 135
            }
497 136
        }
498
499 132
        return true;
500
    }
501
}
502