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.
74 lines
2.3 KiB
Go
74 lines
2.3 KiB
Go
package format
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/pgtidy/pkg/lexer"
|
|
)
|
|
|
|
// SemanticallyEqual reports whether a and b have the same non-trivia token
|
|
// stream, i.e. formatting may only ever change whitespace/comment trivia and
|
|
// layout — it must never add, remove, or alter a token of actual code.
|
|
// Unquoted identifiers/keywords compare case-insensitively (casing is a
|
|
// style choice); everything else (strings, numbers, operators, punctuation)
|
|
// must match exactly. Dollar-quoted body tokens are compared recursively so
|
|
// that independent body reformatting doesn't trigger a false failure.
|
|
//
|
|
// The CLI and LSP must call this before ever writing or emitting formatted
|
|
// output: if it returns false, the formatter has a bug and the original
|
|
// source must be kept, never the (corrupting) formatted output.
|
|
func SemanticallyEqual(a, b string) bool {
|
|
ta := significantTokens(a)
|
|
tb := significantTokens(b)
|
|
if len(ta) != len(tb) {
|
|
return false
|
|
}
|
|
for i := range ta {
|
|
if ta[i].Kind != tb[i].Kind {
|
|
return false
|
|
}
|
|
switch ta[i].Kind {
|
|
case lexer.Ident:
|
|
if !strings.EqualFold(ta[i].Text, tb[i].Text) {
|
|
return false
|
|
}
|
|
case lexer.String, lexer.EscapeString, lexer.BitString, lexer.HexString, lexer.UnicodeString:
|
|
// A CRLF vs LF difference inside a multi-line string literal is a
|
|
// line-ending normalisation, not a change of code content — the
|
|
// formatter always re-emits layout with st.Newline. Compare the
|
|
// literal modulo \r\n ↔ \n.
|
|
if normNL(ta[i].Text) != normNL(tb[i].Text) {
|
|
return false
|
|
}
|
|
case lexer.DollarString:
|
|
_, innerA, _, okA := splitDollarQuote(ta[i].Text)
|
|
_, innerB, _, okB := splitDollarQuote(tb[i].Text)
|
|
if okA != okB || (okA && !SemanticallyEqual(innerA, innerB)) {
|
|
return false
|
|
}
|
|
default:
|
|
if ta[i].Text != tb[i].Text {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// normNL collapses CRLF to LF so string literals compare independent of the
|
|
// source file's line-ending convention.
|
|
func normNL(s string) string { return strings.ReplaceAll(s, "\r\n", "\n") }
|
|
|
|
// significantTokens lexes src and returns its tokens excluding EOF and trivia
|
|
// (whitespace/comments).
|
|
func significantTokens(src string) []lexer.Token {
|
|
var out []lexer.Token
|
|
for _, t := range lexer.Lex(src) {
|
|
if t.Kind == lexer.EOF || t.IsTrivia() {
|
|
continue
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
return out
|
|
}
|