Passed
Push — master ( 8d8336...ff76ee )
by William
291:59 queued 237:03
created

WithStatement::getSubTokenList()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7

Importance

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