932c83dbad
* Implemented `-d`/`--diff` flag to print unified diffs. * Added `unifiedDiff` function for generating diffs. * Config discovery now merges fields from `.pgtidy.yaml`.
162 lines
4.2 KiB
Go
162 lines
4.2 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 leaves the token text unchanged.
|
|
CasePreserve Case = "preserve"
|
|
)
|
|
|
|
// CommaStyle controls where separators sit in multi-line lists.
|
|
type CommaStyle string
|
|
|
|
const (
|
|
// CommaLeading puts the comma at the start of the continuation line
|
|
// (",col"), the house style.
|
|
CommaLeading CommaStyle = "leading"
|
|
// CommaTrailing puts the comma at the end of the preceding line ("col,").
|
|
CommaTrailing CommaStyle = "trailing"
|
|
)
|
|
|
|
// Style is the formatter configuration.
|
|
type Style struct {
|
|
// Indent is one indentation level (default two spaces).
|
|
Indent string
|
|
// Newline is the line terminator emitted by the formatter.
|
|
Newline string
|
|
// KeywordCase controls SQL keyword casing (types excluded — see TypeCase).
|
|
KeywordCase Case
|
|
// IdentCase controls unquoted identifier casing (quoted identifiers are
|
|
// never touched).
|
|
IdentCase Case
|
|
// TypeCase controls built-in type-name casing.
|
|
TypeCase Case
|
|
// Commas controls list separator placement.
|
|
Commas CommaStyle
|
|
}
|
|
|
|
// Default returns the house-style configuration.
|
|
func Default() Style {
|
|
return Style{
|
|
Indent: " ",
|
|
Newline: "\n",
|
|
KeywordCase: CaseUpper,
|
|
IdentCase: CaseLower,
|
|
TypeCase: CaseLower,
|
|
Commas: CommaLeading,
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
Commas *string `yaml:"commas"`
|
|
}
|
|
|
|
// 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 yf.KeywordCase != nil {
|
|
c := Case(*yf.KeywordCase)
|
|
if err := validCase(c); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: keyword_case: %w", path, err)
|
|
}
|
|
st.KeywordCase = c
|
|
}
|
|
if yf.IdentCase != nil {
|
|
c := Case(*yf.IdentCase)
|
|
if err := validCase(c); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: ident_case: %w", path, err)
|
|
}
|
|
st.IdentCase = c
|
|
}
|
|
if yf.TypeCase != nil {
|
|
c := Case(*yf.TypeCase)
|
|
if err := validCase(c); err != nil {
|
|
return st, fmt.Errorf("pgtidy: %s: type_case: %w", path, err)
|
|
}
|
|
st.TypeCase = c
|
|
}
|
|
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
|
|
}
|
|
|
|
return st, nil
|
|
}
|
|
|
|
// 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\"")
|
|
}
|