* update error handling in various commands to use blank identifier * enhance output formatting for better readability * add golangci-lint to Makefile for linting checks
383 lines
12 KiB
Go
383 lines
12 KiB
Go
// Package config defines PgTidy's formatter (and, later, linter) configuration.
|
|
//
|
|
// Defaults encode the project house style. A .pgtidy.yaml file discovered by
|
|
// walking up from the target directory overrides individual fields.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Case controls keyword/identifier casing.
|
|
type Case string
|
|
|
|
const (
|
|
CaseUpper Case = "upper"
|
|
CaseLower Case = "lower"
|
|
CasePreserve Case = "preserve"
|
|
)
|
|
|
|
// CommaStyle controls where separators sit in multi-line lists.
|
|
type CommaStyle string
|
|
|
|
const (
|
|
CommaLeading CommaStyle = "leading"
|
|
CommaTrailing CommaStyle = "trailing"
|
|
)
|
|
|
|
// WrapMode controls whether a construct wraps to multiple lines.
|
|
type WrapMode string
|
|
|
|
const (
|
|
WrapAlways WrapMode = "always"
|
|
WrapWhenLong WrapMode = "when_long"
|
|
WrapNever WrapMode = "never"
|
|
)
|
|
|
|
// Placement controls where a token or block is placed relative to surrounding content.
|
|
type Placement string
|
|
|
|
const (
|
|
PlacementSameLine Placement = "same_line"
|
|
PlacementNewLine Placement = "new_line"
|
|
)
|
|
|
|
// Style is the formatter configuration.
|
|
type Style struct {
|
|
// --- Core ---
|
|
Indent string
|
|
Newline string
|
|
|
|
// --- Casing ---
|
|
KeywordCase Case
|
|
IdentCase Case
|
|
TypeCase Case
|
|
AliasCase Case // token immediately following AS in SELECT/FROM
|
|
BuiltinCase Case // built-in function names (COALESCE, MAX, NOW, …)
|
|
CustomTypeCase Case // user-defined / domain types not in the built-in set
|
|
|
|
// --- Query layout ---
|
|
Commas CommaStyle
|
|
AlignColumns bool // pad SELECT list so values align
|
|
AlignLineComments bool // align trailing -- comments in a block
|
|
SelectAlignAs bool // pad between expression and AS in SELECT list
|
|
SetAlignEqual bool // align = in UPDATE SET list
|
|
IndentJoin bool // extra indentation for JOIN … ON lines
|
|
JoinIndentSize int // extra indent levels for JOINs (default 1)
|
|
WhereWrap WrapMode // always|when_long|never — each AND/OR on its own line
|
|
WhereAndOrIndent bool // AND/OR indented one level under WHERE
|
|
|
|
// --- Subqueries ---
|
|
SubqueryOpening Placement // opening ( placement: same_line|new_line
|
|
SubqueryContent Placement // content indentation: same_line|new_line
|
|
SubqueryClosing Placement // closing ) placement: same_line|new_line
|
|
SubquerySpaceBeforeParen bool // space before ( in subqueries
|
|
|
|
// --- INSERT ---
|
|
InsertCollapseValues bool // fold multiple VALUES rows onto fewer lines
|
|
|
|
// --- Routines ---
|
|
AlignParamTypes bool // pad param names so type column aligns
|
|
RoutineAsWrap bool // newline before AS $$
|
|
|
|
// --- PL/pgSQL body ---
|
|
PlpgsqlMaxBlankLines int // max consecutive blank lines in body
|
|
PlpgsqlDeclareAlignType bool // align type column in DECLARE block
|
|
PlpgsqlDeclareAlignEq bool // align := / = in DECLARE block
|
|
PlpgsqlIfThenNewline bool // THEN on its own line
|
|
PlpgsqlLoopCollapse bool // collapse empty loop bodies to one line
|
|
|
|
// --- Expressions ---
|
|
BinaryOpAlign bool // align =, <>, || etc. vertically in WHERE/expr lists
|
|
SpaceAfterCommaInCalls bool // space after , in function calls: func(a, b)
|
|
CaseWhenWrap bool // each WHEN … THEN on its own line
|
|
CaseEnd Placement // END placement: same_line|new_line
|
|
CaseCollapse bool // collapse short CASE to one line
|
|
RecordSpaceBeforeParen bool // space before ( in ROW(…) / record constructors
|
|
}
|
|
|
|
// Default returns the house-style configuration.
|
|
func Default() Style {
|
|
return Style{
|
|
Indent: " ",
|
|
Newline: "\n",
|
|
|
|
KeywordCase: CaseUpper,
|
|
IdentCase: CaseLower,
|
|
TypeCase: CaseLower,
|
|
AliasCase: CaseLower,
|
|
BuiltinCase: CaseLower,
|
|
CustomTypeCase: CaseLower,
|
|
|
|
Commas: CommaLeading,
|
|
AlignColumns: false,
|
|
AlignLineComments: false,
|
|
SelectAlignAs: false,
|
|
SetAlignEqual: false,
|
|
IndentJoin: false,
|
|
JoinIndentSize: 1,
|
|
WhereWrap: WrapAlways,
|
|
WhereAndOrIndent: true,
|
|
|
|
SubqueryOpening: PlacementSameLine,
|
|
SubqueryContent: PlacementNewLine,
|
|
SubqueryClosing: PlacementNewLine,
|
|
SubquerySpaceBeforeParen: false,
|
|
|
|
InsertCollapseValues: true,
|
|
|
|
AlignParamTypes: true,
|
|
RoutineAsWrap: true,
|
|
|
|
PlpgsqlMaxBlankLines: 1,
|
|
PlpgsqlDeclareAlignType: false,
|
|
PlpgsqlDeclareAlignEq: false,
|
|
PlpgsqlIfThenNewline: true,
|
|
PlpgsqlLoopCollapse: true,
|
|
|
|
BinaryOpAlign: false,
|
|
SpaceAfterCommaInCalls: false,
|
|
CaseWhenWrap: false,
|
|
CaseEnd: PlacementNewLine,
|
|
CaseCollapse: false,
|
|
RecordSpaceBeforeParen: false,
|
|
}
|
|
}
|
|
|
|
// yamlFile is the on-disk representation of .pgtidy.yaml.
|
|
// All fields are pointers so we can distinguish "not set" from "set to zero value".
|
|
type yamlFile struct {
|
|
Indent *string `yaml:"indent"`
|
|
Newline *string `yaml:"newline"`
|
|
|
|
KeywordCase *string `yaml:"keyword_case"`
|
|
IdentCase *string `yaml:"ident_case"`
|
|
TypeCase *string `yaml:"type_case"`
|
|
AliasCase *string `yaml:"alias_case"`
|
|
BuiltinCase *string `yaml:"builtin_case"`
|
|
CustomTypeCase *string `yaml:"custom_type_case"`
|
|
|
|
Commas *string `yaml:"commas"`
|
|
AlignColumns *bool `yaml:"align_columns"`
|
|
AlignLineComments *bool `yaml:"align_line_comments"`
|
|
SelectAlignAs *bool `yaml:"select_align_as"`
|
|
SetAlignEqual *bool `yaml:"set_align_equal"`
|
|
IndentJoin *bool `yaml:"indent_join"`
|
|
JoinIndentSize *int `yaml:"join_indent_size"`
|
|
WhereWrap *string `yaml:"where_wrap"`
|
|
WhereAndOrIndent *bool `yaml:"where_and_or_indent"`
|
|
|
|
SubqueryOpening *string `yaml:"subquery_opening"`
|
|
SubqueryContent *string `yaml:"subquery_content"`
|
|
SubqueryClosing *string `yaml:"subquery_closing"`
|
|
SubquerySpaceBeforeParen *bool `yaml:"subquery_space_before_paren"`
|
|
|
|
InsertCollapseValues *bool `yaml:"insert_collapse_values"`
|
|
|
|
AlignParamTypes *bool `yaml:"align_param_types"`
|
|
RoutineAsWrap *bool `yaml:"routine_as_wrap"`
|
|
|
|
PlpgsqlMaxBlankLines *int `yaml:"plpgsql_max_blank_lines"`
|
|
PlpgsqlDeclareAlignType *bool `yaml:"plpgsql_declare_align_type"`
|
|
PlpgsqlDeclareAlignEq *bool `yaml:"plpgsql_declare_align_eq"`
|
|
PlpgsqlIfThenNewline *bool `yaml:"plpgsql_if_then_newline"`
|
|
PlpgsqlLoopCollapse *bool `yaml:"plpgsql_loop_collapse"`
|
|
|
|
BinaryOpAlign *bool `yaml:"binary_op_align"`
|
|
SpaceAfterCommaInCalls *bool `yaml:"space_after_comma_in_calls"`
|
|
CaseWhenWrap *bool `yaml:"case_when_wrap"`
|
|
CaseEnd *string `yaml:"case_end"`
|
|
CaseCollapse *bool `yaml:"case_collapse"`
|
|
RecordSpaceBeforeParen *bool `yaml:"record_space_before_paren"`
|
|
}
|
|
|
|
// Load discovers and parses the nearest .pgtidy.yaml by walking up from
|
|
// startDir. Fields present in the file override the house-style defaults;
|
|
// missing fields keep the default value. Returns Default() when no config
|
|
// file is found.
|
|
func Load(startDir string) (Style, error) {
|
|
st := Default()
|
|
path, err := findConfig(startDir)
|
|
if err != nil || path == "" {
|
|
return st, err
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return st, fmt.Errorf("pgtidy: read %s: %w", path, err)
|
|
}
|
|
|
|
var yf yamlFile
|
|
if err := yaml.Unmarshal(data, &yf); err != nil {
|
|
return st, fmt.Errorf("pgtidy: parse %s: %w", path, err)
|
|
}
|
|
|
|
if yf.Indent != nil {
|
|
st.Indent = *yf.Indent
|
|
}
|
|
if yf.Newline != nil {
|
|
st.Newline = *yf.Newline
|
|
}
|
|
|
|
if err := loadCase(yf.KeywordCase, &st.KeywordCase, path, "keyword_case"); err != nil {
|
|
return st, err
|
|
}
|
|
if err := loadCase(yf.IdentCase, &st.IdentCase, path, "ident_case"); err != nil {
|
|
return st, err
|
|
}
|
|
if err := loadCase(yf.TypeCase, &st.TypeCase, path, "type_case"); err != nil {
|
|
return st, err
|
|
}
|
|
if err := loadCase(yf.AliasCase, &st.AliasCase, path, "alias_case"); err != nil {
|
|
return st, err
|
|
}
|
|
if err := loadCase(yf.BuiltinCase, &st.BuiltinCase, path, "builtin_case"); err != nil {
|
|
return st, err
|
|
}
|
|
if err := loadCase(yf.CustomTypeCase, &st.CustomTypeCase, path, "custom_type_case"); err != nil {
|
|
return st, err
|
|
}
|
|
|
|
if yf.Commas != nil {
|
|
cs := CommaStyle(*yf.Commas)
|
|
if cs != CommaLeading && cs != CommaTrailing {
|
|
return st, fmt.Errorf("pgtidy: %s: commas: must be \"leading\" or \"trailing\"", path)
|
|
}
|
|
st.Commas = cs
|
|
}
|
|
|
|
loadBool(yf.AlignColumns, &st.AlignColumns)
|
|
loadBool(yf.AlignLineComments, &st.AlignLineComments)
|
|
loadBool(yf.SelectAlignAs, &st.SelectAlignAs)
|
|
loadBool(yf.SetAlignEqual, &st.SetAlignEqual)
|
|
loadBool(yf.IndentJoin, &st.IndentJoin)
|
|
if yf.JoinIndentSize != nil {
|
|
st.JoinIndentSize = *yf.JoinIndentSize
|
|
}
|
|
if yf.WhereWrap != nil {
|
|
wm := WrapMode(*yf.WhereWrap)
|
|
if err := validWrap(wm); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: where_wrap: %w", path, err)
|
|
}
|
|
st.WhereWrap = wm
|
|
}
|
|
loadBool(yf.WhereAndOrIndent, &st.WhereAndOrIndent)
|
|
|
|
if yf.SubqueryOpening != nil {
|
|
pl := Placement(*yf.SubqueryOpening)
|
|
if err := validPlacement(pl); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: subquery_opening: %w", path, err)
|
|
}
|
|
st.SubqueryOpening = pl
|
|
}
|
|
if yf.SubqueryContent != nil {
|
|
pl := Placement(*yf.SubqueryContent)
|
|
if err := validPlacement(pl); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: subquery_content: %w", path, err)
|
|
}
|
|
st.SubqueryContent = pl
|
|
}
|
|
if yf.SubqueryClosing != nil {
|
|
pl := Placement(*yf.SubqueryClosing)
|
|
if err := validPlacement(pl); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: subquery_closing: %w", path, err)
|
|
}
|
|
st.SubqueryClosing = pl
|
|
}
|
|
loadBool(yf.SubquerySpaceBeforeParen, &st.SubquerySpaceBeforeParen)
|
|
|
|
loadBool(yf.InsertCollapseValues, &st.InsertCollapseValues)
|
|
|
|
loadBool(yf.AlignParamTypes, &st.AlignParamTypes)
|
|
loadBool(yf.RoutineAsWrap, &st.RoutineAsWrap)
|
|
|
|
if yf.PlpgsqlMaxBlankLines != nil {
|
|
st.PlpgsqlMaxBlankLines = *yf.PlpgsqlMaxBlankLines
|
|
}
|
|
loadBool(yf.PlpgsqlDeclareAlignType, &st.PlpgsqlDeclareAlignType)
|
|
loadBool(yf.PlpgsqlDeclareAlignEq, &st.PlpgsqlDeclareAlignEq)
|
|
loadBool(yf.PlpgsqlIfThenNewline, &st.PlpgsqlIfThenNewline)
|
|
loadBool(yf.PlpgsqlLoopCollapse, &st.PlpgsqlLoopCollapse)
|
|
|
|
loadBool(yf.BinaryOpAlign, &st.BinaryOpAlign)
|
|
loadBool(yf.SpaceAfterCommaInCalls, &st.SpaceAfterCommaInCalls)
|
|
loadBool(yf.CaseWhenWrap, &st.CaseWhenWrap)
|
|
if yf.CaseEnd != nil {
|
|
pl := Placement(*yf.CaseEnd)
|
|
if err := validPlacement(pl); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: case_end: %w", path, err)
|
|
}
|
|
st.CaseEnd = pl
|
|
}
|
|
loadBool(yf.CaseCollapse, &st.CaseCollapse)
|
|
loadBool(yf.RecordSpaceBeforeParen, &st.RecordSpaceBeforeParen)
|
|
|
|
return st, nil
|
|
}
|
|
|
|
func loadCase(src *string, dst *Case, path, key string) error {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
c := Case(*src)
|
|
if err := validCase(c); err != nil {
|
|
return fmt.Errorf("pgtidy: %s: %s: %w", path, key, err)
|
|
}
|
|
*dst = c
|
|
return nil
|
|
}
|
|
|
|
func loadBool(src *bool, dst *bool) {
|
|
if src != nil {
|
|
*dst = *src
|
|
}
|
|
}
|
|
|
|
// findConfig walks parent directories from startDir looking for .pgtidy.yaml.
|
|
// Returns ("", nil) when no file is found before reaching the filesystem root.
|
|
func findConfig(startDir string) (string, error) {
|
|
dir, err := filepath.Abs(startDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for {
|
|
candidate := filepath.Join(dir, ".pgtidy.yaml")
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return "", nil
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|
|
|
|
func validCase(c Case) error {
|
|
switch c {
|
|
case CaseUpper, CaseLower, CasePreserve:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("must be \"upper\", \"lower\", or \"preserve\"")
|
|
}
|
|
|
|
func validWrap(w WrapMode) error {
|
|
switch w {
|
|
case WrapAlways, WrapWhenLong, WrapNever:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("must be \"always\", \"when_long\", or \"never\"")
|
|
}
|
|
|
|
func validPlacement(p Placement) error {
|
|
switch p {
|
|
case PlacementSameLine, PlacementNewLine:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("must be \"same_line\" or \"new_line\"")
|
|
}
|