packages/sql-formatter/src/Core/Compact/Document.php

1<?php
2
3declare(strict_types=1);
4
5namespace SqlFormatter\Core\Compact;
6
7use SqlParser\Lexer\Token;
8use SqlParser\Parser\Node;
9
10/**
11 * Builds a canonical token stream without changing the parser's immutable tree.
12 *
13 * @visibility SqlFormatter
14 */
15final class Document
16{
17    /**
18     * @var list<Token> Canonical tokens in source order
19     */
20    public readonly array $tokens;
21    /**
22     * Retained directives after the last SQL token.
23     */
24    public readonly string $trailing;
25    /**
26     * @var list<list<int>> Grammar-owned reduction candidates
27     */
28    public readonly array $candidates;
29    private readonly ?string $shape;
30
31    /**
32     * Preserves directive ownership before applying grammar-specific reductions.
33     */
34    public function __construct(Node $tree, Settings $settings)
35    {
36        $trivia = new Trivia($settings->nesting);
37        $cleaned = [];
38        $protected = [];
39        $verbatim = [];
40        $trailing = '';
41        foreach ($tree->tokens() as $token) {
42            $leading = $trivia->clean($token->leading);
43            if ($trivia->executable) {
44                $verbatim[$token->offset] = true;
45            }
46            if ($trivia->executable || $leading !== '') {
47                $protected[$token->offset] = true;
48            }
49            if ($token->text === '') {
50                $trailing .= $leading;
51            }
52            $cleaned[$token->offset] = new Token($token->symbol, $token->name, $token->text, $token->offset, $leading);
53        }
54        $this->trailing = $trailing . $trivia->clean($tree->trailing);
55        $tokens = (new Visitor($cleaned, $protected, $verbatim, $settings))->tokens($tree);
56        $last = $tokens[count($tokens) - 1] ?? null;
57        while ($last?->text === ';' && $this->trailing === '' && !isset($protected[$last->offset])) {
58            array_pop($tokens);
59            $last = $tokens[count($tokens) - 1] ?? null;
60        }
61        $this->tokens = $tokens;
62        $this->candidates = (new Reductions($settings->grouping, $settings->aliases))->candidates($tree, $protected);
63        $grouping = array_fill_keys(array_merge([], ...$this->candidates), true);
64        $this->shape = (new Shape(array_column($tokens, null, 'offset'), $grouping))->of($tree);
65    }
66
67    /**
68     * Verifies canonical terminal kinds, spellings, and retained directives.
69     */
70    public function signature(): string
71    {
72        return serialize([$this->shape, $this->trailing]);
73    }
74}
75