Passed
Push — master ( eccca0...6f769c )
by Maurício
03:09 queued 12s
created

WithStatement   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 137
c 3
b 0
f 0
dl 0
loc 309
ccs 128
cts 128
cp 1
rs 6.96
wmc 53

3 Methods

Rating   Name   Duplication   Size   Complexity  
A build() 0 18 5
B getSubTokenList() 0 36 7
D parse() 0 200 41

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