snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
269
internal/sql/ast/print.go
Normal file
269
internal/sql/ast/print.go
Normal file
@@ -0,0 +1,269 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user