Passed
Pull Request — master (#483)
by
unknown
03:01
created

Condition::buildAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Components;
6
7
use PhpMyAdmin\SqlParser\Component;
8
use PhpMyAdmin\SqlParser\Parser;
9
use PhpMyAdmin\SqlParser\Token;
10
use PhpMyAdmin\SqlParser\TokensList;
11
12
use function implode;
13
use function in_array;
14
use function trim;
15
16
/**
17
 * `WHERE` keyword parser.
18
 */
19
final class Condition implements Component
20
{
21
    /**
22
     * Logical operators that can be used to delimit expressions.
23
     *
24
     * @var string[]
25
     */
26
    public static $delimiters = [
27
        '&&',
28
        '||',
29
        'AND',
30
        'OR',
31
        'XOR',
32
    ];
33
34
    /**
35
     * List of allowed reserved keywords in conditions.
36
     *
37
     * @var array<string, int>
38
     */
39
    public static $allowedKeywords = [
40
        'ALL' => 1,
41
        'AND' => 1,
42
        'BETWEEN' => 1,
43
        'COLLATE' => 1,
44
        'EXISTS' => 1,
45
        'IF' => 1,
46
        'IN' => 1,
47
        'INTERVAL' => 1,
48
        'IS' => 1,
49
        'LIKE' => 1,
50
        'MATCH' => 1,
51
        'NOT IN' => 1,
52
        'NOT NULL' => 1,
53
        'NOT' => 1,
54
        'NULL' => 1,
55
        'OR' => 1,
56
        'REGEXP' => 1,
57
        'RLIKE' => 1,
58
        'SOUNDS' => 1,
59
        'XOR' => 1,
60
    ];
61
62
    /**
63
     * Identifiers recognized.
64
     *
65
     * @var array<int, mixed>
66
     */
67
    public $identifiers = [];
68
69
    /**
70
     * Whether this component is an operator.
71
     *
72
     * @var bool
73
     */
74
    public $isOperator = false;
75
76
    /**
77
     * The condition.
78
     *
79
     * @var string
80
     */
81
    public $expr;
82
83
    /**
84
     * @param string $expr the condition or the operator
85
     */
86 196
    public function __construct($expr = null)
87
    {
88 196
        $this->expr = trim((string) $expr);
89
    }
90
91
    /**
92
     * @param Parser               $parser  the parser that serves as context
93
     * @param TokensList           $list    the list of tokens that are being parsed
94
     * @param array<string, mixed> $options parameters for parsing
95
     *
96
     * @return Condition[]
97
     */
98 194
    public static function parse(Parser $parser, TokensList $list, array $options = []): array
99
    {
100 194
        $ret = [];
101
102 194
        $expr = new static();
103
104
        /**
105
         * Counts brackets.
106
         *
107
         * @var int
108
         */
109 194
        $brackets = 0;
110
111
        /**
112
         * Whether there was a `BETWEEN` keyword before or not.
113
         *
114
         * It is required to keep track of them because their structure contains
115
         * the keyword `AND`, which is also an operator that delimits
116
         * expressions.
117
         *
118
         * @var bool
119
         */
120 194
        $betweenBefore = false;
121
122 194
        for (; $list->idx < $list->count; ++$list->idx) {
123
            /**
124
             * Token parsed at this moment.
125
             */
126 194
            $token = $list->tokens[$list->idx];
127
128
            // End of statement.
129 194
            if ($token->type === Token::TYPE_DELIMITER) {
130 92
                break;
131
            }
132
133
            // Skipping whitespaces and comments.
134 194
            if ($token->type === Token::TYPE_COMMENT) {
135 12
                continue;
136
            }
137
138
            // Replacing all whitespaces (new lines, tabs, etc.) with a single
139
            // space character.
140 194
            if ($token->type === Token::TYPE_WHITESPACE) {
141 190
                $expr->expr .= ' ';
142 190
                continue;
143
            }
144
145
            // Conditions are delimited by logical operators.
146 194
            if (in_array($token->value, static::$delimiters, true)) {
147 36
                if ($betweenBefore && ($token->value === 'AND')) {
148
                    // The syntax of keyword `BETWEEN` is hard-coded.
149 4
                    $betweenBefore = false;
150
                } else {
151
                    // The expression ended.
152 36
                    $expr->expr = trim($expr->expr);
153 36
                    if (! empty($expr->expr)) {
154 36
                        $ret[] = $expr;
155
                    }
156
157
                    // Adding the operator.
158 36
                    $expr = new static($token->value);
159 36
                    $expr->isOperator = true;
160 36
                    $ret[] = $expr;
161
162
                    // Preparing to parse another condition.
163 36
                    $expr = new static();
164 36
                    continue;
165
                }
166
            }
167
168
            if (
169 194
                ($token->type === Token::TYPE_KEYWORD)
170 194
                && ($token->flags & Token::FLAG_KEYWORD_RESERVED)
171 194
                && ! ($token->flags & Token::FLAG_KEYWORD_FUNCTION)
172
            ) {
173 124
                if ($token->value === 'BETWEEN') {
174 4
                    $betweenBefore = true;
175
                }
176
177 124
                if (($brackets === 0) && empty(static::$allowedKeywords[$token->value])) {
178 116
                    break;
179
                }
180
            }
181
182 192
            if ($token->type === Token::TYPE_OPERATOR) {
183 184
                if ($token->value === '(') {
184 34
                    ++$brackets;
185 184
                } elseif ($token->value === ')') {
186 36
                    if ($brackets === 0) {
187 2
                        break;
188
                    }
189
190 34
                    --$brackets;
191
                }
192
            }
193
194 192
            $expr->expr .= $token->token;
195
            if (
196 192
                ($token->type !== Token::TYPE_NONE)
197 192
                && (($token->type !== Token::TYPE_KEYWORD)
198 192
                || ($token->flags & Token::FLAG_KEYWORD_RESERVED))
199 192
                && ($token->type !== Token::TYPE_STRING)
200 192
                && ($token->type !== Token::TYPE_SYMBOL)
201
            ) {
202 192
                continue;
203
            }
204
205 144
            if (in_array($token->value, $expr->identifiers)) {
206 34
                continue;
207
            }
208
209 144
            $expr->identifiers[] = $token->value;
210
        }
211
212
        // Last iteration was not processed.
213 194
        $expr->expr = trim($expr->expr);
214 194
        if (! empty($expr->expr)) {
215 192
            $ret[] = $expr;
216
        }
217
218 194
        --$list->idx;
219
220 194
        return $ret;
221
    }
222
223
    /**
224
     * @param Condition $component the component to be built
225
     */
226 62
    public static function build($component): string
227
    {
228 62
        return $component->expr;
229
    }
230
231
    /**
232
     * @param Condition[] $component the component to be built
233
     */
234 62
    public static function buildAll(array $component): string
235
    {
236 62
        return implode(' ', $component);
237
    }
238
239 62
    public function __toString(): string
240
    {
241 62
        return static::build($this);
242
    }
243
}
244