Passed
Pull Request — master (#477)
by
unknown
21:18 queued 18:31
created

OptionsArray::isEmpty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Components;
6
7
use PhpMyAdmin\SqlParser\Component;
8
use PhpMyAdmin\SqlParser\Parser;
9
use PhpMyAdmin\SqlParser\Token;
10
use PhpMyAdmin\SqlParser\TokensList;
11
use PhpMyAdmin\SqlParser\Translator;
12
13
use function array_merge_recursive;
14
use function count;
15
use function implode;
16
use function is_array;
17
use function ksort;
18
use function sprintf;
19
use function strcasecmp;
20
use function strtoupper;
21
22
/**
23
 * Parses a list of options.
24
 */
25
final class OptionsArray implements Component
26
{
27
    /**
28
     * ArrayObj of selected options.
29
     *
30
     * @var array<int, mixed>
31
     */
32
    public $options = [];
33
34
    /**
35
     * @param array<int, mixed> $options The array of options. Options that have a value
36
     *                       must be an array with at least two keys `name` and
37
     *                       `expr` or `value`.
38
     */
39 1066
    public function __construct(array $options = [])
40
    {
41 1066
        $this->options = $options;
42
    }
43
44
    /**
45
     * @param Parser               $parser  the parser that serves as context
46
     * @param TokensList           $list    the list of tokens that are being parsed
47
     * @param array<string, mixed> $options parameters for parsing
48
     *
49
     * @return OptionsArray
50
     */
51 1058
    public static function parse(Parser $parser, TokensList $list, array $options = [])
52
    {
53 1058
        $ret = new static();
54
55
        /**
56
         * The ID that will be assigned to duplicate options.
57
         *
58
         * @var int
59
         */
60 1058
        $lastAssignedId = count($options) + 1;
61
62
        /**
63
         * The option that was processed last time.
64
         */
65 1058
        $lastOption = null;
66
67
        /**
68
         * The index of the option that was processed last time.
69
         *
70
         * @var int
71
         */
72 1058
        $lastOptionId = 0;
73
74
        /**
75
         * Counts brackets.
76
         *
77
         * @var int
78
         */
79 1058
        $brackets = 0;
80
81
        /**
82
         * The state of the parser.
83
         *
84
         * Below are the states of the parser.
85
         *
86
         *      0 ---------------------[ option ]----------------------> 1
87
         *
88
         *      1 -------------------[ = (optional) ]------------------> 2
89
         *
90
         *      2 ----------------------[ value ]----------------------> 0
91
         *
92
         * @var int
93
         */
94 1058
        $state = 0;
95
96 1058
        for (; $list->idx < $list->count; ++$list->idx) {
97
            /**
98
             * Token parsed at this moment.
99
             */
100 1058
            $token = $list->tokens[$list->idx];
101
102
            // End of statement.
103 1058
            if ($token->type === Token::TYPE_DELIMITER) {
104 282
                break;
105
            }
106
107
            // Skipping comments.
108 1052
            if ($token->type === Token::TYPE_COMMENT) {
109 24
                continue;
110
            }
111
112
            // Skipping whitespace if not parsing value.
113 1052
            if (($token->type === Token::TYPE_WHITESPACE) && ($brackets === 0)) {
114 1012
                continue;
115
            }
116
117 1052
            if ($lastOption === null) {
118 1052
                $upper = strtoupper($token->token);
119 1052
                if (! isset($options[$upper])) {
120
                    // There is no option to be processed.
121 1012
                    break;
122
                }
123
124 630
                $lastOption = $options[$upper];
125 630
                $lastOptionId = is_array($lastOption) ?
126 630
                    $lastOption[0] : $lastOption;
127 630
                $state = 0;
128
129
                // Checking for option conflicts.
130
                // For example, in `SELECT` statements the keywords `ALL`
131
                // and `DISTINCT` conflict and if used together, they
132
                // produce an invalid query.
133
                //
134
                // Usually, tokens can be identified in the array by the
135
                // option ID, but if conflicts occur, a generated option ID
136
                // is used.
137
                //
138
                // The first pseudo duplicate ID is the maximum value of the
139
                // real options (e.g.  if there are 5 options, the first
140
                // fake ID is 6).
141 630
                if (isset($ret->options[$lastOptionId])) {
142 16
                    $parser->error(
143 16
                        sprintf(
144 16
                            Translator::gettext('This option conflicts with "%1$s".'),
145 16
                            is_array($ret->options[$lastOptionId])
146 10
                            ? $ret->options[$lastOptionId]['name']
147 16
                            : $ret->options[$lastOptionId]
148 16
                        ),
149 16
                        $token
150 16
                    );
151 16
                    $lastOptionId = $lastAssignedId++;
152
                }
153
            }
154
155 630
            if ($state === 0) {
156 630
                if (! is_array($lastOption)) {
157
                    // This is a just keyword option without any value.
158
                    // This is the beginning and the end of it.
159 570
                    $ret->options[$lastOptionId] = $token->value;
160 570
                    $lastOption = null;
161 570
                    $state = 0;
162 284
                } elseif (($lastOption[1] === 'var') || ($lastOption[1] === 'var=')) {
163
                    // This is a keyword that is followed by a value.
164
                    // This is only the beginning. The value is parsed in state
165
                    // 1 and 2. State 1 is used to skip the first equals sign
166
                    // and state 2 to parse the actual value.
167 212
                    $ret->options[$lastOptionId] = [
168
                        // @var string The name of the option.
169 212
                        'name' => $token->value,
170
                        // @var bool Whether it contains an equal sign.
171
                        //           This is used by the builder to rebuild it.
172 212
                        'equals' => $lastOption[1] === 'var=',
173
                        // @var string Raw value.
174 212
                        'expr' => '',
175
                        // @var string Processed value.
176 212
                        'value' => '',
177 212
                    ];
178 212
                    $state = 1;
179 142
                } elseif ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') {
180
                    // This is a keyword that is followed by an expression.
181
                    // The expression is used by the specialized parser.
182
183
                    // Skipping this option in order to parse the expression.
184 142
                    ++$list->idx;
185 142
                    $ret->options[$lastOptionId] = [
186
                        // @var string The name of the option.
187 142
                        'name' => $token->value,
188
                        // @var bool Whether it contains an equal sign.
189
                        //           This is used by the builder to rebuild it.
190 142
                        'equals' => $lastOption[1] === 'expr=',
191
                        // @var Expression The parsed expression.
192 142
                        'expr' => '',
193 142
                    ];
194 630
                    $state = 1;
195
                }
196 276
            } elseif ($state === 1) {
197 276
                $state = 2;
198 276
                if ($token->token === '=') {
199 114
                    $ret->options[$lastOptionId]['equals'] = true;
200 114
                    continue;
201
                }
202
            }
203
204
            // This is outside the `elseif` group above because the change might
205
            // change this iteration.
206 630
            if ($state !== 2) {
207 630
                continue;
208
            }
209
210 276
            if ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') {
211 142
                $ret->options[$lastOptionId]['expr'] = Expression::parse(
212 142
                    $parser,
213 142
                    $list,
214 142
                    empty($lastOption[2]) ? [] : $lastOption[2]
215 142
                );
216 142
                if ($ret->options[$lastOptionId]['expr'] !== null) {
217 142
                    $ret->options[$lastOptionId]['value']
218 142
                        = $ret->options[$lastOptionId]['expr']->expr;
219
                }
220
221 142
                $lastOption = null;
222 142
                $state = 0;
223
            } else {
224 204
                if ($token->token === '(') {
225 6
                    ++$brackets;
226 204
                } elseif ($token->token === ')') {
227 6
                    --$brackets;
228
                }
229
230 204
                $ret->options[$lastOptionId]['expr'] .= $token->token;
231
232
                if (
233 204
                    ! (($token->token === '(') && ($brackets === 1)
234 204
                    || (($token->token === ')') && ($brackets === 0)))
235
                ) {
236
                    // First pair of brackets is being skipped.
237 204
                    $ret->options[$lastOptionId]['value'] .= $token->value;
238
                }
239
240
                // Checking if we finished parsing.
241 204
                if ($brackets === 0) {
242 204
                    $lastOption = null;
243
                }
244
            }
245
        }
246
247
        /*
248
         * We reached the end of statement without getting a value
249
         * for an option for which a value was required
250
         */
251
        if (
252 1058
            $state === 1
0 ignored issues
show
introduced by
The condition $state === 1 is always false.
Loading history...
253
            && $lastOption
254 1058
            && ($lastOption[1] === 'expr'
255 1058
            || $lastOption[1] === 'var'
256 1058
            || $lastOption[1] === 'var='
257 1058
            || $lastOption[1] === 'expr=')
258
        ) {
259 8
            $parser->error(
260 8
                sprintf(
261 8
                    'Value/Expression for the option %1$s was expected.',
262 8
                    $ret->options[$lastOptionId]['name']
263 8
                ),
264 8
                $list->tokens[$list->idx - 1]
265 8
            );
266
        }
267
268 1058
        if (empty($options['_UNSORTED'])) {
269 1058
            ksort($ret->options);
270
        }
271
272 1058
        --$list->idx;
273
274 1058
        return $ret;
275
    }
