Files
PgTidy/cmd/pgtidy/fmt.go
T
Hein 5cdec88299
CI / Test (push) Successful in 37s
CI / Build (push) Successful in 52s
feat(format): expand the runtime safety gate
Replace the bare SemanticallyEqual call in every frontend (CLI fmt, LSP
formatting + rangeFormatting) with format.VerifySafe, which runs four
checks before any formatted output is emitted:

  - semantic equivalence  - the code token stream is unchanged
  - comment preservation  - no -- or /* */ comment is dropped, merged,
    split, reordered, or reworded (line endings / indentation normalised
    away; recurses into dollar-quoted bodies)
  - structural balance    - the () [] and BEGIN/CASE/IF/LOOP...END nesting
    profile matches, ignoring anything inside a comment or a string
  - idempotence           - a second format pass would not change it

On failure the CLI now prints the specific reason and keeps the original.

The comment check surfaced two real formatter bugs, both fixed in
formatBodyStatements:

  - multi-line /* */ comments inside a PL/pgSQL body had their interior
    lines re-split and reindented as if they were statements; they are
    now tracked and carried verbatim with the opening line
  - a column-0 -- line was glued onto the preceding line by the
    split-line-join, which merged consecutive comment lines into one

Regenerate testdata/corpus/test_mm_proc.pgsql (was carrying the mangled
output). TestCorpusIdempotentAndSafe now runs the full VerifySafe bundle;
add safety_test.go with targeted cases.
2026-09-10 15:17:07 +02:00

129 lines
2.9 KiB
Go

package main
import (
"fmt"
"io"
"os"
"git.warky.dev/wdevs/pgtidy/pkg/config"
"git.warky.dev/wdevs/pgtidy/pkg/format"
"git.warky.dev/wdevs/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; -d prints a
// unified diff for each file that would change.
func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var (
write bool
list bool
check bool
diff bool
files []string
)
for _, a := range args {
switch a {
case "-w", "--write":
write = true
case "-l", "--list":
list = true
case "--check":
check = true
case "-d", "--diff":
diff = 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)
}
}
// Discover config: walk up from the working directory.
wd, err := os.Getwd()
if err != nil {
wd = "."
}
st, err := config.Load(wd)
if err != nil {
_, _ = fmt.Fprintf(stderr, "%v\n", err)
return 2
}
// 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 err := format.VerifySafe(string(src), out, st); err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: refusing to format stdin: formatter safety check failed: %v\n", err)
return 2
}
switch {
case check:
if out != string(src) {
return 1
}
case diff:
_, _ = io.WriteString(stdout, unifiedDiff(string(src), out, "stdin"))
default:
_, _ = 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)
if err := format.VerifySafe(string(src), out, st); err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: refusing to format %s: formatter safety check failed: %v\n", path, err)
exit = 2
continue
}
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 diff:
if changed {
_, _ = io.WriteString(stdout, unifiedDiff(string(src), out, path))
}
case check:
// handled after loop via anyDiff
default:
_, _ = io.WriteString(stdout, out)
}
}
if check && anyDiff && exit == 0 {
return 1
}
return exit
}