SqlToken   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Test Coverage

Coverage 93.33%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 47
eloc 100
c 2
b 0
f 0
dl 0
loc 395
ccs 98
cts 105
cp 0.9333
rs 8.64

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getContent() 0 3 1
A startOffset() 0 5 1
A offsetExists() 0 3 1
A offsetGet() 0 5 1
A offsetSet() 0 13 3
A offsetUnset() 0 9 2
A getHasChildren() 0 3 2
A getChildren() 0 3 1
A calculateOffset() 0 7 2
A getIsCollection() 0 3 1
A endOffset() 0 5 1
A __toString() 0 3 1
A getType() 0 3 1
A type() 0 5 1
A getSql() 0 19 3
A updateCollectionOffsets() 0 8 2
A content() 0 5 1
A parent() 0 5 1
C tokensMatch() 0 61 16
A setChildren() 0 10 2
A matches() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like SqlToken often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SqlToken, and based on these observations, apply Extract Interface, too.

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

238
            $result = $this->tokensMatch(/** @scrutinizer ignore-type */ $patternToken[0], $this, $offset, $firstMatchIndex, $lastMatchIndex);
Loading history...
239
        }
240
241 130
        return $result;
242
    }
243
244
    /**
245
     * Tests the given token to match the specified pattern token.
246
     */
247 130
    private function tokensMatch(
248
        self $patternToken,
249
        self $token,
250
        int $offset = 0,
251
        int &$firstMatchIndex = null,
252
        int &$lastMatchIndex = null
253
    ): bool {
254
        if (
255 130
            $patternToken->getIsCollection() !== $token->getIsCollection() ||
256 130
            (!$patternToken->getIsCollection() && $patternToken->content !== $token->content)
257
        ) {
258 130
            return false;
259
        }
260
261 130
        if ($patternToken->children === $token->children) {
262 130
            $firstMatchIndex = $lastMatchIndex = $offset;
263
264 130
            return true;
265
        }
266
267 130
        $firstMatchIndex = $lastMatchIndex = null;
268 130
        $wildcard = false;
269
270 130
        for ($index = 0, $count = count($patternToken->children); $index < $count; $index++) {
271
            /*
272
             Iterate token by token with an exception to "any" that toggles an iteration until matched
273
             with a next pattern token or EOF.
274
             */
275 130
            if ($patternToken[$index] instanceof self && $patternToken[$index]->content === 'any') {
276 130
                $wildcard = true;
277 130
                continue;
278
            }
279
280 130
            for ($limit = $wildcard ? count($token->children) : $offset + 1; $offset < $limit; $offset++) {
281 130
                if (!$wildcard && !isset($token[$offset])) {
282
                    break;
283
                }
284
285
                if (
286 130
                    $patternToken[$index] instanceof self &&
287 130
                    $token[$offset] instanceof self  &&
288 130
                    !$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\SqlToken::tokensMatch() does only seem to accept Yiisoft\Db\Sqlite\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

288
                    !$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\SqlToken::tokensMatch() does only seem to accept Yiisoft\Db\Sqlite\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

288
                    !$this->tokensMatch(/** @scrutinizer ignore-type */ $patternToken[$index], $token[$offset])
Loading history...
289
                ) {
290 130
                    continue;
291
                }
292
293 130
                if ($firstMatchIndex === null) {
294 130
                    $firstMatchIndex = $offset;
295
                }
296
297 130
                $lastMatchIndex = $offset;
298 130
                $wildcard = false;
299 130
                $offset++;
300
301 130
                continue 2;
302
            }
303
304 130
            return false;
305
        }
306
307 129
        return true;
308
    }
309
310
    /**
311
     * Returns an absolute offset in the children array.
312
     */
313 171
    private function calculateOffset(int $offset): int
314
    {
315 171
        if ($offset >= 0) {
316 171
            return $offset;
317
        }
318
319 171
        return count($this->children) + $offset;
320
    }
321
322
    /**
323
     * Updates token SQL code start and end offsets based on its children.
324
     */
325 172
    private function updateCollectionOffsets(): void
326
    {
327 172
        if (!empty($this->children)) {
328 172
            $this->startOffset = reset($this->children)->startOffset;
329 172
            $this->endOffset = end($this->children)->endOffset;
330
        }
331
332 172
        $this->parent?->updateCollectionOffsets();
333
    }
334
335
    /**
336
     * Set token type.
337
     *
338
     * @param int $value Token type.
339
     * It has to be one of the following constants:
340
     *
341
     * - {@see TYPE_CODE}
342
     * - {@see TYPE_STATEMENT}
343
     * - {@see TYPE_TOKEN}
344
     * - {@see TYPE_PARENTHESIS}
345
     * - {@see TYPE_KEYWORD}
346
     * - {@see TYPE_OPERATOR}
347
     * - {@see TYPE_IDENTIFIER}
348
     * - {@see TYPE_STRING_LITERAL}
349
     */
350 171
    public function type(int $value): self
351
    {
352 171
        $this->type = $value;
353
354 171
        return $this;
355
    }
356
357
    /**
358
     * Set token content.
359
     */
360 173
    public function content(string|null $value): self
361
    {
362 173
        $this->content = $value;
363
364 173
        return $this;
365
    }
366
367
    /**
368
     * Set original SQL token start position.
369
     *
370
     * @param int $value Original SQL token start position.
371
     */
372 171
    public function startOffset(int $value): self
373
    {
374 171
        $this->startOffset = $value;
375
376 171
        return $this;
377
    }
378
379
    /**
380
     * Set original SQL token end position.
381
     *
382
     * @param int $value Original SQL token end position.
383
     */
384 171
    public function endOffset(int $value): self
385
    {
386 171
        $this->endOffset = $value;
387
388 171
        return $this;
389
    }
390
391
    /**
392
     * Set parent token.
393
     *
394
     * @param SqlToken $value The parent token.
395
     */
396
    public function parent(self $value): self
397
    {
398
        $this->parent = $value;
399
400
        return $this;
401
    }
402
403
    /**
404
     * @return string|null The token content.
405
     */
406 3
    public function getContent(): string|null
407
    {
408 3
        return $this->content;
409
    }
410
411
    /**
412
     * @return int The type of the token.
413
     */
414
    public function getType(): int
415
    {
416
        return $this->type;
417
    }
418
}
419