ExplainStatement   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 115
c 0
b 0
f 0
dl 0
loc 248
rs 8.64
ccs 103
cts 103
cp 1
wmc 47

2 Methods

Rating   Name   Duplication   Size   Complexity  
D parse() 0 159 36
B build() 0 35 11

How to fix   Complexity   

Complex Class

Complex classes like ExplainStatement 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 ExplainStatement, 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\Context;
8
use PhpMyAdmin\SqlParser\Parser;
9
use PhpMyAdmin\SqlParser\Parsers\OptionsArrays;
10
use PhpMyAdmin\SqlParser\Statement;
11
use PhpMyAdmin\SqlParser\TokensList;
12
use PhpMyAdmin\SqlParser\TokenType;
13
14
use function array_slice;
15
16
/**
17
 * `EXPLAIN` statement.
18
 */
19
class ExplainStatement extends Statement
20
{
21
    /**
22
     * Options for `EXPLAIN` statements.
23
     */
24
    private const OPTIONS = [
25
26
        'EXTENDED' => 1,
27
        'PARTITIONS' => 1,
28
        'FORMAT' => [
29
            1,
30
            'var',
31
        ],
32
    ];
33
34
    /**
35
     * The parser of the statement to be explained
36
     */
37
    public Parser|null $bodyParser = null;
38
39
    /**
40
     * The statement alias, could be any of the following:
41
     * - {EXPLAIN | DESCRIBE | DESC}
42
     * - {EXPLAIN | DESCRIBE | DESC} ANALYZE
43
     * - ANALYZE
44
     */
45
    public string $statementAlias;
46
47
    /**
48
     * The connection identifier, if used.
49
     */
50
    public int|null $connectionId = null;
51
52
    /**
53
     * The explained database for the table's name, if used.
54
     */
55
    public string|null $explainedDatabase = null;
56
57
    /**
58
     * The explained table's name, if used.
59
     */
60
    public string|null $explainedTable = null;
61
62
    /**
63
     * The explained column's name, if used.
64
     */
65
    public string|null $explainedColumn = null;
66
67
    /**
68
     * @param Parser     $parser the instance that requests parsing
69
     * @param TokensList $list   the list of tokens to be parsed
70
     */
71 44
    public function parse(Parser $parser, TokensList $list): void
72
    {
73
        /**
74
         * The state of the parser.
75
         *
76
         * Below are the states of the parser.
77
         *
78
         *      0 -------------------[ EXPLAIN/EXPLAIN ANALYZE/ANALYZE ]-----------------------> 1
79
         *
80
         *      0 ------------------------[ EXPLAIN/DESC/DESCRIBE ]----------------------------> 3
81
         *
82
         *      1 ------------------------------[ OPTIONS ]------------------------------------> 2
83
         *
84
         *      2 --------------[ tablename / STATEMENT / FOR CONNECTION ]---------------------> 2
85
         *
86
         *      3 -----------------------------[ tablename ]-----------------------------------> 3
87
         */
88 44
        $state = 0;
89
90
        /**
91
         * To Differentiate between ANALYZE / EXPLAIN / EXPLAIN ANALYZE
92
         * 0 -> ANALYZE ( used by mariaDB https://mariadb.com/kb/en/analyze-statement)
93
         * 1 -> {EXPLAIN | DESCRIBE | DESC}
94
         * 2 -> {EXPLAIN | DESCRIBE | DESC} ANALYZE
95
         */
96 44
        $miniState = 0;
97
98 44
        for (; $list->idx < $list->count; ++$list->idx) {
99
            /**
100
             * Token parsed at this moment.
101
             */
102 44
            $token = $list->tokens[$list->idx];
103
104
            // End of statement.
105 44
            if ($token->type === TokenType::Delimiter) {
106 20
                --$list->idx; // Back up one token, no real reasons to document
107 20
                break;
108
            }
109
110
            // Skipping whitespaces and comments.
111 44
            if ($token->type === TokenType::Whitespace || $token->type === TokenType::Comment) {
112 38
                continue;
113
            }
114
115 44
            if ($state === 0) {
116 44
                if ($token->keyword === 'ANALYZE' && $miniState === 0) {
117 10
                    $state = 1;
118 10
                    $this->statementAlias = 'ANALYZE';
119
                } elseif (
120 36
                    $token->keyword === 'EXPLAIN'
121 20
                    || $token->keyword === 'DESC'
122 36
                    || $token->keyword === 'DESCRIBE'
123
                ) {
124 36
                    $this->statementAlias = $token->keyword;
125
126 36
                    $lastIdx = $list->idx;
127 36
                    $list->idx++; // Ignore the current token
128 36
                    $nextKeyword = $list->getNextOfType(TokenType::Keyword);
129 36
                    $list->idx = $lastIdx;
130
131
                    // There is no other keyword, we must be describing a table
132 36
                    if ($nextKeyword === null) {
133 18
                        $state = 3;
134 18
                        continue;
135
                    }
136
137 20
                    $miniState = 1;
138
139 20
                    $lastIdx = $list->idx;
140 20
                    $nextKeyword = $list->getNextOfTypeAndValue(TokenType::Keyword, 'ANALYZE');
141 20
                    if ($nextKeyword && $nextKeyword->keyword !== null) {
142 6
                        $miniState = 2;
143 6
                        $this->statementAlias .= ' ANALYZE';
144
                    } else {
145 16
                        $list->idx = $lastIdx;
146
                    }
147
148 20
                    $state = 1;
149
                }
150 40
            } elseif ($state === 1) {
151
                // Parsing options.
152 26
                $this->options = OptionsArrays::parse($parser, $list, self::OPTIONS);
153 26
                $state = 2;
154 40
            } elseif ($state === 2) {
155 26
                $currIdx = $list->idx;
156 26
                $list->idx++; // Ignore the current token
157 26
                $nextToken = $list->getNext();
158 26
                $list->idx = $currIdx;
159
160 26
                if ($token->keyword === 'FOR' && $nextToken->keyword === 'CONNECTION') {
161 2
                    $list->idx++; // Ignore the current token
162 2
                    $list->getNext(); // CONNECTION
163 2
                    $nextToken = $list->getNext(); // Identifier
164 2
                    $this->connectionId = $nextToken->value;
165 2
                    break;
166
                }
167
168
                if (
169 26
                    $token->keyword !== 'SELECT'
170 26
                    && $token->keyword !== 'TABLE'
171 26
                    && $token->keyword !== 'INSERT'
172 26
                    && $token->keyword !== 'REPLACE'
173 26
                    && $token->keyword !== 'UPDATE'
174 26
                    && $token->keyword !== 'DELETE'
175
                ) {
176 6
                    $parser->error('Unexpected token.', $token);
177 6
                    break;
178
                }
179
180
                // Index of the last parsed token by default would be the last token in the $list, because we're
181
                // assuming that all remaining tokens at state 2, are related to the to-be-explained statement.
182 20
                $idxOfLastParsedToken = $list->count - 1;
183 20
                $subList = new TokensList(array_slice($list->tokens, $list->idx));
184
185 20
                $this->bodyParser = new Parser($subList);
186 20
                if ($this->bodyParser->errors !== []) {
187 4
                    foreach ($this->bodyParser->errors as $error) {
188 4
                        $parser->errors[] = $error;
189
                    }
190
191 4
                    break;
192
                }
193
194 16
                $list->idx = $idxOfLastParsedToken;
195 16
                break;
196 16
            } elseif ($state === 3) {
197 16
                if (($token->type === TokenType::Operator) && ($token->value === '.')) {
198 4
                    continue;
199
                }
200
201 16
                if ($this->explainedDatabase === null) {
202 16
                    $lastIdx = $list->idx;
203 16
                    $nextDot = $list->getNextOfTypeAndValue(TokenType::Operator, '.');
204 16
                    $list->idx = $lastIdx;
205 16
                    if ($nextDot !== null) {// We found a dot, so it must be a db.table name format
206 4
                        $this->explainedDatabase = $token->value;
207 4
                        continue;
208
                    }
209
                }
210
211 16
                if ($this->explainedTable === null) {
212 16
                    $this->explainedTable = $token->value;
213 16
                    continue;
214
                }
215
216 10
                if ($this->explainedColumn === null && $token->value !== null) {
217 10
                    $this->explainedColumn = (string) $token->value;
218
                }
219
            }
220
        }
221
222 44
        if ($state !== 3 || $this->explainedTable !== null) {
0 ignored issues
show
introduced by
The condition $state !== 3 is always true.
Loading history...
223 42
            return;
224
        }
225
226
        // We reached end of the state 3 and no table name was found
227
        /** Token parsed at this moment. */
228 2
        $token = $list->tokens[$list->idx];
229 2
        $parser->error('Expected a table name.', $token);
230
    }
231
232 2
    public function build(): string
233
    {
234 2
        $str = $this->statementAlias;
235
236 2
        if ($this->options !== null) {
237 2
            if ($this->options->options !== []) {
238 2
                $str .= ' ';
239
            }
240
241 2
            $str .= $this->options->build() . ' ';
242
        }
243
244 2
        if ($this->options === null) {
245 2
            $str .= ' ';
246
        }
247
248 2
        if ($this->bodyParser) {
249 2
            foreach ($this->bodyParser->statements as $statement) {
250 2
                $str .= $statement->build();
251
            }
252 2
        } elseif ($this->connectionId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->connectionId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
253 2
            $str .= 'FOR CONNECTION ' . $this->connectionId;
254
        }
255
256 2
        if ($this->explainedDatabase !== null && $this->explainedTable !== null) {
257 2
            $str .= Context::escape($this->explainedDatabase) . '.' . Context::escape($this->explainedTable);
258 2
        } elseif ($this->explainedTable !== null) {
259 2
            $str .= Context::escape($this->explainedTable);
260
        }
261
262 2
        if ($this->explainedColumn !== null) {
263 2
            $str .= ' ' . Context::escape($this->explainedColumn);
264
        }
265
266 2
        return $str;
267
    }
268
}
269