Files
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

251 lines
7.2 KiB
Go

// Package printer implements the document renderer the SQL formatter
// prints through: a small Wadler-style engine, the model behind Prettier
// and ruff (via Biome's printer). Callers emit a stream of tokens — literal
// text, break opportunities, and markers that open and close groups and
// indented regions — and Print lays each group out on a single line when
// its flat width fits, breaking it otherwise. The package knows nothing
// about SQL or the AST; the ast package's TrackedBuffer embeds a Buffer and
// layers node formatting and comment emission on top.
package printer
import (
"strings"
"unicode/utf8"
)
type tokenKind uint8
const (
tokenText tokenKind = iota
// tokenLine renders as a single space when its group fits on one line
// and as a line break when the group is broken.
tokenLine
// tokenSoftline renders as nothing when its group fits on one line and
// as a line break when the group is broken.
tokenSoftline
// tokenHardline always renders a line break and forces every group
// containing it to break (it measures as infinitely wide). Emitted
// around comments, which pin the author's layout open.
tokenHardline
// tokenBreaker renders nothing but forces every group containing it to
// break. Emitted after a trailing line comment, whose text swallows the
// rest of the line, and at boundaries where the author broke the line.
tokenBreaker
tokenOpenGroup
tokenCloseGroup
tokenOpenIndent
tokenCloseIndent
)
// breakWidth is the measured width of tokens that force a break: wider than
// any real line, so no group containing one ever fits.
const breakWidth = 1 << 24
// indentWidth is the number of spaces per indentation level in broken
// (multi-line) output.
const indentWidth = 2
type token struct {
kind tokenKind
text string
}
// Buffer accumulates the document token stream.
type Buffer struct {
tokens []token
}
func (t *Buffer) WriteString(s string) (int, error) {
if s == "" {
return 0, nil
}
t.tokens = append(t.tokens, token{kind: tokenText, text: s})
return len(s), nil
}
// Write implements io.Writer so callers can use fmt.Fprintf.
func (t *Buffer) Write(p []byte) (int, error) {
return t.WriteString(string(p))
}
func (t *Buffer) WriteRune(r rune) (int, error) {
return t.WriteString(string(r))
}
// String renders the buffer on a single line.
func (t *Buffer) String() string {
return t.Print(-1)
}
// Line marks a break opportunity between two tokens: a space when the
// surrounding group fits on one line, a line break when it does not.
func (t *Buffer) Line() {
t.tokens = append(t.tokens, token{kind: tokenLine})
}
// Softline marks a break opportunity that disappears entirely when the
// surrounding group fits on one line.
func (t *Buffer) Softline() {
t.tokens = append(t.tokens, token{kind: tokenSoftline})
}
// Hardline always renders a line break and forces every enclosing group to
// break.
func (t *Buffer) Hardline() {
t.tokens = append(t.tokens, token{kind: tokenHardline})
}
// Breaker renders nothing but forces every enclosing group to break, so the
// next break opportunity becomes a real line break.
func (t *Buffer) Breaker() {
t.tokens = append(t.tokens, token{kind: tokenBreaker})
}
// Group opens a region the renderer tries to lay out on a single line,
// breaking it only when its flat form does not fit. Must be paired with
// EndGroup.
func (t *Buffer) Group() {
t.tokens = append(t.tokens, token{kind: tokenOpenGroup})
}
func (t *Buffer) EndGroup() {
t.tokens = append(t.tokens, token{kind: tokenCloseGroup})
}
// Indent opens a region printed one indentation level deeper when line
// breaks occur inside it. Must be paired with EndIndent.
func (t *Buffer) Indent() {
t.tokens = append(t.tokens, token{kind: tokenOpenIndent})
}
func (t *Buffer) EndIndent() {
t.tokens = append(t.tokens, token{kind: tokenCloseIndent})
}
// docNode is the tree form of the token stream: text and break tokens are
// leaves, groups and indented regions are interior nodes.
type docNode struct {
kind tokenKind
text string
kids []*docNode
width int // flat width in runes, filled in by measure
}
// tree assembles the flat token stream into a document tree. Stray close
// markers are ignored and unclosed regions end at the end of the stream.
func (t *Buffer) tree() *docNode {
root := &docNode{kind: tokenOpenGroup}
stack := []*docNode{root}
for _, tok := range t.tokens {
top := stack[len(stack)-1]
switch tok.kind {
case tokenText, tokenLine, tokenSoftline, tokenHardline, tokenBreaker:
top.kids = append(top.kids, &docNode{kind: tok.kind, text: tok.text})
case tokenOpenGroup, tokenOpenIndent:
n := &docNode{kind: tok.kind}
top.kids = append(top.kids, n)
stack = append(stack, n)
case tokenCloseGroup, tokenCloseIndent:
if len(stack) > 1 {
stack = stack[:len(stack)-1]
}
}
}
return root
}
// measure computes the width of every node when rendered flat. Hardlines
// and breakers measure wider than any real line, so no group containing one
// ever fits — Prettier's breakParent, by arithmetic.
func measure(d *docNode) int {
switch d.kind {
case tokenText:
d.width = utf8.RuneCountInString(d.text)
case tokenLine:
d.width = 1
case tokenSoftline:
d.width = 0
case tokenHardline, tokenBreaker:
d.width = breakWidth
default:
for _, kid := range d.kids {
d.width += measure(kid)
if d.width > breakWidth {
d.width = breakWidth
}
}
}
return d.width
}
// Print renders the token stream. A negative width renders everything on
// one line; otherwise each group is rendered flat only when it fits in the
// space remaining on its line — and a group holding a hardline or breaker
// never fits, whatever the width, so forced breaks survive even an
// unlimited line length.
func (t *Buffer) Print(width int) string {
root := t.tree()
measure(root)
type frame struct {
d *docNode
indent int
flat bool
}
var sb strings.Builder
col := 0
// atLineStart dedupes consecutive breaks: a break that fires when the
// output is already at the start of a fresh line writes nothing, so a
// comment's surrounding hardlines collapse against clause breaks
// instead of leaving blank lines.
atLineStart := true
newline := func(indent int) {
if atLineStart {
return
}
sb.WriteByte('\n')
for i := 0; i < indent; i++ {
sb.WriteByte(' ')
}
col = indent
atLineStart = true
}
stack := []frame{{d: root, flat: width < 0}}
for len(stack) > 0 {
f := stack[len(stack)-1]
stack = stack[:len(stack)-1]
switch f.d.kind {
case tokenText:
sb.WriteString(f.d.text)
col += f.d.width
atLineStart = false
case tokenLine:
if f.flat {
sb.WriteByte(' ')
col++
atLineStart = false
} else {
newline(f.indent)
}
case tokenSoftline:
if !f.flat {
newline(f.indent)
}
case tokenHardline:
newline(f.indent)
case tokenBreaker:
// forces enclosing groups broken; renders nothing
case tokenOpenGroup:
flat := f.flat || (f.d.width < breakWidth && f.d.width <= width-col)
for i := len(f.d.kids) - 1; i >= 0; i-- {
stack = append(stack, frame{d: f.d.kids[i], indent: f.indent, flat: flat})
}
case tokenOpenIndent:
for i := len(f.d.kids) - 1; i >= 0; i-- {
stack = append(stack, frame{d: f.d.kids[i], indent: f.indent + indentWidth, flat: f.flat})
}
}
}
return sb.String()
}