Completed
Pull Request — master (#143)
by Deven
03:58
created

LoadStatement::parseKeywordsAccordingToState()   C

Complexity

Conditions 16
Paths 25

Size

Total Lines 54
Code Lines 41

Duplication

Lines 21
Ratio 38.89 %

Code Coverage

Tests 39
CRAP Score 16

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 21
loc 54
ccs 39
cts 39
cp 1
rs 6.6856
cc 16
eloc 41
nc 25
nop 3
crap 16

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
 * `LOAD` statement.
5
 */
6
7
namespace PhpMyAdmin\SqlParser\Statements;
8
9
use PhpMyAdmin\SqlParser\Components\ArrayObj;
10
use PhpMyAdmin\SqlParser\Components\Condition;
11
use PhpMyAdmin\SqlParser\Components\Expression;
12
use PhpMyAdmin\SqlParser\Components\ExpressionArray;
13
use PhpMyAdmin\SqlParser\Components\FunctionCall;
14
use PhpMyAdmin\SqlParser\Components\IntoKeyword;
15
use PhpMyAdmin\SqlParser\Components\JoinKeyword;
16
use PhpMyAdmin\SqlParser\Components\Limit;
17
use PhpMyAdmin\SqlParser\Components\OrderKeyword;
18
use PhpMyAdmin\SqlParser\Components\OptionsArray;
19
use PhpMyAdmin\SqlParser\Components\SetOperation;
20
use PhpMyAdmin\SqlParser\Parser;
21
use PhpMyAdmin\SqlParser\Statement;
22
use PhpMyAdmin\SqlParser\Token;
23
use PhpMyAdmin\SqlParser\TokensList;
24
25
/**
26
 * `LOAD` statement.
27
 *
28
 * LOAD DATA [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name'
29
 *   [REPLACE | IGNORE]
30
 *   INTO TABLE tbl_name
31
 *   [PARTITION (partition_name,...)]
32
 *   [CHARACTER SET charset_name]
33
 *   [{FIELDS | COLUMNS}
34
 *       [TERMINATED BY 'string']
35
 *       [[OPTIONALLY] ENCLOSED BY 'char']
36
 *       [ESCAPED BY 'char']
37
 *   ]
38
 *   [LINES
39
 *       [STARTING BY 'string']
40
 *       [TERMINATED BY 'string']
41
 *  ]
42
 *   [IGNORE number {LINES | ROWS}]
43
 *   [(col_name_or_user_var,...)]
44
 *   [SET col_name = expr,...]
45
 *
46
 *
47
 * @category   Statements
48
 *
49
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
50
 */
51
class LoadStatement extends Statement
52
{
53
    /**
54
     * Options for `LOAD` statements and their slot ID.
55
     *
56
     * @var array
57
     */
58
    public static $OPTIONS = array(
59
        'LOW_PRIORITY' => 1,
60
        'CONCURRENT' => 1,
61
        'LOCAL' => 2
62
    );
63
64
    /**
65
     * FIELDS/COLUMNS Options for `LOAD DATA...INFILE` statements.
66
     *
67
     * @var array
68
     */
69
    public static $FIELDS_OPTIONS = array(
70
        'TERMINATED BY' => array(1, 'expr'),
71
        'OPTIONALLY' => 2,
72
        'ENCLOSED BY' => array(3, 'expr'),
73
        'ESCAPED BY' => array(4, 'expr'),
74
    );
75
76
    /**
77
     * LINES Options for `LOAD DATA...INFILE` statements.
78
     *
79
     * @var array
80
     */
81
    public static $LINES_OPTIONS = array(
82
        'STARTING BY' => array(1, 'expr'),
83
        'TERMINATED BY' => array(2, 'expr'),
84
    );
85
86
    /**
87
     * File name being used to load data
88
     *
89
     * @var Expression
90
     */
91
    public $file_name;
92
93
    /**
94
     * Table used as destination for this statement.
95
     *
96
     * @var Expression
97
     */
98
    public $table;
99
100
    /**
101
     * Partitions used as source for this statement.
102
     *
103
     * @var ArrayObj
104
     */
105
    public $partition;
106
107
    /**
108
     * Character set used in this statement.
109
     *
110
     * @var Expression
111
     */
112
    public $charset_name;
113
114
    /**
115
     * Options for FIELDS/COLUMNS keyword.
116
     *
117
     * @var OptionsArray
118
     *
119
     * @see static::$FIELDS_OPTIONS
120
     */
121
    public $fields_options;
122
123
    /**
124
     * Whether to use `FIELDS` or `COLUMNS` while building.
125
     *
126
     * @var bool
127
     */
128
    public $fields_keyword;
129
130
    /**
131
     * Options for OPTIONS keyword.
132
     *
133
     * @var OptionsArray
134
     *
135
     * @see static::$LINES_OPTIONS
136
     */
137
    public $lines_options;
138
139
    /**
140
     * Column names or user variables
141
     *
142
     * @var ExpressionArray
143
     */
144
    public $col_name_or_user_var;
145
146
    /**
147
     * SET clause's updated values(optional)
148
     *
149
     * @var SetOperation[]
150
     */
151
    public $set;
152
153
    /**
154
     * Ignore 'number' LINES/ROWS
155
     *
156
     * @var Expression
157
     */
158
    public $ignore_number;
159
160
    /**
161
     * REPLACE/IGNORE Keyword
162
     *
163
     * @var string
164
     */
165
    public $replace_ignore;
166
167
    /**
168
     * LINES/ROWS Keyword
169
     *
170
     * @var string
171
     */
172
    public $lines_rows;
173
174
    /**
175
     * @return string
176
     */
177 1
    public function build()
178
    {
179 1
        $ret = 'LOAD DATA ' . $this->options
180 1
            . ' INFILE ' . $this->file_name;
181
182 1
        if ($this->replace_ignore !== null) {
183 1
            $ret .= ' ' . trim($this->replace_ignore);
184
        }
185
186 1
        $ret .= ' INTO TABLE ' . $this->table;
187
188 1
        if ($this->partition !== null && count($this->partition) > 0) {
189 1
            $ret .= ' PARTITION ' . ArrayObj::build($this->partition);
190
        }
191
192 1
        if ($this->charset_name !== null) {
193 1
            $ret .= ' CHARACTER SET ' . $this->charset_name;
194
        }
195
196 1
        if ($this->fields_keyword !== null) {
197 1
            $ret .= ' ' . $this->fields_keyword . ' ' . $this->fields_options;
198
        }
199
200 1
        if ($this->lines_options !== null && count($this->lines_options) > 0) {
201 1
            $ret .= ' LINES ' . $this->lines_options;
202
        }
203
204 1
        if ($this->ignore_number !== null) {
205 1
            $ret .= ' IGNORE ' . $this->ignore_number . ' ' . $this->lines_rows;
206
        }
207
208 1 View Code Duplication
        if ($this->col_name_or_user_var !== null && count($this->col_name_or_user_var) > 0) {
209 1
            $ret .= ' ' . ExpressionArray::build($this->col_name_or_user_var);
0 ignored issues
show
Documentation introduced by
$this->col_name_or_user_var is of type object<PhpMyAdmin\SqlPar...onents\ExpressionArray>, but the function expects a array<integer,object<Php...nents\ExpressionArray>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
210
        }
211
212 1
        if ($this->set !== null && count($this->set) > 0) {
213 1
            $ret .= ' SET ' . SetOperation::build($this->set);
214
        }
215
216 1
        return $ret;
217
    }
218
219
    /**
220
     * @param Parser     $parser the instance that requests parsing
221
     * @param TokensList $list   the list of tokens to be parsed
222
     */
223 14
    public function parse(Parser $parser, TokensList $list)
224
    {
225 14
        ++$list->idx; // Skipping `LOAD DATA`.
226
227
        // parse any options if provided
228 14
        $this->options = OptionsArray::parse(
229
            $parser,
230
            $list,
231 14
            static::$OPTIONS
232
        );
233 14
        ++$list->idx;
234
235
                /**
236
         * The state of the parser.
237
         *
238
         * @var int
239
         */
240 14
        $state = 0;
241
242 14
        for (; $list->idx < $list->count; ++$list->idx) {
243
            /**
244
             * Token parsed at this moment.
245
             *
246
             * @var Token
247
             */
248 14
            $token = $list->tokens[$list->idx];
249
250
            // End of statement.
251 14
            if ($token->type === Token::TYPE_DELIMITER) {
252 8
                break;
253
            }
254
255
            // Skipping whitespaces and comments.
256 14
            if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
257 12
                continue;
258
            }
259
260 14
            if ($state === 0) {
261 14
                if ($token->type === Token::TYPE_KEYWORD
262 14
                    && $token->keyword !== 'INFILE'
263
                ) {
264 1
                    $parser->error('Unexpected keyword.', $token);
265 1
                    break;
266 13
                } elseif ($token->type !== Token::TYPE_KEYWORD) {
267 1
                    $parser->error('Unexpected token1.', $token);
268 1
                    break;
269
                }
270
271 12
                ++$list->idx;
272 12
                $this->file_name = Expression::parse(
273
                    $parser,
274
                    $list,
275 12
                    array('parseField' => 'file')
276
                );
277 12
                $state = 1;
278 12
            } elseif ($state === 1) {
279 12
                if (($token->type === Token::TYPE_KEYWORD)
280 12
                    && ($token->keyword === 'REPLACE'
281 12
                    || $token->keyword === 'IGNORE')
282
                ) {
283 8
                    $this->replace_ignore = trim($token->keyword);
284 12
                } elseif ($token->type === Token::TYPE_KEYWORD
285 12
                    && $token->keyword === 'INTO'
286
                ) {
287 12
                    $state = 2;
288
                }
289 12
            } elseif ($state === 2) {
290 12
                if ($token->type === Token::TYPE_KEYWORD
291 12
                    && $token->keyword === 'TABLE'
292
                ) {
293 11
                    $list->idx++;
294 11
                    $this->table = Expression::parse($parser, $list, array('parseField' => 'table'));
295 11
                    $state = 3;
296
                } else {
297 1
                    $parser->error('Unexpected token2.', $token);
298 12
                    break;
299
                }
300 9
            } elseif ($state >= 3 && $state <= 7) {
301 9
                if ($token->type === Token::TYPE_KEYWORD) {
302 8
                    $newState = $this->parseKeywordsAccordingToState(
303
                        $parser, $list, $state
304
                    );
305 8
                    if ($newState === $state) {
306
                        // Avoid infinite loop
307 8
                        break;
308
                    }
309 4
                } elseif ($token->type === Token::TYPE_OPERATOR
310 4
                    && $token->token === '('
311
                ) {
312 3
                    $this->col_name_or_user_var
313 3
                        = ExpressionArray::parse($parser, $list);
0 ignored issues
show
Documentation Bug introduced by
It seems like \PhpMyAdmin\SqlParser\Co...::parse($parser, $list) of type array<integer,object<Php...Components\Expression>> is incompatible with the declared type object<PhpMyAdmin\SqlPar...onents\ExpressionArray> of property $col_name_or_user_var.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
314 3
                    $state = 7;
315
                } else {
316 1
                    $parser->error('Unexpected token3.', $token);
317 1
                    break;
318
                }
319
            }
320
        }
321
322 14
        --$list->idx;
323 14
    }
324
325 6
    public function parseFileOptions(Parser $parser, TokensList $list, $keyword = 'FIELDS')
326
    {
327 6
        ++$list->idx;
328
329 6
        if ($keyword === 'FIELDS' || $keyword === 'COLUMNS') {
330
            // parse field options
331 6
            $this->fields_options = OptionsArray::parse(
332
                $parser,
333
                $list,
334 6
                static::$FIELDS_OPTIONS
335
            );
336
337 6
            $this->fields_keyword = $keyword;
0 ignored issues
show
Documentation Bug introduced by
The property $fields_keyword was declared of type boolean, but $keyword is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
338
        } else {
339
            // parse line options
340 3
            $this->lines_options = OptionsArray::parse(
341
                $parser,
342
                $list,
343 3
                static::$LINES_OPTIONS
344
            );
345
        }
346 6
    }
347
348
349 8
    public function parseKeywordsAccordingToState($parser, $list, $state) {
350 8
        $token = $list->tokens[$list->idx];
351
352
        switch ($state) {
353 8 View Code Duplication
            case 3:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
354 8
                if ($token->keyword === 'PARTITION') {
355 2
                    $list->idx++;
356 2
                    $this->partition = ArrayObj::parse($parser, $list);
0 ignored issues
show
Documentation Bug introduced by
It seems like \PhpMyAdmin\SqlParser\Co...::parse($parser, $list) can also be of type array. However, the property $partition is declared as type object<PhpMyAdmin\SqlParser\Components\ArrayObj>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
357 2
                    $state = 4;
358 2
                    return $state;
359
                }
360 3 View Code Duplication
            case 4:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
361 8
                if ($token->keyword === 'CHARACTER SET') {
362 3
                    $list->idx++;
363 3
                    $this->charset_name = Expression::parse($parser, $list);
364 3
                    $state = 5;
365 3
                    return $state;
366
                }
367 3
            case 5:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
368 8
                if ($token->keyword === 'FIELDS'
369 7
                    || $token->keyword === 'COLUMNS'
370 8
                    || $token->keyword === 'LINES'
371
                ) {
372 6
                    $this->parseFileOptions($parser, $list, $token->value);
373 6
                    $state = 6;
374 6
                    return $state;
375
                }
376 3
            case 6:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
377 7
                if ($token->keyword === 'IGNORE') {
378 5
                    $list->idx++;
379
380 5
                    $this->ignore_number = Expression::parse($parser, $list);
381 5
                    $nextToken = $list->getNextOfType(Token::TYPE_KEYWORD);
382
383 5
                    if ($nextToken->type === Token::TYPE_KEYWORD
384 5
                        && (($nextToken->keyword === 'LINES')
385 5
                        || ($nextToken->keyword === 'ROWS'))
386
                    ) {
387 5
                        $this->lines_rows = $nextToken->token;
388
                    }
389 5
                    $state = 7;
390 5
                    return $state;
391
                }
392 3 View Code Duplication
            case 7:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
393 5
                if ($token->keyword === 'SET') {
394 3
                    $list->idx++;
395 3
                    $this->set = SetOperation::parse($parser, $list);
396 3
                    $state = 8;
397 3
                    return $state;
398
                }
399
            default:
400
        }
401 2
        return $state;
402
    }
403
404
}
405