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

270 lines
7.5 KiB
Go

package ast
import (
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/sql/ast/printer"
"github.com/sqlc-dev/sqlc/internal/sql/format"
)
type nodeFormatter interface {
Format(*TrackedBuffer, format.Dialect)
}
// TrackedBuffer layers AST formatting over the document renderer in
// ast/printer: node dispatch, list joins, and comment emission. The
// renderer itself — tokens, groups, width fitting — lives in the printer
// package; TrackedBuffer stays here because every node's Format method
// names it, which an ast → printer → ast import cycle would forbid.
type TrackedBuffer struct {
*printer.Buffer
// ct, when set, carries the statement's interior comments attached to
// their anchor nodes; the buffer emits each comment when it reaches the
// comment's anchor. Positions were consulted once, in AttachComments —
// never here — so edited and synthetic trees print comments correctly.
ct *CommentTable
// anchors, when set, records every positioned node and emission point
// in print order instead of producing output; AttachComments uses this
// dry run to classify comments against the same order printing will
// use.
anchors *[]anchor
}
// NewTrackedBuffer creates a new TrackedBuffer.
func NewTrackedBuffer() *TrackedBuffer {
return &TrackedBuffer{Buffer: &printer.Buffer{}}
}
func (t *TrackedBuffer) astFormat(n Node, d format.Dialect) {
if t.anchors != nil && n != nil && n.Pos() > 0 {
*t.anchors = append(*t.anchors, anchor{node: n, pos: n.Pos()})
}
if t.ct != nil && n != nil {
t.emitComments(t.ct.take(n))
}
if ft, ok := n.(nodeFormatter); ok {
ft.Format(t, d)
} else {
debug.Dump(n)
}
}
// emitComments prints attached comments at the boundary before their anchor
// node. A trailing comment continues the line before it (a line comment
// then forces the enclosing groups to break); an own-line or line comment
// goes on its own line; an inline block comment stays in the flow.
func (t *TrackedBuffer) emitComments(recs []commentRec) {
for _, rec := range recs {
switch {
case rec.trailing:
t.WriteString(" ")
t.WriteString(rec.c.Text)
if rec.c.Line() {
t.Breaker()
}
case rec.c.OwnLine || rec.c.Line():
t.Hardline()
t.WriteString(rec.c.Text)
t.Hardline()
default:
t.WriteString(rec.c.Text)
t.WriteString(" ")
}
}
}
// beforeClause is the emission point for comments that sit between the
// previous clause and the one about to be printed, so they land before the
// clause keyword. Call it before the clause's line break. During the
// AttachComments dry run it records itself as a boundary marker, which is
// how classification and emission are guaranteed to pick the same spot —
// and how the author's own line break before the clause is observed and
// kept.
func (t *TrackedBuffer) beforeClause(n Node, d format.Dialect) {
t.boundary(n)
}
// boundary is a separator-adjacent emission point (before a clause, after a
// comma, before an AND/OR): comments classified to the upcoming node print
// here, before the separator's line break, and a line break the author
// wrote at this boundary forces the break to stay. On the dry run it
// records a marker instead, so classification and emission agree by
// construction.
func (t *TrackedBuffer) boundary(next Node) {
if next == nil {
return
}
if t.anchors != nil {
*t.anchors = append(*t.anchors, anchor{node: next, marker: true})
return
}
if t.ct != nil {
t.emitComments(t.ct.take(next))
if t.ct.breakAt(next) {
t.Breaker()
}
}
}
// flushRemaining prints every comment not yet printed; the statement is
// over, so everything left trails it.
func (t *TrackedBuffer) flushRemaining() {
if t.ct == nil {
return
}
for _, rec := range t.ct.takeRemaining() {
if rec.trailing {
t.WriteString(" ")
t.WriteString(rec.c.Text)
if rec.c.Line() {
t.Breaker()
}
} else {
t.Hardline()
t.WriteString(rec.c.Text)
}
}
}
func (t *TrackedBuffer) join(n *List, d format.Dialect, sep string) {
if n == nil {
return
}
first := true
for _, item := range n.Items {
if _, ok := item.(*TODO); ok {
continue
}
if !first {
t.WriteString(sep)
}
first = false
t.astFormat(item, d)
}
}
// joinComma writes the list items separated by a comma and a break
// opportunity, so a list that fits stays on one line and a list that does
// not puts every item on its own line. An item's trailing comment prints
// after the comma, gofmt-style: `total, -- computed`.
func (t *TrackedBuffer) joinComma(n *List, d format.Dialect) {
if n == nil {
return
}
items := make([]Node, 0, len(n.Items))
for _, item := range n.Items {
if _, ok := item.(*TODO); ok {
continue
}
items = append(items, item)
}
for i, item := range items {
if i > 0 {
t.WriteString(",")
t.boundary(item)
t.Line()
}
t.astFormat(item, d)
}
}
// condition formats a clause-level boolean condition (WHERE, HAVING, ON).
// A top-level AND/OR chain is printed without the enclosing parentheses a
// nested BoolExpr gets — the clause keyword already delimits it — and each
// branch goes on its own line when the chain does not fit:
//
// WHERE name LIKE $1
// AND id > $2
func (t *TrackedBuffer) condition(n Node, d format.Dialect) {
be, ok := n.(*BoolExpr)
if !ok || (be.Boolop != BoolExprTypeAnd && be.Boolop != BoolExprTypeOr) {
t.astFormat(n, d)
return
}
op := "AND "
if be.Boolop == BoolExprTypeOr {
op = "OR "
}
t.Group()
t.Indent()
first := true
t.conditionArgs(be, d, op, &first)
t.EndIndent()
t.EndGroup()
}
// conditionArgs writes the branches of an AND/OR chain, flattening nested
// chains of the same operator (some parsers produce `a AND b AND c` as a
// left-nested tree) so no redundant parentheses appear.
func (t *TrackedBuffer) conditionArgs(be *BoolExpr, d format.Dialect, op string, first *bool) {
if be.Args == nil {
return
}
for _, item := range be.Args.Items {
if _, ok := item.(*TODO); ok {
continue
}
if nested, ok := item.(*BoolExpr); ok && nested.Boolop == be.Boolop {
t.conditionArgs(nested, d, op, first)
continue
}
if !*first {
t.boundary(item)
t.Line()
t.WriteString(op)
}
*first = false
t.astFormat(item, d)
}
}
// Format renders the node as SQL on a single line.
func Format(n Node, d format.Dialect) string {
return Pretty(n, d, -1)
}
// Pretty renders the node as SQL, breaking lines so the output fits within
// width columns where the statement's structure allows it. A negative width
// renders everything on a single line.
func Pretty(n Node, d format.Dialect, width int) string {
return PrettyWithComments(n, d, width, nil)
}
// PrettyWithComments renders the node as SQL like Pretty, emitting the
// comments a prior AttachComments call anchored to its nodes and keeping
// the line breaks the author wrote at the boundaries the printer models.
// Each comment prints at the boundary before its anchor, a trailing comment
// continues the line of the code it followed, and every comment in the
// table is printed — anything left when the statement ends trails it.
func PrettyWithComments(n Node, d format.Dialect, width int, ct *CommentTable) string {
tb := NewTrackedBuffer()
tb.ct = ct
if ft, ok := n.(nodeFormatter); ok {
ft.Format(tb, d)
}
return tb.Print(width)
}
func set(n Node) bool {
if n == nil {
return false
}
_, ok := n.(*TODO)
return !ok
}
func items(n *List) bool {
if n == nil {
return false
}
return len(n.Items) > 0
}
func todo(n *List) bool {
for _, item := range n.Items {
if _, ok := item.(*TODO); !ok {
return false
}
}
return true
}