Issues (119)

src/Parsers/OptionsArrays.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Parsers;
6
7
use PhpMyAdmin\SqlParser\Components\OptionsArray;
8
use PhpMyAdmin\SqlParser\Parseable;
9
use PhpMyAdmin\SqlParser\Parser;
10
use PhpMyAdmin\SqlParser\TokensList;
11
use PhpMyAdmin\SqlParser\TokenType;
12
use PhpMyAdmin\SqlParser\Translator;
13
14
use function count;
15
use function is_array;
16
use function ksort;
17
use function sprintf;
18
use function strtoupper;
19
20
/**
21
 * Parses a list of options.
22
 */
23
final class OptionsArrays implements Parseable
24
{
25
    /**
26
     * @param Parser               $parser  the parser that serves as context
27
     * @param TokensList           $list    the list of tokens that are being parsed
28
     * @param array<string, mixed> $options parameters for parsing
29
     */
30 1148
    public static function parse(Parser $parser, TokensList $list, array $options = []): OptionsArray
31
    {
32 1148
        $ret = new OptionsArray();
33
34
        /**
35
         * The ID that will be assigned to duplicate options.
36
         */
37 1148
        $lastAssignedId = count($options) + 1;
38
39
        /**
40
         * The option that was processed last time.
41
         */
42 1148
        $lastOption = null;
43
44
        /**
45
         * The index of the option that was processed last time.
46
         */
47 1148
        $lastOptionId = 0;
48
49
        /**
50
         * Counts brackets.
51
         */
52 1148
        $brackets = 0;
53
54
        /**
55
         * The state of the parser.
56
         *
57
         * Below are the states of the parser.
58
         *
59
         *      0 ---------------------[ option ]----------------------> 1
60
         *
61
         *      1 -------------------[ = (optional) ]------------------> 2
62
         *
63
         *      2 ----------------------[ value ]----------------------> 0
64
         */
65 1148
        $state = 0;
66
67 1148
        for (; $list->idx < $list->count; ++$list->idx) {
68
            /**
69
             * Token parsed at this moment.
70
             */
71 1148
            $token = $list->tokens[$list->idx];
72
73
            // End of statement.
74 1148
            if ($token->type === TokenType::Delimiter) {
75 332
                break;
76
            }
77
78
            // Skipping comments.
79 1142
            if ($token->type === TokenType::Comment) {
80 26
                continue;
81
            }
82
83
            // Skipping whitespace if not parsing value.
84 1142
            if (($token->type === TokenType::Whitespace) && ($brackets === 0)) {
85 1094
                continue;
86
            }
87
88 1142
            if ($lastOption === null) {
89 1142
                $upper = strtoupper($token->token);
90 1142
                if (! isset($options[$upper])) {
91
                    // There is no option to be processed.
92 1102
                    break;
93
                }
94
95 692
                $lastOption = $options[$upper];
96 692
                $lastOptionId = is_array($lastOption) ?
97 692
                    $lastOption[0] : $lastOption;
98 692
                $state = 0;
99
100
                // Checking for option conflicts.
101
                // For example, in `SELECT` statements the keywords `ALL`
102
                // and `DISTINCT` conflict and if used together, they
103
                // produce an invalid query.
104
                //
105
                // Usually, tokens can be identified in the array by the
106
                // option ID, but if conflicts occur, a generated option ID
107
                // is used.
108
                //
109
                // The first pseudo duplicate ID is the maximum value of the
110
                // real options (e.g.  if there are 5 options, the first
111
                // fake ID is 6).
112 692
                if (isset($ret->options[$lastOptionId])) {
113 16
                    $parser->error(
114 16
                        sprintf(
115 16
                            Translator::gettext('This option conflicts with "%1$s".'),
116 16
                            is_array($ret->options[$lastOptionId])
117 10
                            ? $ret->options[$lastOptionId]['name']
118 16
                            : $ret->options[$lastOptionId],
119 16
                        ),
120 16
                        $token,
121 16
                    );
122 16
                    $lastOptionId = $lastAssignedId++;
123
                }
124
            }
125
126 692
            if ($state === 0) {
127 692
                if (! is_array($lastOption)) {
128
                    // This is a just keyword option without any value.
129
                    // This is the beginning and the end of it.
130 632
                    $ret->options[$lastOptionId] = $token->value;
131 632
                    $lastOption = null;
132 632
                    $state = 0;
133 324
                } elseif (($lastOption[1] === 'var') || ($lastOption[1] === 'var=')) {
134
                    // This is a keyword that is followed by a value.
135
                    // This is only the beginning. The value is parsed in state
136
                    // 1 and 2. State 1 is used to skip the first equals sign
137
                    // and state 2 to parse the actual value.
138 252
                    $ret->options[$lastOptionId] = [
139
                        // @var string The name of the option.
140 252
                        'name' => $token->value,
141
                        // @var bool Whether it contains an equal sign.
142
                        //           This is used by the builder to rebuild it.
143 252
                        'equals' => $lastOption[1] === 'var=',
144
                        // @var string Raw value.
145 252
                        'expr' => '',
146
                        // @var string Processed value.
147 252
                        'value' => '',
148 252
                    ];
149 252
                    $state = 1;
150 142
                } elseif ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') {
151
                    // This is a keyword that is followed by an expression.
152
                    // The expression is used by the specialized parser.
153
154
                    // Skipping this option in order to parse the expression.
155 142
                    ++$list->idx;
156 142
                    $ret->options[$lastOptionId] = [
157
                        // @var string The name of the option.
158 142
                        'name' => $token->value,
159
                        // @var bool Whether it contains an equal sign.
160
                        //           This is used by the builder to rebuild it.
161 142
                        'equals' => $lastOption[1] === 'expr=',
162
                        // @var Expression The parsed expression.
163 142
                        'expr' => '',
164 142
                    ];
165 142
                    $state = 1;
166
                }
167 316
            } elseif ($state === 1) {
168 316
                $state = 2;
169 316
                if ($token->token === '=') {
170 114
                    $ret->options[$lastOptionId]['equals'] = true;
171 114
                    continue;
172
                }
173
            }
174
175
            // This is outside the `elseif` group above because the change might
176
            // change this iteration.
177 692
            if ($state !== 2) {
178 692
                continue;
179
            }
180
181 316
            if ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') {
182 142
                $ret->options[$lastOptionId]['expr'] = Expressions::parse(
183 142
                    $parser,
184 142
                    $list,
185 142
                    empty($lastOption[2]) ? [] : $lastOption[2],
186 142
                );
187 142
                if ($ret->options[$lastOptionId]['expr'] !== null) {
188 142
                    $ret->options[$lastOptionId]['value']
189 142
                        = $ret->options[$lastOptionId]['expr']->expr;
190
                }
191
192 142
                $lastOption = null;
193 142
                $state = 0;
194
            } else {
195 244
                if ($token->token === '(') {
196 6
                    ++$brackets;
197 244
                } elseif ($token->token === ')') {
198 6
                    --$brackets;
199
                }
200
201 244
                $ret->options[$lastOptionId]['expr'] .= $token->token;
202
203
                if (
204 244
                    ! (($token->token === '(') && ($brackets === 1)
205 244
                    || (($token->token === ')') && ($brackets === 0)))
206
                ) {
207
                    // First pair of brackets is being skipped.
208 244
                    $ret->options[$lastOptionId]['value'] .= $token->value;
209
                }
210
211
                // Checking if we finished parsing.
212 244
                if ($brackets === 0) {
213 244
                    $lastOption = null;
214
                }
215
            }
216
        }
217
218
        /*
219
         * We reached the end of statement without getting a value
220
         * for an option for which a value was required
221
         */
222
        if (
223 1148
            $state === 1
0 ignored issues
show
The condition $state === 1 is always false.
Loading history...
224
            && $lastOption
225 1148
            && ($lastOption[1] === 'expr'
226 1148
            || $lastOption[1] === 'var'
227 1148
            || $lastOption[1] === 'var='
228 1148
            || $lastOption[1] === 'expr=')
229
        ) {
230 8
            $parser->error(
231 8
                sprintf(
232 8
                    'Value/Expression for the option %1$s was expected.',
233 8
                    $ret->options[$lastOptionId]['name'],
234 8
                ),
235 8
                $list->tokens[$list->idx - 1],
236 8
            );
237
        }
238
239 1148
        if (empty($options['_UNSORTED'])) {
240 1148
            ksort($ret->options);
241
        }
242
243 1148
        --$list->idx;
244
245 1148
        return $ret;
246
    }
247
}
248