Files
PgTidy/pkg/format/body.go
T
warkanum 6492ab35b7 feat(format): implement PL/pgSQL body formatting
* add formatBody and formatBodyInner functions for DECLARE section
* update needSpace to handle LBracket correctly
* enhance semanticallyEqual to compare dollar-quoted bodies
* add test data for broken layout scenarios
2026-06-23 21:15:27 +02:00

183 lines
4.2 KiB
Go

package format
import (
"strings"
"github.com/hein/pgtidy/pkg/config"
"github.com/hein/pgtidy/pkg/cst"
"github.com/hein/pgtidy/pkg/lexer"
)
// formatBody applies house-style formatting to a PL/pgSQL dollar-quoted body
// token. Currently only the DECLARE section is formatted; the rest is verbatim.
func formatBody(bodyText string, st config.Style) string {
open, inner, close, ok := splitDollarQuote(bodyText)
if !ok {
return bodyText
}
return open + formatBodyInner(inner, st) + close
}
// splitDollarQuote splits a dollar-quoted token (e.g. "$$...\n$$" or
// "$S$...$S$") into (open tag, inner text, close tag). The open and close tags
// are the same string; the last occurrence in s is taken as the close tag.
func splitDollarQuote(s string) (open, inner, close string, ok bool) {
if len(s) == 0 || s[0] != '$' {
return
}
end := strings.Index(s[1:], "$")
if end < 0 {
return
}
openLen := end + 2
open = s[:openLen]
closeStart := strings.LastIndex(s, open)
if closeStart < openLen {
return
}
inner = s[openLen:closeStart]
close = s[closeStart:]
ok = true
return
}
func formatBodyInner(inner string, st config.Style) string {
sig, _ := cst.Attach(lexer.Lex(inner))
nl := st.Newline
// Find DECLARE at depth 0.
declareIdx := -1
for i, t := range sig {
if t.Tok.Kind == lexer.Ident && lowerASCII(t.Tok.Text) == "declare" {
declareIdx = i
break
}
}
if declareIdx < 0 {
return inner
}
// Find BEGIN at depth 0 after DECLARE.
beginIdx := -1
depth := 0
for i := declareIdx + 1; i < len(sig); i++ {
switch sig[i].Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
case lexer.RParen, lexer.RBracket:
if depth > 0 {
depth--
}
}
if depth == 0 && sig[i].Tok.Kind == lexer.Ident && lowerASCII(sig[i].Tok.Text) == "begin" {
beginIdx = i
break
}
}
if beginIdx < 0 {
return inner
}
var b strings.Builder
// Emit verbatim up to and including DECLARE (keyword-cased).
for i := 0; i <= declareIdx; i++ {
t := sig[i]
for _, tr := range t.Lead {
b.WriteString(tr.Text)
}
if i == declareIdx {
b.WriteString(applyCase(t.Tok.Text, st.KeywordCase))
} else {
b.WriteString(t.Tok.Text)
}
}
b.WriteString(nl)
// Format each variable declaration in the DECLARE section.
formatDeclareVars(&b, sig[declareIdx+1:beginIdx], st)
// Emit BEGIN and everything after it verbatim from the original source.
// The newline before BEGIN is supplied by the last declaration's line end.
b.WriteString(inner[sig[beginIdx].Tok.Off:])
return b.String()
}
// formatDeclareVars writes each variable declaration as a single indented line.
// Comments in the leading trivia of a declaration's first token are preserved
// on their own lines. If a declaration carries mid-body comments it is emitted
// verbatim to avoid losing them.
func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
nl := st.Newline
indent := st.Indent
depth := 0
var cur []cst.Tok
var preComments []string
emit := func() {
if len(cur) == 0 {
return
}
for _, c := range preComments {
b.WriteString(indent)
b.WriteString(c)
b.WriteString(nl)
}
preComments = nil
// Graceful degradation: mid-declaration comments stay verbatim.
if anyComment(cur[1:]) {
b.WriteString(indent)
b.WriteString(verbatimSpan(cur))
b.WriteString(nl)
cur = nil
return
}
body := cur
hasSemi := len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon
if hasSemi {
body = body[:len(body)-1]
}
b.WriteString(indent)
for i, t := range body {
if i > 0 && needSpace(body[i-1].Tok, t.Tok) {
b.WriteByte(' ')
}
b.WriteString(caseText(t.Tok, st))
}
if hasSemi {
b.WriteString(";")
}
b.WriteString(nl)
cur = nil
}
for _, t := range toks {
if len(cur) == 0 {
for _, tr := range t.Lead {
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
preComments = append(preComments, strings.TrimRight(tr.Text, " \t"))
}
}
}
switch t.Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
case lexer.RParen, lexer.RBracket:
if depth > 0 {
depth--
}
}
cur = append(cur, t)
if t.Tok.Kind == lexer.Semicolon && depth == 0 {
emit()
}
}
emit()
}