276
277
    /**
278
     * @param OptionsArray         $component the component to be built
279
     * @param array<string, mixed> $options   parameters for building
280
     */
281 196
    public static function build($component, array $options = []): string
282
    {
283 196
        if (empty($component->options)) {
284 168
            return '';
285
        }
286
287 118
        $options = [];
288 118
        foreach ($component->options as $option) {
289 118
            if (! is_array($option)) {
290 100
                $options[] = $option;
291
            } else {
292 70
                $options[] = $option['name']
293 70
                    . (! empty($option['equals']) ? '=' : ' ')
294 70
                    . (! empty($option['expr']) ? $option['expr'] : $option['value']);
295
            }
296
        }
297
298 118
        return implode(' ', $options);
299
    }
300
301
    /**
302
     * Checks if it has the specified option and returns it value or true.
303
     *
304
     * @param string $key     the key to be checked
305
     * @param bool   $getExpr Gets the expression instead of the value.
306
     *                        The value is the processed form of the expression.
307
     *
308
     * @return mixed
309
     */
310 452
    public function has($key, $getExpr = false)
311
    {
312 452
        foreach ($this->options as $option) {
313 426
            if (is_array($option)) {
314 114
                if (! strcasecmp($key, $option['name'])) {
315 114
                    return $getExpr ? $option['expr'] : $option['value'];
316
                }
317 424
            } elseif (! strcasecmp($key, $option)) {
318 406
                return true;
319
            }
320
        }
321
322 428
        return false;
323
    }
