Passed
Pull Request — master (#363)
by
unknown
17:54 queued 08:04
created

WithStatement   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 113
dl 0
loc 262
ccs 70
cts 70
cp 1
rs 9.1199
c 1
b 0
f 0
wmc 41

3 Methods

Rating   Name   Duplication   Size   Complexity  
D parse() 0 164 31
B getSubTokenList() 0 37 7
A build() 0 10 3

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
     * @param Parser     $parser the instance that requests parsing
65
     * @param TokensList $list   the list of tokens to be parsed
66
     */
67 16
    public function parse(Parser $parser, TokensList $list)
68
    {
69 16
        /**
70
         * The state of the parser.
71
         *
72 16
         * Below are the states of the parser.
73 16
         *
74
         *      0 ---------------- [ name ] -----------------> 1
75
         *
76
         *      1 ------------------ [ ( ] ------------------> 2
77
         *
78
         *      2 ------------------ [ AS ] -----------------> 3
79
         *
80
         *      3 ------------------ [ ( ] ------------------> 4
81
         *
82
         *      4 ------------------ [ , ] ------------------> 1
83
         *
84
         *      4 ----- [ SELECT/UPDATE/DELETE/INSERT ] -----> 5
85
         *
86 16
         * @var int
87 16
         */
88
        $state = 0;
89 16
        $wither = null;
90
91
        ++$list->idx; // Skipping `WITH`.
92
93
        // parse any options if provided
94
        $this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
95 16
        ++$list->idx;
96
97
        for (; $list->idx < $list->count; ++$list->idx) {
98 16
            /**
99 12
             * Token parsed at this moment.
100
             *
101
             * @var Token
102 16
             */
103 16
            $token = $list->tokens[$list->idx];
104 16
105 16
            // Skipping whitespaces and comments.
106 16
            if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
107
                continue;
108
            }
109 16
110 16
            if ($state === 0) {
111 12
                if ($token->type === Token::TYPE_NONE) {
112 12
                    $wither = $token->value;
113
                    $this->withers[$wither] = new WithKeyword($wither);
114
                    $state = 1;
115 16
                } else {
116 16
                    $parser->error('The name of the CTE was expected.', $token);
117 16
                }
118 16
            } elseif ($state === 1) {
119
                if ($token->type === Token::TYPE_OPERATOR && $token->value === '(') {
120 16
                    $this->withers[$wither]->columns = Array2d::parse($parser, $list);
121 16
                    $state = 2;
122 16
                } elseif ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'AS') {
123 16
                    $state = 3;
124 16
                } else {
125 4
                    $parser->error('Unexpected token.', $token);
126 4
                    break;
127
                }
128
            } elseif ($state === 2) {
129 12
                if (! ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'AS')) {
130
                    $parser->error('AS keyword was expected.', $token);
131 12
                    break;
132 4
                }
133 4
134
                $state = 3;
135
            } elseif ($state === 3) {
136
                if ($token->value !== '(') {
137 12
                    $parser->error('Subquery of the CTE was expected.', $token);
138 12
                    break;
139
                }
140
141
                ++$list->idx;
142 12
                $subList = $this->getSubTokenList($list);
143 4
                if ($subList instanceof ParserException) {
144 4
                    $parser->errors[] = $subList;
145 4
                    continue;
146
                }
147
148
                $subParser = new Parser($subList);
149 12
150
                if (count($subParser->errors)) {
151
                    foreach ($subParser->errors as $error) {
152
                        $parser->errors[] = $error;
153 16
                    }
154 16
                }
155
156
                $this->withers[$wither]->statement = $subParser;
157
158
                $state = 4;
159 8
            } elseif ($state === 4) {
160
                if ($token->value === ',') {
161 8
                    // There's another WITH expression to parse, go back to state=0
162
                    $state = 0;
163 8
                    continue;
164 8
                }
165 8
166
                if (
167
                    $token->type === Token::TYPE_KEYWORD && (
168 8
                    $token->value === 'SELECT'
169
                    || $token->value === 'INSERT'
170
                    || $token->value === 'UPDATE'
171
                    || $token->value === 'DELETE'
172
                    )
173
                ) {
174
                    $state = 5;
175
                    --$list->idx;
176 16
                    continue;
177
                }
178 16
179
                $parser->error('An expression was expected.', $token);
180 16
                break;
181 16
            } elseif ($state === 5) {
182
                /**
183 16
                 * We need to parse all of the remaining tokens becuase mostly, they are only the CTE expression
184 16
                 * which's mostly is SELECT, or INSERT, UPDATE, or delete statement.
185 8
                 * e.g: INSERT .. ( SELECT 1 ) SELECT col1 FROM cte ON DUPLICATE KEY UPDATE col_name = 3.
186 16
                 * The issue is that, `ON DUPLICATE KEY UPDATE col_name = 3` is related to the main INSERT query
187 16
                 * not the cte expression (SELECT col1 FROM cte) we need to determine the end of the expression
188 12
                 * to parse `ON DUPLICATE KEY UPDATE` from the InsertStatement parser instead.
189
                 */
190
191
                // Index of the last parsed token by default would be the last token in the $list, because we're
192 16
                // assuming that all remaining tokens at state 4, are related to the expression.
193 16
                $idxOfLastParsedToken = $list->count - 1;
194 4
                // Index before search to be able to restore the index.
195
                $idxBeforeSearch = $list->idx;
196
                // Length of expression tokens is null by default, in order for the $subList to start
197 16
                // from $list->idx to the end of the $list.
198
                $lengthOfExpressionTokens = null;
199
200
                if ($list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ON')) {
201 16
                    // (-1) because getNextOfTypeAndValue returned ON and increased the index.
202 4
                    $idxOfOn = $list->idx - 1;
203
                    // Index of the last parsed token will be the token before the ON Keyword, therefore $idxOfOn - 1.
204 4
                    $idxOfLastParsedToken = $idxOfOn - 1;
205 4
                    // The length of the expression tokens would be the position of
206 1
                    $lengthOfExpressionTokens = $idxOfOn - $idxBeforeSearch;
207
                }
208
209
                // Restore the index
210 12
                $list->idx = $idxBeforeSearch;
211
212 12
                $subList = new TokensList(array_slice($list->tokens, $list->idx, $lengthOfExpressionTokens));
213
                $subParser = new Parser($subList);
214
                if (count($subParser->errors)) {
215
                    foreach ($subParser->errors as $error) {
216
                        $parser->errors[] = $error;
217
                    }
218
                }
219
220
                $list->idx = $idxOfLastParsedToken;
221
                break;
222
            }
