Passed
Pull Request — master (#363)
by
unknown
08:52
created

WithStatement   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 134
c 3
b 0
f 0
dl 0
loc 316
ccs 70
cts 70
cp 1
rs 7.92
wmc 51

3 Methods

Rating   Name   Duplication   Size   Complexity  
D parse() 0 203 39
B getSubTokenList() 0 37 7
A build() 0 18 5

How to fix   Complexity   

Complex Class

Complex classes like WithStatement 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 WithStatement, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * `WITH` statement.
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\SqlParser\Statements;
9
10
use PhpMyAdmin\SqlParser\Components\Array2d;
11
use PhpMyAdmin\SqlParser\Components\OptionsArray;
12
use PhpMyAdmin\SqlParser\Components\WithKeyword;
13
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
14
use PhpMyAdmin\SqlParser\Parser;
15
use PhpMyAdmin\SqlParser\Statement;
16
use PhpMyAdmin\SqlParser\Token;
17
use PhpMyAdmin\SqlParser\TokensList;
18
use PhpMyAdmin\SqlParser\Translator;
19
20
use function array_slice;
21
use function count;
22
23
/**
24
 * `WITH` statement.
25
26
 *  WITH [RECURSIVE] query_name [ (column_name [,...]) ] AS (SELECT ...) [, ...]
27
 */
28
final class WithStatement extends Statement
29
{
30
    /**
31
     * Options for `WITH` statements and their slot ID.
32
     *
33
     * @var mixed[]
34
     */
35
    public static $OPTIONS = ['RECURSIVE' => 1];
36
37
    /**
38
     * The clauses of this statement, in order.
39
     *
40
     * @see Statement::$CLAUSES
41
     *
42
     * @var mixed[]
43
     */
44
    public static $CLAUSES = [
45
        'WITH' => [
46
            'WITH',
47
            2,
48
        ],
49
        // Used for options.
50
        '_OPTIONS' => [
51
            '_OPTIONS',
52
            1,
53
        ],
54
        'AS' => [
55
            'AS',
56
            2,
57
        ],
58
    ];
59
60
    /** @var WithKeyword[] */
61
    public $withers = [];
62
63
    /**
64
     * holds the CTE parser.
65
     *
66
     * @var Parser|null
67 16
     */
68
    public $cteStatementParser;
69 16
70
    /**
71
     * @param Parser     $parser the instance that requests parsing
72 16
     * @param TokensList $list   the list of tokens to be parsed
73 16
     */
74
    public function parse(Parser $parser, TokensList $list)
75
    {
76
        /**
77
         * The state of the parser.
78
         *
79
         * Below are the states of the parser.
80
         *
81
         *      0 ---------------- [ name ] -----------------> 1
82
         *
83
         *      1 ------------------ [ ( ] ------------------> 2
84
         *
85
         *      2 ------------------ [ AS ] -----------------> 3
86 16
         *
87 16
         *      3 ------------------ [ ( ] ------------------> 4
88
         *
89 16
         *      4 ------------------ [ , ] ------------------> 1
90
         *
91
         *      4 ----- [ SELECT/UPDATE/DELETE/INSERT ] -----> 5
92
         *
93
         * @var int
94
         */
95 16
        $state = 0;
96
        $wither = null;
97
98 16
        ++$list->idx; // Skipping `WITH`.
99 12
100
        // parse any options if provided
101
        $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
102 16
        ++$list->idx;
103 16
104 16
        for (; $list->idx < $list->count; ++$list->idx) {
105 16
            /**
106 16
             * Token parsed at this moment.
107
             *
108
             * @var Token
109 16
             */
110 16
            $token = $list->tokens[$list->idx];
111 12
112 12
            // Skipping whitespaces and comments.
113
            if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
114
                continue;
115 16
            }
116 16
117 16
            if ($state === 0) {
118 16
                if ($token->type !== Token::TYPE_NONE) {
119
                    $parser->error('The name of the CTE was expected.', $token);
120 16
                    break;
121 16
                }
122 16
123 16
                $wither = $token->value;
124 16
                $this->withers[$wither] = new WithKeyword($wither);
125 4
                $state = 1;
126 4
            } elseif ($state === 1) {
127
                if ($token->type === Token::TYPE_OPERATOR && $token->value === '(') {
128
                    $this->withers[$wither]->columns = Array2d::parse($parser, $list);
129 12
                    $state = 2;
130
                } elseif ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'AS') {
131 12
                    $state = 3;
132 4
                } else {
133 4
                    $parser->error('Unexpected token.', $token);
134
                    break;
135
                }
136
            } elseif ($state === 2) {
137 12
                if (! ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'AS')) {
138 12
                    $parser->error('AS keyword was expected.', $token);
139
                    break;
140
                }
141
142 12
                $state = 3;
143 4
            } elseif ($state === 3) {
144 4
                $idxBeforeGetNext = $list->idx;
145 4
146
                // We want to get the next non-comment and non-space token after $token
147
                // therefore, the first getNext call will start with the current $idx which's $token,
148
                // will return it and increase $idx by 1, which's not guaranteed to be non-comment
149 12
                // and non-space, that's why we're calling getNext again.
150
                $list->getNext();
151
                $nextKeyword = $list->getNext();
152
153 16
                if (! ($token->value === '(' && ($nextKeyword && $nextKeyword->value === 'SELECT'))) {
154 16
                    $parser->error('Subquery of the CTE was expected.', $token);
155
                    $list->idx = $idxBeforeGetNext;
156
                    break;
157
                }
158
159 8
                // Restore the index
160
                $list->idx = $idxBeforeGetNext;
161 8
162
                ++$list->idx;
163 8
                $subList = $this->getSubTokenList($list);
164 8
                if ($subList instanceof ParserException) {
165 8
                    $parser->errors[] = $subList;
166
                    break;
167
                }
168 8
169
                $subParser = new Parser($subList);
170
171
                if (count($subParser->errors)) {
172
                    foreach ($subParser->errors as $error) {
173
                        $parser->errors[] = $error;
174
                    }
175
176 16
                    break;
177
                }
178 16
179
                $this->withers[$wither]->statement = $subParser;
180 16
181 16
                $state = 4;
182
            } elseif ($state === 4) {
183 16
                if ($token->value === ',') {
184 16
                    // There's another WITH expression to parse, go back to state=0
185 8
                    $state = 0;
186 16
                    continue;
187 16
                }
188 12
189
                if (
190
                    $token->type === Token::TYPE_KEYWORD && (
191
                    $token->value === 'SELECT'
192 16
                    || $token->value === 'INSERT'
193 16
                    || $token->value === 'UPDATE'
194 4
                    || $token->value === 'DELETE'
195
                    )
196
                ) {
197 16
                    $state = 5;
198
                    --$list->idx;
199
                    continue;
200
                }
201 16
202 4
                $parser->error('An expression was expected.', $token);
203
                break;
204 4
            } elseif ($state === 5) {
205 4
                /**
206 1
                 * We need to parse all of the remaining tokens becuase mostly, they are only the CTE expression
207
                 * which's mostly is SELECT, or INSERT, UPDATE, or delete statement.
208
                 * e.g: INSERT .. ( SELECT 1 ) SELECT col1 FROM cte ON DUPLICATE KEY UPDATE col_name = 3.
209
                 * The issue is that, `ON DUPLICATE KEY UPDATE col_name = 3` is related to the main INSERT query
210 12
                 * not the cte expression (SELECT col1 FROM cte) we need to determine the end of the expression
211
                 * to parse `ON DUPLICATE KEY UPDATE` from the InsertStatement parser instead.
212 12
                 */
213
214
                // Index of the last parsed token by default would be the last token in the $list, because we're
215
                // assuming that all remaining tokens at state 4, are related to the expression.
216
                $idxOfLastParsedToken = $list->count - 1;
217
                // Index before search to be able to restore the index.
218
                $idxBeforeSearch = $list->idx;
219
                // Length of expression tokens is null by default, in order for the $subList to start
220
                // from $list->idx to the end of the $list.
221
                $lengthOfExpressionTokens = null;
222
223
                if ($list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ON')) {
224
                    // (-1) because getNextOfTypeAndValue returned ON and increased the index.
225
                    $idxOfOn = $list->idx - 1;
226
                    // We want to make sure that it's `ON DUPLICATE KEY UPDATE`
227
                    $dubplicateToken = $list->getNext();
228
                    $keyToken = $list->getNext();
229
                    $updateToken = $list->getNext();
230
                    if (
231
                        $dubplicateToken && $dubplicateToken->keyword === 'DUPLICATE'
232
                        && ($keyToken && $keyToken->keyword === 'KEY')
233
                        && ($updateToken && $updateToken->keyword === 'UPDATE')
234
                    ) {
235
                        // Index of the last parsed token will be the token before the ON Keyword
236
                        $idxOfLastParsedToken = $idxOfOn - 1;
237
                        // The length of the expression tokens would be the difference
238
                        // between the first unrelated token `ON` and the idx
239
                        // before skipping the CTE tokens.
240
                        $lengthOfExpressionTokens = $idxOfOn - $idxBeforeSearch;
241
                    }
242
                }
243
244
                // Restore the index
245
                $list->idx = $idxBeforeSearch;
246
247
                $subList = new TokensList(array_slice($list->tokens, $list->idx, $lengthOfExpressionTokens));
248
                $subParser = new Parser($subList);
249
                if (count($subParser->errors)) {
250
                    foreach ($subParser->errors as $error) {
251
                        $parser->errors[] = $error;
252
                    }
253
254
                    break;
255
                }
256
257
                $this->cteStatementParser = $subParser;
258
259
                $list->idx = $idxOfLastParsedToken;
260
                break;
261
            }
262
        }
