feat: initial plan

This commit is contained in:
Hein
2026-06-23 16:59:50 +02:00
parent 90fe1a448b
commit 625ddc79a1
23 changed files with 3390 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
package main
import (
"fmt"
"io"
"os"
"github.com/hein/pgtidy/pkg/config"
"github.com/hein/pgtidy/pkg/format"
"github.com/hein/pgtidy/pkg/parser"
)
// cmdFmt implements `pgtidy fmt`. It follows the gofmt model: with no flags it
// prints the formatted result to stdout; -w rewrites in place; -l lists files
// that differ; --check exits non-zero if any input is unformatted.
func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var (
write bool
list bool
check bool
files []string
)
for _, a := range args {
switch a {
case "-w", "--write":
write = true
case "-l", "--list":
list = true
case "--check":
check = true
case "-h", "--help":
usage(stdout)
return 0
default:
if len(a) > 1 && a[0] == '-' {
fmt.Fprintf(stderr, "pgtidy fmt: unknown flag %q\n", a)
return 2
}
files = append(files, a)
}
}
// TODO: discover and parse .pgtidy.yaml; for now use the house-style default.
st := config.Default()
// stdin → stdout when no files are given.
if len(files) == 0 {
src, err := io.ReadAll(stdin)
if err != nil {
fmt.Fprintf(stderr, "pgtidy: reading stdin: %v\n", err)
return 2
}
out := format.File(parser.Parse(string(src)), st)
if check {
if out != string(src) {
return 1
}
return 0
}
io.WriteString(stdout, out)
return 0
}
exit := 0
anyDiff := false
for _, path := range files {
src, err := os.ReadFile(path)
if err != nil {
fmt.Fprintf(stderr, "pgtidy: %v\n", err)
exit = 2
continue
}
out := format.File(parser.Parse(string(src)), st)
changed := out != string(src)
if changed {
anyDiff = true
}
switch {
case write:
if changed {
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
fmt.Fprintf(stderr, "pgtidy: writing %s: %v\n", path, err)
exit = 2
}
}
case list:
if changed {
fmt.Fprintln(stdout, path)
}
case check:
// handled after loop via anyDiff
default:
io.WriteString(stdout, out)
}
}
if check && anyDiff && exit == 0 {
return 1
}
return exit
}
+69
View File
@@ -0,0 +1,69 @@
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestFmtStdin(t *testing.T) {
in := strings.NewReader("create function f() returns int language sql as $$ select 1 $$;\n")
var out, errb bytes.Buffer
rc := run([]string{"fmt"}, in, &out, &errb)
if rc != 0 {
t.Fatalf("rc=%d stderr=%s", rc, errb.String())
}
if !strings.HasPrefix(out.String(), "CREATE FUNCTION f(") {
t.Errorf("unexpected output:\n%s", out.String())
}
}
func TestFmtCheckStdin(t *testing.T) {
unformatted := "create function f() returns int language sql as $$ select 1 $$;"
// Unformatted input → rc 1.
var out, errb bytes.Buffer
if rc := run([]string{"fmt", "--check"}, strings.NewReader(unformatted), &out, &errb); rc != 1 {
t.Errorf("unformatted input: rc=%d, want 1", rc)
}
// Its own formatted output → rc 0 (idempotent + check agree).
out.Reset()
errb.Reset()
run([]string{"fmt"}, strings.NewReader(unformatted), &out, &errb)
formatted := out.String()
var out2, errb2 bytes.Buffer
if rc := run([]string{"fmt", "--check"}, strings.NewReader(formatted), &out2, &errb2); rc != 0 {
t.Errorf("formatted input: rc=%d, want 0\noutput was:\n%s", rc, formatted)
}
}
func TestFmtWriteInPlace(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "f.pgsql")
orig := "create function f() returns int language sql as $$ select 1 $$;\n"
if err := os.WriteFile(path, []byte(orig), 0o644); err != nil {
t.Fatal(err)
}
var out, errb bytes.Buffer
if rc := run([]string{"fmt", "-w", path}, nil, &out, &errb); rc != 0 {
t.Fatalf("rc=%d stderr=%s", rc, errb.String())
}
got, _ := os.ReadFile(path)
if string(got) == orig {
t.Error("file was not rewritten")
}
// Second write is a no-op (idempotent).
if rc := run([]string{"fmt", "--check", path}, nil, &out, &errb); rc != 0 {
t.Errorf("after -w, --check rc=%d, want 0", rc)
}
}
func TestUnknownCommand(t *testing.T) {
var out, errb bytes.Buffer
if rc := run([]string{"frobnicate"}, nil, &out, &errb); rc != 2 {
t.Errorf("rc=%d, want 2", rc)
}
}
+52
View File
@@ -0,0 +1,52 @@
// Command pgtidy is the PgTidy CLI: a PostgreSQL formatter (and, later, linter)
// and LSP server. Today it provides the `fmt` subcommand.
package main
import (
"fmt"
"io"
"os"
)
// version is overridden at build time via -ldflags "-X main.version=...".
var version = "dev"
func main() {
os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
}
func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if len(args) == 0 {
usage(stderr)
return 2
}
switch args[0] {
case "fmt", "format":
return cmdFmt(args[1:], stdin, stdout, stderr)
case "version", "--version", "-v":
fmt.Fprintf(stdout, "pgtidy %s\n", version)
return 0
case "help", "-h", "--help":
usage(stdout)
return 0
default:
fmt.Fprintf(stderr, "pgtidy: unknown command %q\n", args[0])
usage(stderr)
return 2
}
}
func usage(w io.Writer) {
fmt.Fprint(w, `pgtidy — PostgreSQL formatter and linter
Usage:
pgtidy fmt [flags] [files...] Format SQL/PL-pgSQL (stdin if no files)
pgtidy version Print version
pgtidy help Show this help
fmt flags:
-w, --write Rewrite files in place
-l, --list List files whose formatting differs (no writes)
--check Exit non-zero if any input is not already formatted (CI)
`)
}