223
        }
224
225
        // 5 is the only valid end state
226
        if ($state !== 5) {
0 ignored issues
show
introduced by
The condition $state !== 5 is always true.
Loading history...
227
            $parser->error('Unexpected end of WITH CTE.', $token);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $token does not seem to be defined for all execution paths leading up to this point.
Loading history...
228
        }
229
230
        --$list->idx;
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236
    public function build()
237
    {
238
        $str = 'WITH ';
239
240
        foreach ($this->withers as $wither) {
241
            $str .= $str === 'WITH ' ? '' : ', ';
242
            $str .= WithKeyword::build($wither);
243
        }
244
245
        return $str;
246
    }
247
248
    /**
249
     * Get tokens within the WITH expression to use them in another parser
250
     *
251
     * @return ParserException|TokensList
252
     */
253
    private function getSubTokenList(TokensList $list)
254
    {
255
        $idx = $list->idx;
256
        /** @var Token $token */
257
        $token = $list->tokens[$list->idx];
258
        $openParenthesis = 0;
259
260
        while ($list->idx < $list->count) {
261
            if ($token->value === '(') {
262
                ++$openParenthesis;
263
            } elseif ($token->value === ')') {
264
                if (--$openParenthesis === -1) {
265
                    break;
266
                }
267
            }
268
269
            ++$list->idx;
270
            if (! isset($list->tokens[$list->idx])) {
271
                break;
272
            }
273
274
            $token = $list->tokens[$list->idx];
275
        }
276
277
        // performance improvement: return the error to avoid a try/catch in the loop
278
        if ($list->idx === $list->count) {
279
            --$list->idx;
280
281
            return new ParserException(
282
                Translator::gettext('A closing bracket was expected.'),
283
                $token
284
            );
285
        }
286
287
        $length = $list->idx - $idx;
288
289
        return new TokensList(array_slice($list->tokens, $idx, $length), $length);
290
    }
291
}
292