chore: more work done and planning with AI.
CI / Test (push) Failing after 25s
CI / Build (push) Has been skipped

This commit is contained in:
2026-06-30 22:43:25 +02:00
parent a58b081cae
commit c8030247f2
15 changed files with 4948 additions and 157 deletions
+268 -53
View File
@@ -92,10 +92,11 @@ func formatBodyInner(inner string, st config.Style) string {
var b strings.Builder
// Emit verbatim up to and including DECLARE (keyword-cased).
// Normalize CRLF in trivia so the output always uses st.Newline.
for i := 0; i <= declareIdx; i++ {
t := sig[i]
for _, tr := range t.Lead {
b.WriteString(tr.Text)
b.WriteString(strings.ReplaceAll(tr.Text, "\r\n", nl))
}
if i == declareIdx {
b.WriteString(applyCase(t.Tok.Text, st.KeywordCase))
@@ -122,57 +123,30 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
nl := st.Newline
indent := st.Indent
depth := 0
var decls [][]cst.Tok
var cur []cst.Tok
var preComments []string
var preCommentSets [][]string
var curPreComments []string
emit := func() {
collect := 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)
decls = append(decls, cur)
preCommentSets = append(preCommentSets, curPreComments)
cur = nil
curPreComments = 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"))
text := strings.TrimRight(strings.ReplaceAll(tr.Text, "\r", ""), " \t")
curPreComments = append(curPreComments, text)
}
}
}
switch t.Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
@@ -181,14 +155,195 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
depth--
}
}
cur = append(cur, t)
if t.Tok.Kind == lexer.Semicolon && depth == 0 {
emit()
collect()
}
}
collect()
// Compute alignment widths when requested.
var nameColW, typeColW int
if st.PlpgsqlDeclareAlignType || st.PlpgsqlDeclareAlignEq {
for _, decl := range decls {
if anyComment(decl[1:]) {
continue
}
body := decl
if len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon {
body = body[:len(body)-1]
}
nw, tw := declareNameTypeWidth(body, st)
if nw > nameColW {
nameColW = nw
}
if tw > typeColW {
typeColW = tw
}
}
}
for i, cur := range decls {
for _, c := range preCommentSets[i] {
b.WriteString(indent)
b.WriteString(c)
b.WriteString(nl)
}
// Graceful degradation: mid-declaration comments stay verbatim.
if anyComment(cur[1:]) {
b.WriteString(indent)
b.WriteString(verbatimSpan(cur))
b.WriteString(nl)
continue
}
body := cur
hasSemi := len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon
if hasSemi {
body = body[:len(body)-1]
}
b.WriteString(indent)
if (st.PlpgsqlDeclareAlignType || st.PlpgsqlDeclareAlignEq) && nameColW > 0 {
writeDeclareAligned(b, body, st, nameColW, typeColW)
} else {
for j, t := range body {
if j > 0 && needSpace(body[j-1].Tok, t.Tok) {
b.WriteByte(' ')
}
b.WriteString(caseText(t.Tok, st))
}
}
if hasSemi {
b.WriteString(";")
}
b.WriteString(nl)
}
}
// declareNameTypeWidth returns the rendered width of the name and type portions
// of a DECLARE variable declaration (without the default assignment).
// Format is: [name type [:= default]] or [name type [DEFAULT default]].
func declareNameTypeWidth(body []cst.Tok, st config.Style) (nameW, typeW int) {
if len(body) < 2 {
return 0, 0
}
// name is always the first token.
name := caseText(body[0].Tok, st)
nameW = len(name)
// type runs from body[1] until we hit := / DEFAULT / = at depth 0.
var typeTokens []cst.Tok
depth := 0
for _, t := range body[1:] {
switch t.Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
case lexer.RParen, lexer.RBracket:
if depth > 0 {
depth--
}
}
if depth == 0 {
low := lowerASCII(t.Tok.Text)
if t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=") {
break
}
if t.Tok.Kind == lexer.Ident && low == "default" {
break
}
}
typeTokens = append(typeTokens, t)
}
var tb strings.Builder
for j, t := range typeTokens {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
tb.WriteByte(' ')
}
tb.WriteString(caseText(t.Tok, st))
}
typeW = len(tb.String())
return nameW, typeW
}
// writeDeclareAligned writes a single DECLARE variable with aligned columns.
func writeDeclareAligned(b *strings.Builder, body []cst.Tok, st config.Style, nameColW, typeColW int) {
if len(body) == 0 {
return
}
name := caseText(body[0].Tok, st)
b.WriteString(name)
if len(body) == 1 {
return
}
// Pad name to nameColW if align_type is requested.
if st.PlpgsqlDeclareAlignType {
pad := nameColW - len(name)
for k := 0; k < pad; k++ {
b.WriteByte(' ')
}
}
// Collect type tokens.
var typeTokens, restTokens []cst.Tok
depth := 0
pastType := false
for _, t := range body[1:] {
switch t.Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
case lexer.RParen, lexer.RBracket:
if depth > 0 {
depth--
}
}
if !pastType && depth == 0 {
low := lowerASCII(t.Tok.Text)
if (t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=")) ||
(t.Tok.Kind == lexer.Ident && low == "default") {
pastType = true
restTokens = append(restTokens, t)
continue
}
}
if pastType {
restTokens = append(restTokens, t)
} else {
typeTokens = append(typeTokens, t)
}
}
var typeStr strings.Builder
for j, t := range typeTokens {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
typeStr.WriteByte(' ')
}
typeStr.WriteString(caseText(t.Tok, st))
}
typeRendered := typeStr.String()
b.WriteByte(' ')
b.WriteString(typeRendered)
if len(restTokens) > 0 {
// Pad type to typeColW if align_eq is requested.
if st.PlpgsqlDeclareAlignEq {
pad := typeColW - len(typeRendered)
for k := 0; k < pad; k++ {
b.WriteByte(' ')
}
}
for j, t := range restTokens {
prev := restTokens[0].Tok
if j > 0 {
prev = restTokens[j-1].Tok
}
if j == 0 || needSpace(prev, t.Tok) {
b.WriteByte(' ')
}
b.WriteString(caseText(t.Tok, st))
}
}
emit()
}
// bline is one logical line within an accumulated statement.
@@ -208,12 +363,17 @@ type bline struct {
// 3. After EXCEPTION the formatter switches to verbatim-indent mode (original
// leading whitespace is preserved) to avoid conflicts between styles that
// put WHEN at col-0 vs indented.
// 4. Blank-line counts from the original are preserved.
// 4. Blank-line counts from the original are preserved (capped by PlpgsqlMaxBlankLines).
func formatBodyStatements(text string, st config.Style) string {
nl := st.Newline
normalised := strings.ReplaceAll(text, "\r\n", "\n")
rawLines := strings.Split(normalised, "\n")
maxBlanks := st.PlpgsqlMaxBlankLines
if maxBlanks < 0 {
maxBlanks = 0
}
var (
result strings.Builder
stmt []bline
@@ -228,7 +388,11 @@ func formatBodyStatements(text string, st config.Style) string {
if len(stmt) == 0 {
return
}
for i := 0; i < pendingBlanks; i++ {
blanks := pendingBlanks
if blanks > maxBlanks {
blanks = maxBlanks
}
for i := 0; i < blanks; i++ {
result.WriteString(nl)
}
pendingBlanks = 0
@@ -259,8 +423,6 @@ func formatBodyStatements(text string, st config.Style) string {
}
effectiveDepth = blockDepth
case "else", "elsif", "elseif":
// Emit at one level up; blockDepth unchanged so the body continues
// at the same depth (THEN will re-apply depthInc for elsif).
if blockDepth > 0 {
effectiveDepth = blockDepth - 1
}
@@ -271,7 +433,14 @@ func formatBodyStatements(text string, st config.Style) string {
baseIndent := strings.Repeat(st.Indent, effectiveDepth)
for i, ll := range stmt {
// plpgsql_if_then_newline: when false, THEN stays on the same line as
// the condition. When true (default) it's already on its own logical line.
stmtLines := stmt
if !st.PlpgsqlIfThenNewline && fw == "if" {
stmtLines = joinThenToCondition(stmt)
}
for i, ll := range stmtLines {
if i == 0 || ll.indent == "" {
result.WriteString(baseIndent)
} else {
@@ -301,8 +470,6 @@ func formatBodyStatements(text string, st config.Style) string {
fw := lowerASCII(firstBodyKeyword(stripped))
isColZero := indent == ""
// Only join when we are at paren-depth 0; content inside parens (e.g.
// inside a CTE subquery) should not be merged across lines.
joinToPrev := isColZero && parenDepth == 0 && len(stmt) > 0 && !sqlClauseKw[fw]
if joinToPrev {
@@ -312,7 +479,6 @@ func formatBodyStatements(text string, st config.Style) string {
stmt = append(stmt, bline{text: stripped, indent: indent})
}
// Scan tokens to track paren depth and detect flush triggers.
var lastD0Kw string
for _, tok := range lexer.Lex(stripped) {
if tok.IsTrivia() || tok.Kind == lexer.EOF {
@@ -327,6 +493,13 @@ func formatBodyStatements(text string, st config.Style) string {
}
case lexer.Semicolon:
if parenDepth == 0 {
// plpgsql_loop_collapse: fold empty FOR … LOOP END LOOP; to one line.
if st.PlpgsqlLoopCollapse && len(stmt) > 0 {
collapsed, ok := tryCollapseLoop(stmt, st)
if ok {
stmt = []bline{{text: collapsed, indent: ""}}
}
}
flush()
}
}
@@ -335,13 +508,9 @@ func formatBodyStatements(text string, st config.Style) string {
}
}
// Structural keywords at the end of a line (paren depth 0) trigger a
// flush and possibly a block-depth change.
if parenDepth == 0 && len(stmt) > 0 {
switch lastD0Kw {
case "then", "loop", "begin":
// ELSIF/ELSEIF headers end with THEN but must NOT increment depth
// (blockDepth is already at the right level for the body).
fw0 := lowerASCII(firstBodyKeyword(stmt[0].text))
if fw0 != "elsif" && fw0 != "elseif" {
depthInc = true
@@ -357,6 +526,52 @@ func formatBodyStatements(text string, st config.Style) string {
return result.String()
}
// joinThenToCondition merges a THEN line (on its own bline) into the preceding
// condition line when plpgsql_if_then_newline is false.
func joinThenToCondition(lines []bline) []bline {
out := make([]bline, 0, len(lines))
for i, ll := range lines {
if i > 0 && strings.EqualFold(strings.TrimSpace(ll.text), "then") {
out[len(out)-1].text = strings.TrimRight(out[len(out)-1].text, " \t") + " THEN"
} else {
out = append(out, ll)
}
}
return out
}
// tryCollapseLoop tries to collapse an empty loop body to one line.
// Detects: FOR … LOOP\n (empty or only blanks)\nEND LOOP;
// Returns the collapsed line and true on success.
func tryCollapseLoop(lines []bline, st config.Style) (string, bool) {
if len(lines) < 2 {
return "", false
}
first := strings.TrimSpace(lines[0].text)
last := strings.TrimSpace(lines[len(lines)-1].text)
firstLow := lowerASCII(first)
lastLow := lowerASCII(last)
// Check last line is END LOOP; or LOOP (for WHILE/FOR empty bodies that end with LOOP).
if !strings.HasPrefix(lastLow, "end loop") && lastLow != "end loop;" {
return "", false
}
// Check middle lines are all empty.
for _, mid := range lines[1 : len(lines)-1] {
if strings.TrimSpace(mid.text) != "" {
return "", false
}
}
// Check first line ends with LOOP.
if !strings.HasSuffix(firstLow, "loop") {
return "", false
}
_ = st
_ = firstLow
// Collapse to: <header> END LOOP;
return strings.TrimRight(first, " \t") + " " + strings.ToUpper(last), true
}
// firstBodyKeyword returns the text of the first identifier token in s
// (lowercased), or "" if the first significant token is not an identifier.
func firstBodyKeyword(s string) string {