Passed
Pull Request — master (#334)
by Antoine
12:24
created

WithStatement::getSubTokenList()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 20
nc 16
nop 1
dl 0
loc 37
rs 8.6666
c 1
b 0
f 0
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\Statement;
15
use PhpMyAdmin\SqlParser\Parser;
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
    public function parse(Parser $parser, TokensList $list)
68
    {
69
        ++$list->idx; // Skipping `WITH`.
70
71
        // parse any options if provided
72
        $this->options = OptionsArray::parse(
73
            $parser,
74
            $list,
75
            static::$OPTIONS
76
        );
77
        ++$list->idx;
78
79
        /**
80
         * The state of the parser.
81
         *
82
         * Below are the states of the parser.
83
         *
84
         *      0 ---------------- [ name ] -----------------> 1
85
         *      1 -------------- [( columns )] AS ----------------> 2
86
         *      2 ------------------ [ , ] --------------------> 0
87
         *
88
         * @var int
89
         */
90
        $state = 0;
91
        $wither = null;
92
93
        for (; $list->idx < $list->count; ++$list->idx) {
94
            /**
95
             * Token parsed at this moment.
96
             *
97
             * @var Token
98
             */
99
            $token = $list->tokens[$list->idx];
100
101
            // Skipping whitespaces and comments.
102
            if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
103
                continue;
104
            }
105
106
            if ($token->type === Token::TYPE_NONE) {
107
                $wither = $token->value;
108
                $this->withers[$wither] = new WithKeyword($wither);
109
                $state = 1;
110
                continue;
111
            }
112
113
            if ($state === 1) {
114
                if ($token->value === '(') {
115
                    $this->withers[$wither]->columns = Array2d::parse($parser, $list);
116
                    continue;
117
                }
118
119
                if ($token->keyword === 'AS') {
120
                    ++$list->idx;
121
                    $state = 2;
122
                    continue;
123
                }
124
            } elseif ($state === 2) {
125
                if ($token->value === '(') {
126
                    ++$list->idx;
127
                    $subList = $this->getSubTokenList($list);
128
                    if ($subList instanceof ParserException) {
129
                        $parser->errors[] = $subList;
130
                        continue;
131
                    }
132
133
                    $subParser = new Parser($subList);
134
135
                    if (count($subParser->errors)) {
136
                        foreach ($subParser->errors as $error) {
137
                            $parser->errors[] = $error;
138
                        }
139
                    }
140
141
                    $this->withers[$wither]->statement = $subParser;
142
                    continue;
143
                }
144
145
                // There's another WITH expression to parse, go back to state=0
146
                if ($token->value === ',') {
147
                    $list->idx++;
148
                    $state = 0;
149
                    continue;
150
                }
151
152
                // No more WITH expressions, we're done with this statement
153
                break;
154
            }
155
        }
156
157
        --$list->idx;
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163
    public function build()
164
    {
165
        $str = 'WITH ';
166
167
        foreach ($this->withers as $wither) {
168
            $str .= $str === 'WITH ' ? '' : ', ';
169
            $str .= WithKeyword::build($wither);
170
        }
171
172
        return $str;
173
    }
174
175
    /**
176
     * Get tokens within the WITH expression to use them in another parser
177
     *
178
     * @return ParserException|TokensList
179
     */
180
    private function getSubTokenList(TokensList $list)
181
    {
182
        $idx = $list->idx;
183
        /** @var Token $token */
184
        $token = $list->tokens[$list->idx];
185
        $openParenthesis = 0;
186
187
        while ($list->idx < $list->count) {
188
            if ($token->value === '(') {
189
                ++$openParenthesis;
190
            } elseif ($token->value === ')') {
191
                if (--$openParenthesis === -1) {
192
                    break;
193
                }
194
            }
195
196
            ++$list->idx;
197
            if (! isset($list->tokens[$list->idx])) {
198
                break;
199
            }
200
201
            $token = $list->tokens[$list->idx];
202
        }
203
204
        // performance improvement: return the error to avoid a try/catch in the loop
205
        if ($list->idx === $list->count) {
206
            --$list->idx;
207
208
            return new ParserException(
209
                Translator::gettext('A closing bracket was expected.'),
210
                $token
211
            );
212
        }
213
214
        $length = $list->idx - $idx;
215
216
        return new TokensList(array_slice($list->tokens, $idx, $length), $length);
217
    }
218
}
219