263
264
        // 5 is the only valid end state
265
        if ($state !== 5) {
0 ignored issues
show
introduced by
The condition $state !== 5 is always true.
Loading history...
266
             /**
267
             * Token parsed at this moment.
268
             *
269
             * @var Token
270
             */
271
            $token = $list->tokens[$list->idx];
272
273
            $parser->error('Unexpected end of the WITH CTE.', $token);
274
        }
275
276
        --$list->idx;
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     */
282
    public function build()
283
    {
284
        $str = 'WITH ';
285
286
        foreach ($this->withers as $wither) {
287
            $str .= $str === 'WITH ' ? '' : ', ';
288
            $str .= WithKeyword::build($wither);
289
        }
290
291
        $str .= ' ';
292
293
        if ($this->cteStatementParser) {
294
            foreach ($this->cteStatementParser->statements as $statement) {
295
                    $str .= $statement->build();
296
            }
297
        }
298
299
        return $str;
300
    }
301
302
    /**
303
     * Get tokens within the WITH expression to use them in another parser
304
     *
305
     * @return ParserException|TokensList
306
     */
307
    private function getSubTokenList(TokensList $list)
308
    {
309
        $idx = $list->idx;
310
        /** @var Token $token */
311
        $token = $list->tokens[$list->idx];
312
        $openParenthesis = 0;
313
314
        while ($list->idx < $list->count) {
315
            if ($token->value === '(') {
316
                ++$openParenthesis;
317
            } elseif ($token->value === ')') {
318
                if (--$openParenthesis === -1) {
319
                    break;
320
                }
321
            }
322
323
            ++$list->idx;
324
            if (! isset($list->tokens[$list->idx])) {
325
                break;
326
            }
327
328
            $token = $list->tokens[$list->idx];
329
        }
330
331
        // performance improvement: return the error to avoid a try/catch in the loop
332
        if ($list->idx === $list->count) {
333
            --$list->idx;
334
335
            return new ParserException(
336
                Translator::gettext('A closing bracket was expected.'),
337
                $token
338
            );
339
        }
340
341
        $length = $list->idx - $idx;
342
343
        return new TokensList(array_slice($list->tokens, $idx, $length), $length);
344
    }
345
}
346