Passed
Push — master ( 1265f7...db8617 )
by Alexander
01:41
created

SqlToken::tokensMatch()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 56
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 13.0096

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 26
c 1
b 0
f 0
nc 12
nop 5
dl 0
loc 56
ccs 25
cts 26
cp 0.9615
crap 13.0096
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\Token;
6
7
/**
8
 * SqlToken represents SQL tokens produced by {@see SqlTokenizer} or its child classes.
9
 *
10
 * @property SqlToken[] $children Child tokens.
11
 * @property bool $hasChildren Whether the token has children. This property is read-only.
12
 * @property bool $isCollection Whether the token represents a collection of tokens. This property is
13
 * read-only.
14
 * @property string $sql SQL code. This property is read-only.
15
 */
16
class SqlToken implements \ArrayAccess
17
{
18
    public const TYPE_CODE = 0;
19
    public const TYPE_STATEMENT = 1;
20
    public const TYPE_TOKEN = 2;
21
    public const TYPE_PARENTHESIS = 3;
22
    public const TYPE_KEYWORD = 4;
23
    public const TYPE_OPERATOR = 5;
24
    public const TYPE_IDENTIFIER = 6;
25
    public const TYPE_STRING_LITERAL = 7;
26
27
    private int $type = self::TYPE_TOKEN;
28
    private ?string $content = null;
29
    private ?int $startOffset = null;
30
    private ?int $endOffset = null;
31
    private ?SqlToken $parent = null;
32
    private array $children = [];
33
34
    /**
35
     * Returns the SQL code representing the token.
36
     *
37
     * @return string SQL code.
38
     */
39
    public function __toString(): string
40
    {
41
        return $this->getSql();
42
    }
43
44
    /**
45
     * Returns whether there is a child token at the specified offset.
46
     *
47
     * This method is required by the SPL {@see \ArrayAccess} interface. It is implicitly called when you use something
48
     * like `isset($token[$offset])`.
49
     *
50
     * @param int $offset child token offset.
51
     *
52
     * @return bool whether the token exists.
53
     */
54 12
    public function offsetExists($offset): bool
55
    {
56 12
        return isset($this->children[$this->calculateOffset($offset)]);
57
    }
58
59
    /**
60
     * Returns a child token at the specified offset.
61
     *
62
     * This method is required by the SPL {@see \ArrayAccess} interface. It is implicitly called when you use something
63
     * like `$child = $token[$offset];`.
64
     *
65
     * @param int $offset child token offset.
66
     *
67
     * @return SqlToken|null the child token at the specified offset, `null` if there's no token.
68
     */
69 17
    public function offsetGet($offset): ?SqlToken
70
    {
71 17
        $offset = $this->calculateOffset($offset);
72
73 17
        return $this->children[$offset] ?? null;
74
    }
75
76
    /**
77
     * Adds a child token to the token.
78
     *
79
     * This method is required by the SPL {@see \ArrayAccess} interface. It is implicitly called when you use something
80
     * like `$token[$offset] = $child;`.
81
     *
82
     * @param int|null $offset child token offset.
83
     * @param SqlToken $token  token to be added.
84
     */
85 17
    public function offsetSet($offset, $token): void
86
    {
87 17
        $token->parent = $this;
88
89 17
        if ($offset === null) {
90 17
            $this->children[] = $token;
91
        } else {
92
            $this->children[$this->calculateOffset($offset)] = $token;
93
        }
94
95 17
        $this->updateCollectionOffsets();
96 17
    }
97
98
    /**
99
     * Removes a child token at the specified offset.
100
     *
101
     * This method is required by the SPL {@see \ArrayAccess} interface. It is implicitly called when you use something
102
     * like `unset($token[$offset])`.
103
     *
104
     * @param int $offset child token offset.
105
     */
106 5
    public function offsetUnset($offset): void
107
    {
108 5
        $offset = $this->calculateOffset($offset);
109
110 5
        if (isset($this->children[$offset])) {
111 5
            \array_splice($this->children, $offset, 1);
112
        }
113
114 5
        $this->updateCollectionOffsets();
115 5
    }
116
117
    /**
118
     * Returns child tokens.
119
     *
120
     * @return SqlToken[] child tokens.
121
     */
122 5
    public function getChildren(): array
123
    {
124 5
        return $this->children;
125
    }
126
127
    /**
128
     * Sets a list of child tokens.
129
     *
130
     * @param SqlToken[] $children child tokens.
131
     */
132
    public function setChildren(array $children): void
133
    {
134
        $this->children = [];
135
136
        foreach ($children as $child) {
137
            $child->parent = $this;
138
            $this->children[] = $child;
139
        }
140
141
        $this->updateCollectionOffsets();
142
    }
143
144
    /**
145
     * Returns whether the token represents a collection of tokens.
146
     *
147
     * @return bool whether the token represents a collection of tokens.
148
     */
149 17
    public function getIsCollection(): bool
150
    {
151 17
        return \in_array($this->type, [
152 17
            self::TYPE_CODE,
153 17
            self::TYPE_STATEMENT,
154 17
            self::TYPE_PARENTHESIS,
155 17
        ], true);
156
    }
157
158
    /**
159
     * Returns whether the token represents a collection of tokens and has non-zero number of children.
160
     *
161
     * @return bool whether the token has children.
162
     */
163 17
    public function getHasChildren(): bool
164
    {
165 17
        return $this->getIsCollection() && !empty($this->children);
166
    }
167
168
    /**
169
     * Returns the SQL code representing the token.
170
     *
171
     * @return string SQL code.
172
     */
173 8
    public function getSql(): string
174
    {
175 8
        $code = $this;
176 8
        while ($code->parent !== null) {
177 8
            $code = $code->parent;
178
        }
179
180 8
        return mb_substr($code->content, $this->startOffset, $this->endOffset - $this->startOffset, 'UTF-8');
181
    }
182
183
    /**
184
     * Returns whether this token (including its children) matches the specified "pattern" SQL code.
185
     *
186
     * Usage Example:
187
     *
188
     * ```php
189
     * $patternToken = (new \Yiisoft\Db\Sqlite\SqlTokenizer('SELECT any FROM any'))->tokenize();
190
     * if ($sqlToken->matches($patternToken, 0, $firstMatchIndex, $lastMatchIndex)) {
191
     *     // ...
192
     * }
193
     * ```
194
     *
195
     * @param SqlToken $patternToken tokenized SQL code to match against. In addition to normal SQL, the `any` keyword
196
     * is supported which will match any number of keywords, identifiers, whitespaces.
197
     * @param int $offset token children offset to start lookup with.
198
     * @param int|null $firstMatchIndex token children offset where a successful match begins.
199
     * @param int|null $lastMatchIndex  token children offset where a successful match ends.
200
     *
201
     * @return bool whether this token matches the pattern SQL code.
202
     */
203 12
    public function matches(
204
        self $patternToken,
205
        int $offset = 0,
206
        ?int &$firstMatchIndex = null,
207
        ?int &$lastMatchIndex = null
208
    ): bool {
209 12
        if (!$patternToken->getHasChildren()) {
210
            return false;
211
        }
212
213 12
        $patternToken = $patternToken[0];
214
215 12
        return $this->tokensMatch($patternToken, $this, $offset, $firstMatchIndex, $lastMatchIndex);
0 ignored issues
show
Bug introduced by
It seems like $patternToken can also be of type null; however, parameter $patternToken of Yiisoft\Db\Sqlite\Token\SqlToken::tokensMatch() does only seem to accept Yiisoft\Db\Sqlite\Token\SqlToken, maybe add an additional type check? ( Ignorable by Annotation )

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

215
        return $this->tokensMatch(/** @scrutinizer ignore-type */ $patternToken, $this, $offset, $firstMatchIndex, $lastMatchIndex);
Loading history...
216
    }
217
218
    /**
219
     * Tests the given token to match the specified pattern token.
220
     *
221
     * @param SqlToken $patternToken
222
     * @param SqlToken $token
223
     * @param int $offset
224
     * @param int|null $firstMatchIndex
225
     * @param int|null $lastMatchIndex
226
     *
227
     * @return bool
228
     */
229 12
    private function tokensMatch(
230
        self $patternToken,
231
        self $token,
232
        int $offset = 0,
233
        ?int &$firstMatchIndex = null,
234
        ?int &$lastMatchIndex = null
235
    ): bool {
236 12
        if ($patternToken->getIsCollection() !== $token->getIsCollection() || (!$patternToken->getIsCollection() && $patternToken->content !== $token->content)) {
237 12
            return false;
238
        }
239
240 12
        if ($patternToken->children === $token->children) {
241 12
            $firstMatchIndex = $lastMatchIndex = $offset;
242
243 12
            return true;
244
        }
245
246 12
        $firstMatchIndex = $lastMatchIndex = null;
247 12
        $wildcard = false;
248
249 12
        for ($index = 0, $count = count($patternToken->children); $index < $count; $index++) {
250
            /**
251
             *  Here we iterate token by token with an exception of "any" that toggles an iteration until we matched
252
             *  with a next pattern token or EOF.
253
             */
254 12
            if ($patternToken[$index]->content === 'any') {
255 12
                $wildcard = true;
256 12
                continue;
257
            }
258
259 12
            for ($limit = $wildcard ? count($token->children) : $offset + 1; $offset < $limit; $offset++) {
260 12
                if (!$wildcard && !isset($token[$offset])) {
261
                    break;
262
                }
263
264 12
                if (!$this->tokensMatch($patternToken[$index], $token[$offset])) {
0 ignored issues
show
Bug introduced by
It seems like $token[$offset] can also be of type null; however, parameter $token of Yiisoft\Db\Sqlite\Token\SqlToken::tokensMatch() does only seem to accept Yiisoft\Db\Sqlite\Token\SqlToken, maybe add an additional type check? ( Ignorable by Annotation )

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

264
                if (!$this->tokensMatch($patternToken[$index], /** @scrutinizer ignore-type */ $token[$offset])) {
Loading history...
Bug introduced by
It seems like $patternToken[$index] can also be of type null; however, parameter $patternToken of Yiisoft\Db\Sqlite\Token\SqlToken::tokensMatch() does only seem to accept Yiisoft\Db\Sqlite\Token\SqlToken, maybe add an additional type check? ( Ignorable by Annotation )

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

264
                if (!$this->tokensMatch(/** @scrutinizer ignore-type */ $patternToken[$index], $token[$offset])) {
Loading history...
265 12
                    continue;
266
                }
267
268 12
                if ($firstMatchIndex === null) {
269 12
                    $firstMatchIndex = $offset;
270 12
                    $lastMatchIndex = $offset;
271
                } else {
272 12
                    $lastMatchIndex = $offset;
273
                }
274
275 12
                $wildcard = false;
276 12
                $offset++;
277
278 12
                continue 2;
279
            }
280
281 12
            return false;
282
        }
283
284 12
        return true;
285
    }
286
287
    /**
288
     * Returns an absolute offset in the children array.
289
     *
290
     * @param int $offset
291
     *
292
     * @return int
293
     */
294 17
    private function calculateOffset(int $offset): int
295
    {
296 17
        if ($offset >= 0) {
297 17
            return $offset;
298
        }
299
300 17
        return count($this->children) + $offset;
301
    }
302
303
    /**
304
     * Updates token SQL code start and end offsets based on its children.
305
     */
306 17
    private function updateCollectionOffsets(): void
307
    {
308 17
        if (!empty($this->children)) {
309 17
            $this->startOffset = reset($this->children)->startOffset;
310 17
            $this->endOffset = end($this->children)->endOffset;
311
        }
312
313 17
        if ($this->parent !== null) {
314 17
            $this->parent->updateCollectionOffsets();
315
        }
316 17
    }
317
318
    /**
319
     * Set token type. It has to be one of the following constants:
320
     *
321
     * - {@see TYPE_CODE}
322
     * - {@see TYPE_STATEMENT}
323
     * - {@see TYPE_TOKEN}
324
     * - {@see TYPE_PARENTHESIS}
325
     * - {@see TYPE_KEYWORD}
326
     * - {@see TYPE_OPERATOR}
327
     * - {@see TYPE_IDENTIFIER}
328
     * - {@see TYPE_STRING_LITERAL}
329
     *
330
     * @param int $value token type. It has to be one of the following constants:
331
     */
332 17
    public function setType(int $value): void
333
    {
334 17
        $this->type = $value;
335 17
    }
336
337
    /**
338
     * Set token content.
339
     *
340
     * @param string|null $content token content.
341
     */
342 17
    public function setContent(?string $value): void
343
    {
344 17
        $this->content = $value;
345 17
    }
346
347
    /**
348
     * Set original SQL token start position.
349
     *
350
     * @param int $value original SQL token start position.
351
     */
352 17
    public function setStartOffset(int $value): void
353
    {
354 17
        $this->startOffset = $value;
355 17
    }
356
357
    /**
358
     * Set original SQL token end position.
359
     *
360
     * @param int $value original SQL token end position.
361
     */
362 17
    public function setEndOffset(int $value): void
363
    {
364 17
        $this->endOffset = $value;
365 17
    }
366
367
    /**
368
     * Set parent token.
369
     *
370
     * @param SqlToken $value parent token.
371
     */
372
    public function setParent(SqlToken $value): void
373
    {
374
        $this->parent = $value;
375
    }
376
}
377