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

WithStatement::getSubTokenList()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 14
c 1
b 0
f 0
nc 10
nop 1
dl 0
loc 27
rs 9.2222
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\Statement;
14
use PhpMyAdmin\SqlParser\Parser;
15
use PhpMyAdmin\SqlParser\Token;
16
use PhpMyAdmin\SqlParser\TokensList;
17
use RuntimeException;
18
19
use function array_slice;
20
use function count;
21
22
/**
23
 * `WITH` statement.
24
25
 *  WITH [RECURSIVE] query_name [ (column_name [,...]) ] AS (SELECT ...) [, ...]
26
 */
27
class WithStatement extends Statement
28
{
29
    /**
30
     * Options for `WITH` statements and their slot ID.
31
     *
32
     * @var mixed[]
33
     */
34
    public static $OPTIONS = ['RECURSIVE' => 1];
35
36
    /**
37
     * The clauses of this statement, in order.
38
     *
39
     * @see Statement::$CLAUSES
40
     *
41
     * @var mixed[]
42
     */
43
    public static $CLAUSES = [
44
        'WITH' => [
45
            'WITH',
46
            2,
47
        ],
48
        // Used for options.
49
        '_OPTIONS' => [
50
            '_OPTIONS',
51
            1,
52
        ],
53
        'AS' => [
54
            'AS',
55
            2,
56
        ],
57
    ];
58
59
    /** @var WithKeyword[] */
60
    public $withers = [];
61
62
    /**
63
     * @param Parser     $parser the instance that requests parsing
64
     * @param TokensList $list   the list of tokens to be parsed
65
     */
66
    public function parse(Parser $parser, TokensList $list)
67
    {
68
        ++$list->idx; // Skipping `WITH`.
69
70
        // parse any options if provided
71
        $this->options = OptionsArray::parse(
72
            $parser,
73
            $list,
74
            static::$OPTIONS
75
        );
76
        ++$list->idx;
77
78
        $state = 0;
79
        $wither = null;
80
81
        for (; $list->idx < $list->count; ++$list->idx) {
82
            /**
83
             * Token parsed at this moment.
84
             *
85
             * @var Token
86
             */
87
            $token = $list->tokens[$list->idx];
88
89
            // Skipping whitespaces and comments.
90
            if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
91
                continue;
92
            }
93
94
            if ($token->type === Token::TYPE_NONE) {
95
                $wither = $token->value;
96
                $this->withers[$wither] = new WithKeyword($wither);
97
                $state = 1;
98
                continue;
99
            }
100
101
            if ($state === 1) {
102
                if ($token->value === '(') {
103
                    $this->withers[$wither]->columns = Array2d::parse($parser, $list);
104
                    continue;
105
                }
106
107
                if ($token->keyword === 'AS') {
108
                    ++$list->idx;
109
                    $state = 2;
110
                    continue;
111
                }
112
            } elseif ($state === 2) {
113
                if ($token->value === '(') {
114
                    ++$list->idx;
115
                    $subList = $this->getSubTokenList($list);
116
                    $subParser = new Parser($subList);
117
118
                    if (count($subParser->errors)) {
119
                        foreach ($subParser->errors as $error) {
120
                            $parser->errors[] = $error;
121
                        }
122
                    }
123
124
                    $this->withers[$wither]->statement = $subParser;
125
                    continue;
126
                }
127
128
                if ($token->value === ',') {
129
                    $list->idx++;
130
                    $state = 0;
131
                    continue;
132
                }
133
134
                // nothing else
135
                break;
136
            }
137
        }
138
139
        --$list->idx;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function build()
146
    {
147
        $str = 'WITH ';
148
149
        foreach ($this->withers as $wither) {
150
            $str .= $str === 'WITH ' ? '' : ', ';
151
            $str .= WithKeyword::build($wither);
152
        }
153
154
        return $str;
155
    }
156
157
    private function getSubTokenList(TokensList $list): TokensList
158
    {
159
        $idx = $list->idx;
160
        /** @var Token $token */
161
        $token = $list->tokens[$list->idx];
162
        $openParenthesis = 0;
163
164
        while ($list->idx < $list->count) {
165
            // var_dump($idx, $token->value);
166
            if ($token->value === '(') {
167
                ++$openParenthesis;
168
            } elseif ($token->value === ')') {
169
                if (--$openParenthesis === -1) {
170
                    break;
171
                }
172
            }
173
174
            $token = $list->tokens[++$list->idx];
175
        }
176
177
        if ($list->idx === $list->count) {
178
            throw new RuntimeException('Syntax error no matching closing parenthesis.');
179
        }
180
181
        $length = $list->idx - $idx;
182
183
        return new TokensList(array_slice($list->tokens, $idx, $length), $length);
184
    }
185
}
186