Passed
Push — master ( 6c3f09...c54108 )
by William
03:43
created

Condition::__toString()   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 0
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 is_array;
15
use function trim;
16
17
/**
18
 * `WHERE` keyword parser.
19
 */
20
final class Condition implements Component
21
{
22
    /**
23
     * Logical operators that can be used to delimit expressions.
24
     *
25
     * @var string[]
26
     */
27
    public static $delimiters = [
28
        '&&',
29
        '||',
30
        'AND',
31
        'OR',
32
        'XOR',
33
    ];
34
35
    /**
36
     * List of allowed reserved keywords in conditions.
37
     *
38
     * @var array<string, int>
39
     */
40
    public static $allowedKeywords = [
41
        'ALL' => 1,
42
        'AND' => 1,
43
        'BETWEEN' => 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 184
    public function __construct($expr = null)
87
    {
88 184
        $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 182
    public static function parse(Parser $parser, TokensList $list, array $options = [])
99
    {
100 182
        $ret = [];
101
102 182
        $expr = new static();
103
104
        /**
105
         * Counts brackets.
106
         *
107
         * @var int
108
         */
109 182
        $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 182
        $betweenBefore = false;
121
122 182
        for (; $list->idx < $list->count; ++$list->idx) {
123
            /**
124
             * Token parsed at this moment.
125
             */
126 182
            $token = $list->tokens[$list->idx];
127
128
            // End of statement.
129 182
            if ($token->type === Token::TYPE_DELIMITER) {
130 80
                break;
131
            }
132
133
            // Skipping whitespaces and comments.
134 182
            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 182
            if ($token->type === Token::TYPE_WHITESPACE) {
141 178
                $expr->expr .= ' ';
142 178
                continue;
143
            }
144
145
            // Conditions are delimited by logical operators.
146 182
            if (in_array($token->value, static::$delimiters, true)) {
147 34
                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 34
                    $expr->expr = trim($expr->expr);
153 34
                    if (! empty($expr->expr)) {
154 34
                        $ret[] = $expr;
155
                    }
156
157
                    // Adding the operator.
158 34
                    $expr = new static($token->value);
159 34
                    $expr->isOperator = true;
160 34
                    $ret[] = $expr;
161
162
                    // Preparing to parse another condition.
163 34
                    $expr = new static();
164 34
                    continue;
165
                }
166
            }
167
168
            if (
169 182
                ($token->type === Token::TYPE_KEYWORD)
170 182
                && ($token->flags & Token::FLAG_KEYWORD_RESERVED)
171 182
                && ! ($token->flags & Token::FLAG_KEYWORD_FUNCTION)
172
            ) {
173 114
                if ($token->value === 'BETWEEN') {
174 4
                    $betweenBefore = true;
175
                }
176
177 114
                if (($brackets === 0) && empty(static::$allowedKeywords[$token->value])) {
178 108
                    break;
179
                }
180
            }
181
182 180
            if ($token->type === Token::TYPE_OPERATOR) {
183 172
                if ($token->value === '(') {
184 34
                    ++$brackets;
185 172
                } elseif ($token->value === ')') {
186 36
                    if ($brackets === 0) {
187 2
                        break;
188
                    }
189
190 34
                    --$brackets;
191
                }
192
            }
193
194 180
            $expr->expr .= $token->token;
195
            if (
196 180
                ($token->type !== Token::TYPE_NONE)
197 180
                && (($token->type !== Token::TYPE_KEYWORD)
198 180
                || ($token->flags & Token::FLAG_KEYWORD_RESERVED))
199 180
                && ($token->type !== Token::TYPE_STRING)
200 180
                && ($token->type !== Token::TYPE_SYMBOL)
201
            ) {
202 180
                continue;
203
            }
204
205 132
            if (in_array($token->value, $expr->identifiers)) {
206 30
                continue;
207
            }
208
209 132
            $expr->identifiers[] = $token->value;
210
        }
211
212
        // Last iteration was not processed.
213 182
        $expr->expr = trim($expr->expr);
214 182
        if (! empty($expr->expr)) {
215 180
            $ret[] = $expr;
216
        }
217
218 182
        --$list->idx;
219
220 182
        return $ret;
221
    }
222
223
    /**
224
     * @param Condition[]          $component the component to be built
225
     * @param array<string, mixed> $options   parameters for building
226
     */
227 60
    public static function build($component, array $options = []): string
228
    {
229 60
        if (is_array($component)) {
0 ignored issues
show
introduced by
The condition is_array($component) is always true.
Loading history...
230 60
            return implode(' ', $component);
231
        }
232
233 60
        return $component->expr;
234
    }
235
236 60
    public function __toString(): string
237
    {
238 60
        return static::build($this);
0 ignored issues
show
Bug introduced by
$this of type PhpMyAdmin\SqlParser\Components\Condition is incompatible with the type PhpMyAdmin\SqlParser\Components\Condition[] expected by parameter $component of PhpMyAdmin\SqlParser\Components\Condition::build(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

238
        return static::build(/** @scrutinizer ignore-type */ $this);
Loading history...
239
    }
240
}
241