Completed
Push — master ( 65f66e...428edc )
by Michal
04:14
created

BufferedQuery::extract()   D

Complexity

Conditions 67
Paths 105

Size

Total Lines 268
Code Lines 112

Duplication

Lines 13
Ratio 4.85 %

Code Coverage

Tests 129
CRAP Score 67

Importance

Changes 0
Metric Value
cc 67
eloc 112
nc 105
nop 1
dl 13
loc 268
ccs 129
cts 129
cp 1
crap 67
rs 4.1333
c 0
b 0
f 0

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
 * Buffered query utilities.
5
 */
6
7
namespace PhpMyAdmin\SqlParser\Utils;
8
9
use PhpMyAdmin\SqlParser\Context;
10
11
/**
12
 * Buffer query utilities.
13
 *
14
 * Implements a specialized lexer used to extract statements from large inputs
15
 * that are being buffered. After each statement has been extracted, a lexer or
16
 * a parser may be used.
17
 *
18
 * All comments are skipped, with one exception: MySQL commands inside `/*!`.
19
 *
20
 * @category   Lexer
21
 *
22
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
23
 */
24
class BufferedQuery
25
{
26
    // Constants that describe the current status of the parser.
27
28
    // A string is being parsed.
29
    const STATUS_STRING = 16; // 0001 0000
30
    const STATUS_STRING_SINGLE_QUOTES = 17; // 0001 0001
31
    const STATUS_STRING_DOUBLE_QUOTES = 18; // 0001 0010
32
    const STATUS_STRING_BACKTICK = 20; // 0001 0100
33
34
    // A comment is being parsed.
35
    const STATUS_COMMENT = 32; // 0010 0000
36
    const STATUS_COMMENT_BASH = 33; // 0010 0001
37
    const STATUS_COMMENT_C = 34; // 0010 0010
38
    const STATUS_COMMENT_SQL = 36; // 0010 0100
39
40
    /**
41
     * The query that is being processed.
42
     *
43
     * This field can be modified just by appending to it!
44
     *
45
     * @var string
46
     */
47
    public $query = '';
48
49
    /**
50
     * The options of this parser.
51
     *
52
     * @var array
53
     */
54
    public $options = array();
55
56
    /**
57
     * The last delimiter used.
58
     *
59
     * @var string
60
     */
61
    public $delimiter;
62
63
    /**
64
     * The length of the delimiter.
65
     *
66
     * @var int
67
     */
68
    public $delimiterLen;
69
70
    /**
71
     * The current status of the parser.
72
     *
73
     * @var int
74
     */
75
    public $status;
76
77
    /**
78
     * The last incomplete query that was extracted.
79
     *
80
     * @var string
81
     */
82
    public $current = '';
83
84
    /**
85
     * Constructor.
86
     *
87
     * @param string $query   the query to be parsed
88
     * @param array  $options the options of this parser
89
     */
90 7
    public function __construct($query = '', array $options = array())
91
    {
92
        // Merges specified options with defaults.
93 7
        $this->options = array_merge(
94
            array(
95
                /*
96
                 * The starting delimiter.
97
                 *
98
                 * @var string
99
                 */
100 7
                'delimiter' => ';',
101
102
                /*
103
                 * Whether `DELIMITER` statements should be parsed.
104
                 *
105
                 * @var bool
106
                 */
107 7
                'parse_delimiter' => false,
108
109
                /*
110
                 * Whether a delimiter should be added at the end of the
111
                 * statement.
112
                 *
113
                 * @var bool
114
                 */
115 7
                'add_delimiter' => false,
116 7
            ),
117
            $options
118 7
        );
119
120 7
        $this->query = $query;
121 7
        $this->setDelimiter($this->options['delimiter']);
122 7
    }
123
124
    /**
125
     * Sets the delimiter.
126
     *
127
     * Used to update the length of it too.
128
     *
129
     * @param string $delimiter
130
     */
131 7
    public function setDelimiter($delimiter)
132
    {
133 7
        $this->delimiter = $delimiter;
134 7
        $this->delimiterLen = strlen($delimiter);
135 7
    }
136
137
    /**
138
     * Extracts a statement from the buffer.
139
     *
140
     * @param bool $end whether the end of the buffer was reached
141
     *
142
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
143
     */
144 7
    public function extract($end = false)
145
    {
146
        /**
147
         * The last parsed position.
148
         *
149
         * This is statically defined because it is not used outside anywhere
150
         * outside this method and there is probably a (minor) performance
151
         * improvement to it.
152
         *
153
         * @var int
154
         */
155 7
        static $i = 0;
156
157 7
        if (empty($this->query)) {
158 7
            return false;
159
        }
160
161
        /**
162
         * The length of the buffer.
163
         *
164
         * @var int
165
         */
166 7
        $len = strlen($this->query);
167
168
        /**
169
         * The last index of the string that is going to be parsed.
170
         *
171
         * There must be a few characters left in the buffer so the parser can
172
         * avoid confusing some symbols that may have multiple meanings.
173
         *
174
         * For example, if the buffer ends in `-` that may be an operator or the
175
         * beginning of a comment.
176
         *
177
         * Another example if the buffer ends in `DELIMITE`. The parser is going
178
         * to require a few more characters because that may be a part of the
179
         * `DELIMITER` keyword or just a column named `DELIMITE`.
180
         *
181
         * Those extra characters are required only if there is more data
182
         * expected (the end of the buffer was not reached).
183
         *
184
         * @var int
185
         */
186 7
        $loopLen = $end ? $len : $len - 16;
187
188 7
        for (; $i < $loopLen; ++$i) {
189
            /*
190
             * Handling backslash.
191
             *
192
             * Even if the next character is a special character that should be
193
             * treated differently, because of the preceding backslash, it will
194
             * be ignored.
195
             */
196 7
            if ((($this->status & static::STATUS_COMMENT) == 0) && ($this->query[$i] === '\\')) {
197 3
                $this->current .= $this->query[$i] . $this->query[++$i];
198 3
                continue;
199
            }
200
201
            /*
202
             * Handling special parses statuses.
203
             */
204 7
            if ($this->status === static::STATUS_STRING_SINGLE_QUOTES) {
205
                // Single-quoted strings like 'foo'.
206 5
                if ($this->query[$i] === '\'') {
207 5
                    $this->status = 0;
208 5
                }
209 5
                $this->current .= $this->query[$i];
210 5
                continue;
211 7 View Code Duplication
            } elseif ($this->status === static::STATUS_STRING_DOUBLE_QUOTES) {
212
                // Double-quoted strings like "bar".
213 4
                if ($this->query[$i] === '"') {
214 4
                    $this->status = 0;
215 4
                }
216 4
                $this->current .= $this->query[$i];
217 4
                continue;
218 7
            } elseif ($this->status === static::STATUS_STRING_BACKTICK) {
219 4
                if ($this->query[$i] === '`') {
220 4
                    $this->status = 0;
221 4
                }
222 4
                $this->current .= $this->query[$i];
223 4
                continue;
224 7
            } elseif (($this->status === static::STATUS_COMMENT_BASH)
225 7
                || ($this->status === static::STATUS_COMMENT_SQL)
226 7
            ) {
227
                // Bash-like (#) or SQL-like (-- ) comments end in new line.
228 3
                if ($this->query[$i] === "\n") {
229 3
                    $this->status = 0;
230 3
                }
231 3
                continue;
232 7
            } elseif ($this->status === static::STATUS_COMMENT_C) {
233
                // C-like comments end in */.
234 3
                if (($this->query[$i - 1] === '*') && ($this->query[$i] === '/')) {
235 3
                    $this->status = 0;
236 3
                }
237 3
                continue;
238
            }
239
240
            /*
241
             * Checking if a string started.
242
             */
243 7
            if ($this->query[$i] === '\'') {
244 5
                $this->status = static::STATUS_STRING_SINGLE_QUOTES;
245 5
                $this->current .= $this->query[$i];
246 5
                continue;
247 7 View Code Duplication
            } elseif ($this->query[$i] === '"') {
248 4
                $this->status = static::STATUS_STRING_DOUBLE_QUOTES;
249 4
                $this->current .= $this->query[$i];
250 4
                continue;
251 7
            } elseif ($this->query[$i] === '`') {
252 4
                $this->status = static::STATUS_STRING_BACKTICK;
253 4
                $this->current .= $this->query[$i];
254 4
                continue;
255
            }
256
257
            /*
258
             * Checking if a comment started.
259
             */
260 7
            if ($this->query[$i] === '#') {
261 3
                $this->status = static::STATUS_COMMENT_BASH;
262 3
                continue;
263 7
            } elseif (($i + 2 < $len)
264 7
                && ($this->query[$i] === '-')
265 7
                && ($this->query[$i + 1] === '-')
266 7
                && (Context::isWhitespace($this->query[$i + 2]))
267 7
            ) {
268 3
                $this->status = static::STATUS_COMMENT_SQL;
269 3
                continue;
270 7
            } elseif (($i + 2 < $len)
271 7
                && ($this->query[$i] === '/')
272 7
                && ($this->query[$i + 1] === '*')
273 7
                && ($this->query[$i + 2] !== '!')
274 7
            ) {
275 3
                $this->status = static::STATUS_COMMENT_C;
276 3
                continue;
277
            }
278
279
            /*
280
             * Handling `DELIMITER` statement.
281
             *
282
             * The code below basically checks for
283
             *     `strtoupper(substr($this->query, $i, 9)) === 'DELIMITER'`
284
             *
285
             * This optimization makes the code about 3 times faster.
286
             *
287
             * `DELIMITER` is not being considered a keyword. The only context
288
             * it has a special meaning is when it is the beginning of a
289
             * statement. This is the reason for the last condition.
290
             */
291 7
            if (($i + 9 < $len)
292 7
                && (($this->query[$i] === 'D') || ($this->query[$i] === 'd'))
293 7
                && (($this->query[$i + 1] === 'E') || ($this->query[$i + 1] === 'e'))
294 7
                && (($this->query[$i + 2] === 'L') || ($this->query[$i + 2] === 'l'))
295 7
                && (($this->query[$i + 3] === 'I') || ($this->query[$i + 3] === 'i'))
296 7
                && (($this->query[$i + 4] === 'M') || ($this->query[$i + 4] === 'm'))
297 7
                && (($this->query[$i + 5] === 'I') || ($this->query[$i + 5] === 'i'))
298 7
                && (($this->query[$i + 6] === 'T') || ($this->query[$i + 6] === 't'))
299 7
                && (($this->query[$i + 7] === 'E') || ($this->query[$i + 7] === 'e'))
300 7
                && (($this->query[$i + 8] === 'R') || ($this->query[$i + 8] === 'r'))
301 7
                && (Context::isWhitespace($this->query[$i + 9]))
302 7
                && (trim($this->current) === '')
303 7
            ) {
304
                // Saving the current index to be able to revert any parsing
305
                // done in this block.
306 4
                $iBak = $i;
307 4
                $i += 9; // Skipping `DELIMITER`.
308
309
                // Skipping whitespaces.
310 4
                while (($i < $len) && (Context::isWhitespace($this->query[$i]))) {
311 4
                    ++$i;
312 4
                }
313
314
                // Parsing the delimiter.
315 4
                $delimiter = '';
316 4
                while (($i < $len) && (!Context::isWhitespace($this->query[$i]))) {
317 4
                    $delimiter .= $this->query[$i++];
318 4
                }
319
320
                // Checking if the delimiter definition ended.
321 4
                if (($delimiter != '')
322 4
                    && ((($i < $len) && (Context::isWhitespace($this->query[$i])))
323 1
                    || (($i === $len) && ($end)))
324 4
                ) {
325
                    // Saving the delimiter.
326 4
                    $this->setDelimiter($delimiter);
327
328
                    // Whether this statement should be returned or not.
329 4
                    $ret = '';
330 4
                    if (!empty($this->options['parse_delimiter'])) {
331
                        // Appending the `DELIMITER` statement that was just
332
                        // found to the current statement.
333 2
                        $ret = trim(
334 2
                            $this->current . ' ' . substr($this->query, $iBak, $i - $iBak)
335 2
                        );
336 2
                    }
337
338
                    // Removing the statement that was just extracted from the
339
                    // query.
340 4
                    $this->query = substr($this->query, $i);
341 4
                    $i = 0;
342
343
                    // Resetting the current statement.
344 4
                    $this->current = '';
345
346 4
                    return $ret;
347
                }
348
349
                // Incomplete statement. Reverting
350 1
                $i = $iBak;
351
352 1
                return false;
353
            }
354
355
            /*
356
             * Checking if the current statement finished.
357
             *
358
             * The first letter of the delimiter is being checked as an
359
             * optimization. This code is almost as fast as the one above.
360
             *
361
             * There is no point in checking if two strings match if not even
362
             * the first letter matches.
363
             */
364 7
            if (($this->query[$i] === $this->delimiter[0])
365 7
                && (($this->delimiterLen === 1)
366 4
                || (substr($this->query, $i, $this->delimiterLen) === $this->delimiter))
367 7
            ) {
368
                // Saving the statement that just ended.
369 7
                $ret = $this->current;
370
371
                // If needed, adds a delimiter at the end of the statement.
372 7
                if (!empty($this->options['add_delimiter'])) {
373 5
                    $ret .= $this->delimiter;
374 5
                }
375
376
                // Removing the statement that was just extracted from the
377
                // query.
378 7
                $this->query = substr($this->query, $i + $this->delimiterLen);
379 7
                $i = 0;
380
381
                // Resetting the current statement.
382 7
                $this->current = '';
383
384
                // Returning the statement.
385 7
                return trim($ret);
386
            }
387
388
            /*
389
             * Appending current character to current statement.
390
             */
391 7
            $this->current .= $this->query[$i];
392 7
        }
393
394 7
        if (($end) && ($i === $len)) {
395
            // If the end of the buffer was reached, the buffer is emptied and
396
            // the current statement that was extracted is returned.
397 5
            $ret = $this->current;
398
399
            // Emptying the buffer.
400 5
            $this->query = '';
401 5
            $i = 0;
402
403
            // Resetting the current statement.
404 5
            $this->current = '';
405
406
            // Returning the statement.
407 5
            return trim($ret);
408
        }
409
410 7
        return '';
411
    }
412
}
413