Passed
Pull Request — master (#334)
by Antoine
10:13
created

WithStatement   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 175
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 78
dl 0
loc 175
rs 10
c 2
b 0
f 0
wmc 24

3 Methods

Rating   Name   Duplication   Size   Complexity  
C parse() 0 79 14
B getSubTokenList() 0 37 7
A build() 0 10 3
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
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
        $state = 0;
80
        $wither = null;
81
82
        for (; $list->idx < $list->count; ++$list->idx) {
83
            /**
84
             * Token parsed at this moment.
85
             *
86
             * @var Token
87
             */
88
            $token = $list->tokens[$list->idx];
89
90
            // Skipping whitespaces and comments.
91
            if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
92
                continue;
93
            }
94
95
            if ($token->type === Token::TYPE_NONE) {
96
                $wither = $token->value;
97
                $this->withers[$wither] = new WithKeyword($wither);
98
                $state = 1;
99
                continue;
100
            }
101
102
            if ($state === 1) {
103
                if ($token->value === '(') {
104
                    $this->withers[$wither]->columns = Array2d::parse($parser, $list);
105
                    continue;
106
                }
107
108
                if ($token->keyword === 'AS') {
109
                    ++$list->idx;
110
                    $state = 2;
111
                    continue;
112
                }
113
            } elseif ($state === 2) {
114
                if ($token->value === '(') {
115
                    ++$list->idx;
116
                    $subList = $this->getSubTokenList($list);
117
                    if ($subList instanceof ParserException) {
118
                        $parser->errors[] = $subList;
119
                        continue;
120
                    }
121
122
                    $subParser = new Parser($subList);
123
124
                    if (count($subParser->errors)) {
125
                        foreach ($subParser->errors as $error) {
126
                            $parser->errors[] = $error;
127
                        }
128
                    }
129
130
                    $this->withers[$wither]->statement = $subParser;
131
                    continue;
132
                }
133
134
                if ($token->value === ',') {
135
                    $list->idx++;
136
                    $state = 0;
137
                    continue;
138
                }
139
140
                // nothing else
141
                break;
142
            }
143
        }
144
145
        --$list->idx;
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function build()
152
    {
153
        $str = 'WITH ';
154
155
        foreach ($this->withers as $wither) {
156
            $str .= $str === 'WITH ' ? '' : ', ';
157
            $str .= WithKeyword::build($wither);
158
        }
159
160
        return $str;
161
    }
162
163
    /**
164
     * @return ParserException | TokensList
165
     */
166
    private function getSubTokenList(TokensList $list)
167
    {
168
        $idx = $list->idx;
169
        /** @var Token $token */
170
        $token = $list->tokens[$list->idx];
171
        $openParenthesis = 0;
172
173
        while ($list->idx < $list->count) {
174
            if ($token->value === '(') {
175
                ++$openParenthesis;
176
            } elseif ($token->value === ')') {
177
                if (--$openParenthesis === -1) {
178
                    break;
179
                }
180
            }
181
182
            ++$list->idx;
183
            if (! isset($list->tokens[$list->idx])) {
184
                break;
185
            }
186
187
            $token = $list->tokens[$list->idx];
188
        }
189
190
        // performance improvement: return the error to avoid a try/catch in the loop
191
        if ($list->idx === $list->count) {
192
            --$list->idx;
193
194
            return new ParserException(
195
                Translator::gettext('A closing bracket was expected.'),
196
                $token
197
            );
198
        }
199
200
        $length = $list->idx - $idx;
201
202
        return new TokensList(array_slice($list->tokens, $idx, $length), $length);
203
    }
204
}
205