324
325
    /**
326
     * Removes the option from the array.
327
     *
328
     * @param string $key the key to be removed
329
     *
330
     * @return bool whether the key was found and deleted or not
331
     */
332 10
    public function remove($key): bool
333
    {
334 10
        foreach ($this->options as $idx => $option) {
335 10
            if (is_array($option)) {
336 10
                if (! strcasecmp($key, $option['name'])) {
337 10
                    unset($this->options[$idx]);
338
339 10
                    return true;
340
                }
341 10
            } elseif (! strcasecmp($key, $option)) {
342 10
                unset($this->options[$idx]);
343
344 10
                return true;
345
            }
346
        }
347
348 2
        return false;
349
    }
350
351
    /**
352
     * Merges the specified options with these ones. Values with same ID will be
353
     * replaced.
354
     *
355
     * @param array<int, mixed>|OptionsArray $options the options to be merged
356
     */
357 22
    public function merge($options): void
358
    {
359 22
        if (is_array($options)) {
360 2
            $this->options = array_merge_recursive($this->options, $options);
361 20
        } elseif ($options instanceof self) {
0 ignored issues
show
introduced by
$options is always a sub-type of self.
Loading history...
362 20
            $this->options = array_merge_recursive($this->options, $options->options);
363
        }
364
    }
365
366
    /**
367
     * Checks tf there are no options set.
368
     */
369 178
    public function isEmpty(): bool
370
    {
371 178
        return empty($this->options);
372
    }
373
374 98
    public function __toString(): string
375
    {
376 98
        return static::build($this);
377
    }
378
}
379