Skip to content

fix(cli): clearer formatter option errors and check output - #917

Merged
psteinroe merged 13 commits into
mainfrom
fix/cli-format-options
Oct 26, 2026
Merged

psteinroe merged 13 commits into
mainfrom
fix/cli-format-options

Conversation

@psteinroe

Copy link
Copy Markdown
Collaborator

Stacked on #912.

Fixes formatter options and format check output that were hard to use, found in the release review.

Option errors list the valid values

$ postgres-language-server format --clause-body-style=foo a.sql
# before
Error: couldn't parse `foo`: Value not supported for clause body style
# after
Error: couldn't parse `foo`: expected 'break' or 'compact'

$ postgres-language-server format --values-row-layout=foo a.sql
# before
Error: couldn't parse `foo`: Value not supported for Layout. Use 'fit' or 'expanded'.
# after
Error: couldn't parse `foo`: expected 'fit' or 'expanded'

All formatter enums now say what they expect, without naming the Rust type. Layout is shared by --layout and --values-row-layout, and the user never sees that name.

A zero line width or indent size is rejected

--line-width=0 and --indent-size=0, and "lineWidth": 0 / "indentSize": 0 in the config file, were accepted. lineWidth and indentSize are now NonZeroU16 / NonZeroU8, as files.maxSize already is. The CLI says couldn't parse '0': number would be zero for non-zero type, the config file invalid value: integer '0', expected a nonzero u16. The JSON schema now has minimum: 1 for both.

Check output names the line

Without --write, format printed one diagnostic per statement that would change. When a statement's diff fits on one line, it is printed without line numbers, and the header only names the file. In a large file there was no way to find the statement. The message now carries the line:

a.sql format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  × Statement at line 4 could be formatted using `--write`

  - SELECT··x·FROM·y·WHERE·z=1;
  + select·x·from·y·where·z·=·1;

I put the line in the message rather than giving the diagnostic a span. With a span, the printer adds a code frame of the statement above the diff. When only the blank lines between statements change, the whole-file diff now says "File could be formatted" instead of "Statement".

Help shows the values

Every format flag was shown as =ARG in --help and in the CLI reference. They now show NUMBER, <spaces|tabs>, <fit|expanded> and so on, like the other options.

Docs

The layout option was described as "always breaks between clauses". Expanded layout also puts every select list item and every AND/OR operand on its own line and breaks before every CASE branch. A select-list-only subquery and a CHECK condition follow the line width instead. The option's description (and so the schema, bindings and CLI reference) and docs/features/formatting.md now say this, and the docs have an example. Together with the CASE and tabs docs in #894, this covers the docs gaps from the review.

Tests

  • CLI tests for the two option errors and for rejecting zero, with the two check-output snapshots updated for the new messages.
  • A configuration test for rejecting zero in the config file.
  • Schema, TypeScript bindings and CLI reference regenerated with just gen-lint.

Risks

A config file with lineWidth or indentSize set to 0 used to load and now fails to load with the error above. With either set to 0 the formatter output was unusable anyway: every group broke, or nothing was indented.

@psteinroe
psteinroe changed the base branch from test/pretty-print-option-matrix to main October 26, 2026 09:32
@psteinroe
psteinroe merged commit 4af0a49 into main Oct 26, 2026
12 checks passed
@psteinroe
psteinroe deleted the fix/cli-format-options branch October 26, 2026 10:13
psteinroe added a commit that referenced this pull request Oct 26, 2026
…ace (#934)

Stacked on #917.

Comments written before a keyword that has no AST node of its own (`ELSE`, `THEN`, `END`, GRANT's `ON`/`TO`/`FROM`) moved past the keyword, sometimes into another branch or clause. The output was stable on reformat, so nothing flagged it, but the comment no longer sat where it was written. Finding P17 of the release review.

```sql
select case -- why
when a then 'x' /* inline */ else 'y' end from t;
-- before
select
  case
    when a then 'x'
    else /* inline */ 'y' -- why
  end
from
  t;
-- after
select
  case
    -- why
    when a then 'x' /* inline */
    else 'y'
  end
from
  t;

select case when a /* cond */ then 1 else 2 /* last */ end from t;
-- before
select case when a then /* cond */ 1 else 2 end from /* last */ t;
-- after
select case when a /* cond */ then 1 else 2 /* last */ end from t;

grant select -- c
on t to u;
-- before
grant select
  on table -- c
  t
  to u;
-- after
grant select -- c
  on table t
  to u;
```

**Why they moved**

Comments are attached to AST nodes by source position, and a comment that leads goes to the next node: a block comment, an own-line comment, or a comment after a clause keyword. For `'x' /* inline */ else 'y'` the next node is `'y'`, so the comment came out after `else`. A comment right after `case` didn't count as following a clause keyword, so it trailed the whole CASE and came out after the last branch. A comment before `end` led the next node after the CASE, in the FROM clause.

**What changed**

- When the token after a comment is a keyword that no node starts at, the comment remembers that keyword (`before_keyword`). A parent that writes such a keyword before a child can take the comments written before it, with `emit_comments_before_keyword`, and emit them before the keyword. They are emitted like the trailing comments of the previous node, so an own-line comment keeps its own line. Only comments recorded for that exact keyword are taken. A parent that doesn't call it keeps today's placement, so nothing else moves.
- CASE takes them before `ELSE` and `THEN`. GRANT/REVOKE takes them before `ON` and `TO`/`FROM`, and GRANT role before `TO`/`FROM`.
- A comment before `END` stays with the last branch: it trails the previous node instead of leading the next one, because nothing inside the CASE follows `END`.
- `CASE` counts as a clause keyword, so a line comment right after it goes before the first `WHEN`.
- The renderer collapses two spaces in a row inside a group that stays on one line, as it already did for broken groups. The comment emission otherwise gave `a /* cond */  then`.

**Tests**

- New tests in `comment_layout.rs` cover a comment before `ELSE`, `THEN` and `END`, after `CASE`, and before GRANT's `ON` and `TO`, in fit and expanded layout, each formatted twice to check idempotence.
- Two unit tests in `comments.rs` cover the attachment.
- No snapshot changes: the snapshot harness renders without comments.
- The option matrix from #912 passes over the fixtures and the regression suite.

**Not covered**

Other keyword-only positions (`ON` in a join, `AS` before an alias, `FROM` in a SELECT, ...) keep the old placement. They can opt in the same way when needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant