feat(format): implement PL/pgSQL body formatting

* add formatBody and formatBodyInner functions for DECLARE section
* update needSpace to handle LBracket correctly
* enhance semanticallyEqual to compare dollar-quoted bodies
* add test data for broken layout scenarios
This commit is contained in:
2026-06-23 21:15:27 +02:00
parent 625ddc79a1
commit 6492ab35b7
7 changed files with 454 additions and 18 deletions
+50 -11
View File
@@ -38,22 +38,61 @@ Legend: ✅ done · 🚧 in progress · ⬜ not started
- **Status:** all tests pass.
- _Still TODO (later): DML/other-DDL structuring (currently Raw) for full formatting._
### PL/pgSQL body parser ← NEXT (the remaining V1 piece)
- Parse `$$ ... $$` bodies: DECLARE/BEGIN/END, IF/ELSIF/ELSE/END IF, LOOP/FOR/WHILE,
CASE, assignments (`:=` / `=`), RAISE, nested SQL statements, EXCEPTION blocks.
- The crux for stored-procedure formatting (primary use case). Currently the body is
emitted verbatim; this milestone formats inside it. Reuse `inline`/spacing/casing from
`pkg/format` and keep verbatim fallback for unparsable constructs.
### 🚧 PL/pgSQL body parser ← NEXT (the remaining V1 piece)
#### ✅ DECLARE section — `pkg/format/body.go`
- `formatBody` splits the dollar-quote tag, calls `formatBodyInner`.
- `formatBodyInner` locates `DECLARE` and `BEGIN` at depth 0, formats the declare block,
then emits `BEGIN` onwards verbatim.
- `formatDeclareVars`: each variable declaration collapsed to one line
(` name type [= expr];`), `--Block--` comment markers preserved on their own lines,
mid-declaration block comments trigger verbatim fallback.
- `needSpace` fixed for `LBracket` — no space before `[` after ident/closing bracket
(fixes `citext[]`, array subscripts).
- `semanticallyEqual` in tests updated to recurse into dollar-quoted body tokens so
whitespace normalization inside the body does not falsely fail the semantic check.
- Added `testdata/corpus/test_a_broken.pgsql` — a CRLF corpus file with intentionally
broken layout (split-line variables + split-line body statements) used as a formatting
target; `testdata/corpus/test_a.pgsql` is the golden output.
- **Status:** all tests pass; DECLARE section formats correctly.
#### ⬜ Body statement formatter — `pkg/format/body.go` (next step)
Two-phase formatter for the `BEGIN … END` block:
1. **Block-depth tracker** — recognise `BEGIN`/`END`, `IF`/`THEN`/`ELSIF`/`ELSE`/`END IF`,
`LOOP`/`END LOOP`, `FOR`/`END LOOP`, `CASE`/`END CASE`, `EXCEPTION`/`WHEN` to maintain
current indent depth (`depth × 2` spaces).
2. **Split-line join** — within each statement (tokens up to `;` at depth 0), a physical
line whose first non-whitespace token is at column 0 is a broken continuation; join it to
the preceding line with a single space. Exceptions: SQL clause keywords (`FROM`, `WHERE`,
`INTO`, `HAVING`, `GROUP`, `ORDER`, `RETURNING`) stay on their own line.
3. **Base-indent normalisation** — after joining, each logical line is re-emitted at
`depth × 2` spaces, stripping its original leading whitespace and replacing it with the
computed indent. Internal relative indentation of multi-line expressions is preserved.
4. **Blank-line preservation** — blank lines between statements in the original are kept
(they carry author intent about logical grouping).
5. **Verbatim fallback** — any construct the formatter cannot classify cleanly passes
through verbatim, upholding invariant #4.
Acceptance: `pgtidy fmt testdata/corpus/test_a_broken.pgsql` produces output matching
`testdata/corpus/test_a.pgsql` (new golden-file test).
### ✅ Printer + style config — `pkg/format`, `pkg/config`
- `pkg/config`: `Style` struct + `Default()` = house style (UPPERCASE keywords, lowercase
types, 2-space indent, leading commas, spacing rules).
- `pkg/format`: formats CREATE FUNCTION/PROCEDURE **headers** to house style (params
one-per-line leading-comma, option clauses each on own line, AS/`$$` own lines); body and
`Raw` statements emitted verbatim. Spacing engine (`needSpace`, tight ops `:: : -> ->>`)
+ casing (`keywords`/`typeNames` sets). Comment-safety: verbatim fallback if a header
carries comments it cannot relocate.
- Tests: golden header, idempotence, corpus idempotence + **semantic equivalence**.
one-per-line leading-comma, option clauses each on own line, AS/`$$` own lines); DECLARE
section formatted (see body parser entry); `Raw` statements emitted verbatim.
Spacing engine (`needSpace`, tight ops `:: : -> ->>`, array `[]`) + casing
(`keywords`/`typeNames` sets). Comment-safety: verbatim fallback if a header carries
comments it cannot relocate.
- `pkg/format/body.go`: DECLARE section formatter (see body parser entry).
- Tests: golden header, idempotence, corpus idempotence + **semantic equivalence**
(updated to recurse into dollar-quoted body tokens).
- _Note: not a full Wadler Doc-IR yet — fixed-layout printer. Doc-IR for width-based
expression wrapping can come when DML structuring lands._