feat(format): default output to dist/examples house style
CI / Test (push) Successful in 1m12s
CI / Build (push) Successful in 30s

Align the formatter defaults and layout with the hand-formatted reference
procedures in dist/examples so a clean `pgtidy fmt` produces the house style.

config.Default():
- align_param_types: false (no type-column alignment in param lists)
- plpgsql_declare_align_type / plpgsql_declare_align_eq: true

Formatter:
- routine header: leading-comma params at column 0, first param at one
  indent, RETURNS/LANGUAGE/volatility/SECURITY each indented one level
- %type / %rowtype printed tight (isPctTypeBoundary)
- DECLARE = / := / DEFAULT column padded only to the widest declaration
  that carries an assignment
- WHERE continuations in body UPDATE/DELETE: AND/OR aligned with WHERE
- EXCEPTION aligned to its enclosing BEGIN; column-0 comment continuations
  kept flush-left

Safety gate:
- SemanticallyEqual tolerates CRLF vs LF inside string literals (normNL);
  the formatter re-emits all layout with st.Newline, so a \r\n inside a
  multi-line string literal is normalisation, not a code change. This was
  why action_init and event_exec_func previously refused to format.

Corpus:
- add the four CRLF reference files as idempotence/safety fixtures
- regenerate test_a and test_mm_proc goldens

FOR...LOOP body indentation keeps the existing +1 convention (LOOP aligned
with FOR); the dist/examples use +2, so loop-body regions differ by
whitespace only.
This commit is contained in:
Hein
2026-09-10 14:54:20 +02:00
parent 94d776e3de
commit 83b215fd25
17 changed files with 3740 additions and 147 deletions
+49 -7
View File
@@ -177,7 +177,10 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
if nw > nameColW {
nameColW = nw
}
if tw > typeColW {
// typeColW drives the '='/':='/DEFAULT column (align_eq only), so
// only declarations that actually carry an assignment participate —
// a bare "name type;" must not widen it.
if tw > typeColW && declHasAssignment(body) {
typeColW = tw
}
}
@@ -207,7 +210,7 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
writeDeclareAligned(b, body, st, nameColW, typeColW)
} else {
for j, t := range body {
if j > 0 && needSpace(body[j-1].Tok, t.Tok) {
if j > 0 && needSpace(body[j-1].Tok, t.Tok) && !isPctTypeBoundary(body, j) {
b.WriteByte(' ')
}
b.WriteString(caseText(t.Tok, st))
@@ -220,6 +223,32 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
}
}
// declHasAssignment reports whether a DECLARE variable body (name type … ) has
// a default assignment ( := / = / DEFAULT ) at paren depth 0.
func declHasAssignment(body []cst.Tok) bool {
depth := 0
for _, t := range body {
switch t.Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
case lexer.RParen, lexer.RBracket:
if depth > 0 {
depth--
}
}
if depth != 0 {
continue
}
if t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=") {
return true
}
if t.Tok.Kind == lexer.Ident && lowerASCII(t.Tok.Text) == "default" {
return true
}
}
return false
}
// 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]].
@@ -256,7 +285,7 @@ func declareNameTypeWidth(body []cst.Tok, st config.Style) (nameW, typeW int) {
}
var tb strings.Builder
for j, t := range typeTokens {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) && !isPctTypeBoundary(typeTokens, j) {
tb.WriteByte(' ')
}
tb.WriteString(caseText(t.Tok, st))
@@ -315,7 +344,7 @@ func writeDeclareAligned(b *strings.Builder, body []cst.Tok, st config.Style, na
var typeStr strings.Builder
for j, t := range typeTokens {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) && !isPctTypeBoundary(typeTokens, j) {
typeStr.WriteByte(' ')
}
typeStr.WriteString(caseText(t.Tok, st))
@@ -429,7 +458,12 @@ func formatBodyStatements(text string, st config.Style) string {
}
case "exception":
inException = true
effectiveDepth = 0
// EXCEPTION belongs to its nearest enclosing BEGIN, so align it one
// level in from the current block body (col 0 for the outermost).
effectiveDepth = blockDepth - 1
if effectiveDepth < 0 {
effectiveDepth = 0
}
}
baseIndent := strings.Repeat(st.Indent, effectiveDepth)
@@ -589,7 +623,13 @@ func formatBodyStmtLines(lines []bline, baseIndent string, st config.Style) []st
for i, ll := range lines {
text := ll.text
indent := baseIndent
if i > 0 && ll.indent != "" && !isStandaloneBodyKeyword(ll.text, "then", "else", "elsif", "elseif") {
switch {
case i > 0 && ll.indent == "" && len(significantBodyTokens(text)) == 0:
// A column-0 comment line trailing a multi-line (commented-out)
// statement is a continuation the author left flush-left — keep it
// there rather than re-indenting it to block depth.
indent = ""
case i > 0 && ll.indent != "" && !isStandaloneBodyKeyword(ll.text, "then", "else", "elsif", "elseif"):
indent = ll.indent
}
out = append(out, indent+text)
@@ -639,7 +679,9 @@ func reindentBodyDML(lines []bline, baseIndent string, st config.Style) []string
continue
}
if lineDepth == 0 && (kw == "and" || kw == "or") {
out = append(out, baseIndent+st.Indent+strings.TrimSpace(text))
// House style: AND/OR line up with the WHERE keyword; the first
// predicate is indented two levels under it.
out = append(out, baseIndent+strings.TrimSpace(text))
afterWhere = false
updateBodyParenDepth(text, &parenDepth)
continue