Formatter::mergeFormats()   C
last analyzed

Complexity

Conditions 12
Paths 120

Size

Total Lines 54
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 12

Importance

Changes 0
Metric Value
cc 12
eloc 28
nc 120
nop 2
dl 0
loc 54
ccs 31
cts 31
cp 1
crap 12
rs 6.8
c 0
b 0
f 0

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 PhpMyAdmin\SqlParser\Utils;
6
7
use PhpMyAdmin\SqlParser\Components\JoinKeyword;
8
use PhpMyAdmin\SqlParser\Lexer;
9
use PhpMyAdmin\SqlParser\Parser;
10
use PhpMyAdmin\SqlParser\Token;
11
use PhpMyAdmin\SqlParser\TokensList;
12
use PhpMyAdmin\SqlParser\TokenType;
13
14
use function array_merge;
15
use function array_pop;
16
use function end;
17
use function htmlspecialchars;
18
use function in_array;
19
use function mb_strlen;
20
use function str_contains;
21
use function str_repeat;
22
use function str_replace;
23
use function strtoupper;
24
25
use const ENT_NOQUOTES;
26
use const PHP_SAPI;
27
28
/**
29
 * Utilities that are used for formatting queries.
30
 */
31
class Formatter
32
{
33
    /**
34
     * The formatting options.
35
     *
36
     * @var array<string, bool|string|array<int, array<string, int|string>>>
37
     */
38
    public array $options;
39
40
    /**
41
     * Clauses that are usually short.
42
     *
43
     * These clauses share the line with the next clause.
44
     *
45
     * E.g. if INSERT was not here, the formatter would produce:
46
     *
47
     *      INSERT
48
     *      INTO foo
49
     *      VALUES(0, 0, 0),(1, 1, 1);
50
     *
51
     * Instead of:
52
     *
53
     *      INSERT INTO foo
54
     *      VALUES(0, 0, 0),(1, 1, 1)
55
     *
56
     * @var array<string, bool>
57
     */
58
    public static array $shortClauses = [
59
        'CREATE' => true,
60
        'INSERT' => true,
61
    ];
62
63
    /**
64
     * Clauses that must be inlined.
65
     *
66
     * These clauses usually are short and it's nicer to have them inline.
67
     *
68
     * @var array<string, bool>
69
     */
70
    public static array $inlineClauses = [
71
        'CREATE' => true,
72
        'INTO' => true,
73
        'LIMIT' => true,
74
        'PARTITION BY' => true,
75
        'PARTITION' => true,
76
        'PROCEDURE' => true,
77
        'SUBPARTITION BY' => true,
78
        'VALUES' => true,
79
    ];
80
81
    private const FORMATTERS = [
82
        'PARTITION BY',
83
        'SUBPARTITION BY',
84
    ];
85
86
    /** @param array<string, bool|string|array<int, array<string, int|string>>> $options the formatting options */
87 48
    public function __construct(array $options = [])
88
    {
89 48
        $this->options = $this->getMergedOptions($options);
90
    }
91
92
    /**
93
     * The specified formatting options are merged with the default values.
94
     *
95
     * @param array<string, bool|string|array<int, array<string, int|string>>> $options
96
     *
97
     * @return array<string, bool|string|array<int, array<string, int|string>>>
98
     */
99 56
    protected function getMergedOptions(array $options): array
100
    {
101 56
        $options = array_merge(
102 56
            $this->getDefaultOptions(),
103 56
            $options,
104 56
        );
105
106 56
        if (isset($options['formats'])) {
107 8
            $options['formats'] = self::mergeFormats($this->getDefaultFormats(), $options['formats']);
108
        } else {
109 48
            $options['formats'] = $this->getDefaultFormats();
110
        }
111
112 56
        if ($options['line_ending'] === null) {
113 48
            $options['line_ending'] = $options['type'] === 'html' ? '<br/>' : "\n";
114
        }
115
116 56
        if ($options['indentation'] === null) {
117 56
            $options['indentation'] = $options['type'] === 'html' ? '&nbsp;&nbsp;&nbsp;&nbsp;' : '    ';
118
        }
119
120
        // `parts_newline` requires `clause_newline`
121 56
        $options['parts_newline'] &= $options['clause_newline'];
122
123 56
        return $options;
124
    }
125
126
    /**
127
     * The default formatting options.
128
     *
129
     * @return array<string, bool|string|null>
130
     * @psalm-return array{
131
     *   type: ('cli'|'text'),
132
     *   line_ending: null,
133
     *   indentation: null,
134
     *   remove_comments: false,
135
     *   clause_newline: true,
136
     *   parts_newline: true,
137
     *   indent_parts: true
138
     * }
139
     */
140 48
    protected function getDefaultOptions(): array
141
    {
142 48
        return [
143
            /*
144
             * The format of the result.
145
             *
146
             * @var string The type ('text', 'cli' or 'html')
147
             */
148 48
            'type' => PHP_SAPI === 'cli' ? 'cli' : 'text',
149
150
            /*
151
             * The line ending used.
152
             * By default, for text this is "\n" and for HTML this is "<br/>".
153
             *
154
             * @var string
155
             */
156 48
            'line_ending' => null,
157
158
            /*
159
             * The string used for indentation.
160
             *
161
             * @var string
162
             */
163 48
            'indentation' => null,
164
165
            /*
166
             * Whether comments should be removed or not.
167
             *
168
             * @var bool
169
             */
170 48
            'remove_comments' => false,
171
172
            /*
173
             * Whether each clause should be on a new line.
174
             *
175
             * @var bool
176
             */
177 48
            'clause_newline' => true,
178
179
            /*
180
             * Whether each part should be on a new line.
181
             * Parts are delimited by brackets and commas.
182
             *
183
             * @var bool
184
             */
185 48
            'parts_newline' => true,
186
187
            /*
188
             * Whether each part of each clause should be indented.
189
             *
190
             * @var bool
191
             */
192 48
            'indent_parts' => true,
193 48
        ];
194
    }
195
196
    /**
197
     * The styles used for HTML formatting.
198
     * [$type, $flags, $span, $callback].
199
     *
200
     * @return array<int, array<string, int|string>>
201
     * @psalm-return list<array{type: int, flags: int, html: string, cli: string, function: string}>
202
     */
203 48
    protected function getDefaultFormats(): array
204
    {
205 48
        return [
206 48
            [
207 48
                'type' => TokenType::Keyword->value,
208 48
                'flags' => Token::FLAG_KEYWORD_RESERVED,
209 48
                'html' => 'class="sql-reserved"',
210 48
                'cli' => "\x1b[35m",
211 48
                'function' => 'strtoupper',
212 48
            ],
213 48
            [
214 48
                'type' => TokenType::Keyword->value,
215 48
                'flags' => 0,
216 48
                'html' => 'class="sql-keyword"',
217 48
                'cli' => "\x1b[95m",
218 48
                'function' => 'strtoupper',
219 48
            ],
220 48
            [
221 48
                'type' => TokenType::Comment->value,
222 48
                'flags' => 0,
223 48
                'html' => 'class="sql-comment"',
224 48
                'cli' => "\x1b[37m",
225 48
                'function' => '',
226 48
            ],
227 48
            [
228 48
                'type' => TokenType::Bool->value,
229 48
                'flags' => 0,
230 48
                'html' => 'class="sql-atom"',
231 48
                'cli' => "\x1b[36m",
232 48
                'function' => 'strtoupper',
233 48
            ],
234 48
            [
235 48
                'type' => TokenType::Number->value,
236 48
                'flags' => 0,
237 48
                'html' => 'class="sql-number"',
238 48
                'cli' => "\x1b[92m",
239 48
                'function' => 'strtolower',
240 48
            ],
241 48
            [
242 48
                'type' => TokenType::String->value,
243 48
                'flags' => 0,
244 48
                'html' => 'class="sql-string"',
245 48
                'cli' => "\x1b[91m",
246 48
                'function' => '',
247 48
            ],
248 48
            [
249 48
                'type' => TokenType::Symbol->value,
250 48
                'flags' => Token::FLAG_SYMBOL_PARAMETER,
251 48
                'html' => 'class="sql-parameter"',
252 48
                'cli' => "\x1b[31m",
253 48
                'function' => '',
254 48
            ],
255 48
            [
256 48
                'type' => TokenType::Symbol->value,
257 48
                'flags' => 0,
258 48
                'html' => 'class="sql-variable"',
259 48
                'cli' => "\x1b[36m",
260 48
                'function' => '',
261 48
            ],
262 48
        ];
263
    }
264
265
    /**
266
     * @param array<int, array<string, int|string>> $formats
267
     * @param array<int, array<string, int|string>> $newFormats
268
     *
269
     * @return array<int, array<string, int|string>>
270
     */
271 8
    private static function mergeFormats(array $formats, array $newFormats): array
272
    {
273 8
        $added = [];
274 8
        $integers = [
275 8
            'flags',
276 8
            'type',
277 8
        ];
278 8
        $strings = [
279 8
            'html',
280 8
            'cli',
281 8
            'function',
282 8
        ];
283
284
        /* Sanitize the array so that we do not have to care later */
285 8
        foreach ($newFormats as $j => $new) {
286 8
            foreach ($integers as $name) {
287 8
                if (isset($new[$name])) {
288 6
                    continue;
289
                }
290
291 6
                $newFormats[$j][$name] = 0;
292
            }
293
294 8
            foreach ($strings as $name) {
295 8
                if (isset($new[$name])) {
296 6
                    continue;
297
                }
298
299 8
                $newFormats[$j][$name] = '';
300
            }
301
        }
302
303
        /* Process changes to existing formats */
304 8
        foreach ($formats as $i => $original) {
305 8
            foreach ($newFormats as $j => $new) {
306 8
                if ($new['type'] !== $original['type'] || $original['flags'] !== $new['flags']) {
307 6
                    continue;
308
                }
309
310 6
                $formats[$i] = $new;
311 6
                $added[] = $j;
312
            }
313
        }
314
315
        /* Add not already handled formats */
316 8
        foreach ($newFormats as $j => $new) {
317 8
            if (in_array($j, $added)) {
318 6
                continue;
319
            }
320
321 2
            $formats[] = $new;
322
        }
323
324 8
        return $formats;
325
    }
326
327
    /**
328
     * Formats the given list of tokens.
329
     *
330
     * @param TokensList $list the list of tokens
331
     */
332 48
    public function formatList(TokensList $list): string
333
    {
334
        /**
335
         * The query to be returned.
336
         */
337 48
        $ret = '';
338
339
        /**
340
         * The indentation level.
341
         */
342 48
        $indent = 0;
343
344
        /**
345
         * Whether the line ended.
346
         */
347 48
        $lineEnded = false;
348
349
        /**
350
         * Whether current group is short (no linebreaks).
351
         */
352 48
        $shortGroup = false;
353
354
        /**
355
         * The name of the last clause.
356
         */
357 48
        $lastClause = '';
358
359
        /**
360
         * A stack that keeps track of the indentation level every time a new
361
         * block is found.
362
         */
363 48
        $blocksIndentation = [];
364
365
        /**
366
         * A stack that keeps track of the line endings every time a new block
367
         * is found.
368
         */
369 48
        $blocksLineEndings = [];
370
371
        /**
372
         * Whether clause's options were formatted.
373
         */
374 48
        $formattedOptions = false;
375
376
        /**
377
         * Previously parsed token.
378
         */
379 48
        $prev = null;
380
381
        // In order to be able to format the queries correctly, the next token
382
        // must be taken into consideration. The loop below uses two pointers,
383
        // `$prev` and `$curr` which store two consecutive tokens.
384
        // Actually, at every iteration the previous token is being used.
385 48
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
386
            /**
387
             * Token parsed at this moment.
388
             */
389 48
            $curr = $list->tokens[$list->idx];
390 48
            if ($list->idx + 1 < $list->count) {
391 46
                $next = $list->tokens[$list->idx + 1];
392
            } else {
393 48
                $next = null;
394
            }
395
396 48
            if ($curr->type === TokenType::Whitespace) {
397
                // Keep linebreaks before and after comments
398
                if (
399 46
                    str_contains($curr->token, "\n") && (
400 46
                        ($prev !== null && $prev->type === TokenType::Comment) ||
401 46
                        ($next !== null && $next->type === TokenType::Comment)
402
                    )
403
                ) {
404 2
                    $lineEnded = true;
405
                }
406
407
                // Whitespaces are skipped because the formatter adds its own.
408 46
                continue;
409
            }
410
411 48
            if ($curr->type === TokenType::Comment && $this->options['remove_comments']) {
412
                // Skip Comments if option `remove_comments` is enabled
413 2
                continue;
414
            }
415
416
            // Checking if pointers were initialized.
417 48
            if ($prev !== null) {
418
                // Checking if a new clause started.
419 46
                if (static::isClause($prev) !== false) {
0 ignored issues
show
Bug introduced by
$prev of type null is incompatible with the type PhpMyAdmin\SqlParser\Token expected by parameter $token of PhpMyAdmin\SqlParser\Utils\Formatter::isClause(). ( Ignorable by Annotation )

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

419
                if (static::isClause(/** @scrutinizer ignore-type */ $prev) !== false) {
Loading history...
420 46
                    $lastClause = $prev->value;
421 46
                    $formattedOptions = false;
422
                }
423
424
                // The options of a clause should stay on the same line and everything that follows.
425
                if (
426 46
                    $this->options['parts_newline']
427 46
                    && ! $formattedOptions
428 46
                    && empty(self::$inlineClauses[$lastClause])
429
                    && (
430 46
                        $curr->type !== TokenType::Keyword
431 46
                        || ($curr->flags & Token::FLAG_KEYWORD_FUNCTION)
432
                    )
433
                ) {
434 42
                    $formattedOptions = true;
435 42
                    $lineEnded = true;
436 42
                    ++$indent;
437
                }
438
439
                // Checking if this clause ended.
440 46
                $isClause = static::isClause($curr);
441
442 46
                if ($isClause !== false) {
443
                    if (
444 20
                        ($isClause === 2 || $this->options['clause_newline'])
445 20
                        && empty(self::$shortClauses[$lastClause])
446
                    ) {
447 20
                        $lineEnded = true;
448 20
                        if ($this->options['parts_newline'] && $indent > 0) {
449 18
                            --$indent;
450
                        }
451
                    }
452
                }
453
454
                // Inline JOINs
455
                if (
456 46
                    ($prev->type === TokenType::Keyword && isset(JoinKeyword::JOINS[$prev->value]))
457 46
                    || (in_array($curr->value, ['ON', 'USING'], true)
458 46
                        && isset(JoinKeyword::JOINS[$list->tokens[$list->idx - 2]->value]))
459 46
                    || isset($list->tokens[$list->idx - 4], JoinKeyword::JOINS[$list->tokens[$list->idx - 4]->value])
460 46
                    || isset($list->tokens[$list->idx - 6], JoinKeyword::JOINS[$list->tokens[$list->idx - 6]->value])
461
                ) {
462 2
                    $lineEnded = false;
463
                }
464
465
                // Indenting BEGIN ... END blocks.
466 46
                if ($prev->type === TokenType::Keyword && $prev->keyword === 'BEGIN') {
467 2
                    $lineEnded = true;
468 2
                    $blocksIndentation[] = $indent;
469 2
                    ++$indent;
470 46
                } elseif ($curr->type === TokenType::Keyword && $curr->keyword === 'END') {
471 2
                    $lineEnded = true;
472 2
                    $indent = array_pop($blocksIndentation);
473
                }
474
475
                // Formatting fragments delimited by comma.
476 46
                if ($prev->type === TokenType::Operator && $prev->value === ',') {
477
                    // Fragments delimited by a comma are broken into multiple
478
                    // pieces only if the clause is not inlined or this fragment
479
                    // is between brackets that are on new line.
480
                    if (
481 8
                        end($blocksLineEndings) === true
482
                        || (
483 8
                            empty(self::$inlineClauses[$lastClause])
484 8
                            && ! $shortGroup
485 8
                            && $this->options['parts_newline']
486
                        )
487
                    ) {
488 6
                        $lineEnded = true;
489
                    }
490
                }
491
492
                // Handling brackets.
493
                // Brackets are indented only if the length of the fragment between
494
                // them is longer than 30 characters.
495 46
                if ($prev->type === TokenType::Operator && $prev->value === '(') {
496 12
                    $blocksIndentation[] = $indent;
497 12
                    $shortGroup = true;
498 12
                    if (static::getGroupLength($list) > 30) {
499 2
                        ++$indent;
500 2
                        $lineEnded = true;
501 2
                        $shortGroup = false;
502
                    }
503
504 12
                    $blocksLineEndings[] = $lineEnded;
505 46
                } elseif ($curr->type === TokenType::Operator && $curr->value === ')') {
506 10
                    $indent = array_pop($blocksIndentation);
507 10
                    $lineEnded |= array_pop($blocksLineEndings);
508 10
                    $shortGroup = false;
509
                }
510
511
                // Adding the token.
512 46
                $ret .= $this->toString($prev);
0 ignored issues
show
Bug introduced by
$prev of type null is incompatible with the type PhpMyAdmin\SqlParser\Token expected by parameter $token of PhpMyAdmin\SqlParser\Utils\Formatter::toString(). ( Ignorable by Annotation )

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

512
                $ret .= $this->toString(/** @scrutinizer ignore-type */ $prev);
Loading history...
513
514
                // Finishing the line.
515 46
                if ($lineEnded) {
516 44
                    $ret .= $this->options['line_ending'] . str_repeat($this->options['indentation'], (int) $indent);
517 44
                    $lineEnded = false;
518
                } elseif (
519 46
                    $prev->keyword === 'DELIMITER'
520 46
                    || ! (
521 46
                    ($prev->type === TokenType::Operator && ($prev->value === '.' || $prev->value === '('))
522 46
                    // No space after . (
523 46
                    || ($curr->type === TokenType::Operator
524 46
                        && ($curr->value === '.' || $curr->value === ','
525 46
                            || $curr->value === '(' || $curr->value === ')'))
526 46
                    // No space before . , ( )
527 46
                    || $curr->type === TokenType::Delimiter && mb_strlen((string) $curr->value, 'UTF-8') < 2
528 46
                    )
529
                ) {
530
                    // If the line ended, there is no point in adding whitespaces.
531
                    // Also, some tokens do not have spaces before or after them.
532
                    // A space after delimiters that are longer than 2 characters.
533 26
                    $ret .= ' ';
534
                }
535
            }
536
537
            // Iteration finished, consider current token as previous.
538 48
            $prev = $curr;
539
        }
540
541 48
        if ($this->options['type'] === 'cli') {
542 40
            return $ret . "\x1b[0m";
543
        }
544
545 42
        return $ret;
546
    }
547
548 38
    public function escapeConsole(string $string): string
549
    {
550 38
        return str_replace(
551 38
            [
552 38
                "\x00",
553 38
                "\x01",
554 38
                "\x02",
555 38
                "\x03",
556 38
                "\x04",
557 38
                "\x05",
558 38
                "\x06",
559 38
                "\x07",
560 38
                "\x08",
561 38
                "\x09",
562 38
                "\x0A",
563 38
                "\x0B",
564 38
                "\x0C",
565 38
                "\x0D",
566 38
                "\x0E",
567 38
                "\x0F",
568 38
                "\x10",
569 38
                "\x11",
570 38
                "\x12",
571 38
                "\x13",
572 38
                "\x14",
573 38
                "\x15",
574 38
                "\x16",
575 38
                "\x17",
576 38
                "\x18",
577 38
                "\x19",
578 38
                "\x1A",
579 38
                "\x1B",
580 38
                "\x1C",
581 38
                "\x1D",
582 38
                "\x1E",
583 38
                "\x1F",
584 38
            ],
585 38
            [
586 38
                '\x00',
587 38
                '\x01',
588 38
                '\x02',
589 38
                '\x03',
590 38
                '\x04',
591 38
                '\x05',
592 38
                '\x06',
593 38
                '\x07',
594 38
                '\x08',
595 38
                '\x09',
596 38
                '\x0A',
597 38
                '\x0B',
598 38
                '\x0C',
599 38
                '\x0D',
600 38
                '\x0E',
601 38
                '\x0F',
602 38
                '\x10',
603 38
                '\x11',
604 38
                '\x12',
605 38
                '\x13',
606 38
                '\x14',
607 38
                '\x15',
608 38
                '\x16',
609 38
                '\x17',
610 38
                '\x18',
611 38
                '\x19',
612 38
                '\x1A',
613 38
                '\x1B',
614 38
                '\x1C',
615 38
                '\x1D',
616 38
                '\x1E',
617 38
                '\x1F',
618 38
            ],
619 38
            $string,
620 38
        );
621
    }
622
623
    /**
624
     * Tries to print the query and returns the result.
625
     *
626
     * @param Token $token the token to be printed
627
     */
628 46
    public function toString(Token $token): string
629
    {
630 46
        $text = $token->token;
631 46
        static $prev;
632
633 46
        foreach ($this->options['formats'] as $format) {
634
            if (
635 46
                $token->type->value !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags'])
636
            ) {
637 46
                continue;
638
            }
639
640
            // Running transformation function.
641 46
            if (! empty($format['function'])) {
642 46
                $func = $format['function'];
643 46
                $text = $func($text);
644
            }
645
646
            // Formatting HTML.
647 46
            if ($this->options['type'] === 'html') {
648 36
                return '<span ' . $format['html'] . '>' . htmlspecialchars($text, ENT_NOQUOTES) . '</span>';
649
            }
650
651 42
            if ($this->options['type'] === 'cli') {
652 38
                if ($prev !== $format['cli']) {
653 38
                    $prev = $format['cli'];
654
655 38
                    return $format['cli'] . $this->escapeConsole($text);
656
                }
657
658 10
                return $this->escapeConsole($text);
659
            }
660
661 36
            break;
662
        }
663
664 36
        if ($this->options['type'] === 'cli') {
665 28
            if ($prev !== "\x1b[39m") {
666 28
                $prev = "\x1b[39m";
667
668 28
                return "\x1b[39m" . $this->escapeConsole($text);
669
            }
670
671 16
            return $this->escapeConsole($text);
672
        }
673
674 36
        if ($this->options['type'] === 'html') {
675 28
            return htmlspecialchars($text, ENT_NOQUOTES);
676
        }
677
678 36
        return $text;
679
    }
680
681
    /**
682
     * Formats a query.
683
     *
684
     * @param string                                                           $query   The query to be formatted
685
     * @param array<string, bool|string|array<int, array<string, int|string>>> $options the formatting options
686
     *
687
     * @return string the formatted string
688
     */
689 48
    public static function format(string $query, array $options = []): string
690
    {
691 48
        $lexer = new Lexer($query);
692 48
        $formatter = new self($options);
693
694 48
        return $formatter->formatList($lexer->list);
695
    }
696
697
    /**
698
     * Computes the length of a group.
699
     *
700
     * A group is delimited by a pair of brackets.
701
     *
702
     * @param TokensList $list the list of tokens
703
     */
704 12
    public static function getGroupLength(TokensList $list): int
705
    {
706
        /**
707
         * The number of opening brackets found.
708
         * This counter starts at one because by the time this function called,
709
         * the list already advanced one position and the opening bracket was
710
         * already parsed.
711
         */
712 12
        $count = 1;
713
714
        /**
715
         * The length of this group.
716
         */
717 12
        $length = 0;
718
719 12
        for ($idx = $list->idx; $idx < $list->count; ++$idx) {
720
            // Counting the brackets.
721 12
            if ($list->tokens[$idx]->type === TokenType::Operator) {
722 12
                if ($list->tokens[$idx]->value === '(') {
723 2
                    ++$count;
724 12
                } elseif ($list->tokens[$idx]->value === ')') {
725 12
                    --$count;
726 12
                    if ($count === 0) {
727 12
                        break;
728
                    }
729
                }
730
            }
731
732
            // Keeping track of this group's length.
733 10
            $length += mb_strlen((string) $list->tokens[$idx]->value, 'UTF-8');
734
        }
735
736 12
        return $length;
737
    }
738
739
    /**
740
     * Checks if a token is a statement or a clause inside a statement.
741
     *
742
     * @param Token $token the token to be checked
743
     *
744
     * @psalm-return 1|2|false
745
     */
746 46
    public static function isClause(Token $token): int|false
747
    {
748
        if (
749 46
            ($token->type === TokenType::Keyword && isset(Parser::STATEMENT_PARSERS[$token->keyword]))
750 46
            || ($token->type === TokenType::None && strtoupper($token->token) === 'DELIMITER')
751
        ) {
752 44
            return 2;
753
        }
754
755
        if (
756 46
            $token->type === TokenType::Keyword && (
757 46
            in_array($token->keyword, self::FORMATTERS, true) || isset(Parser::KEYWORD_PARSERS[$token->keyword])
758
            )
759
        ) {
760 20
            return 1;
761
        }
762
763 46
        return false;
764
    }
765
}
766