Formatter#

nupp fmt              # list what is unformatted, project-wide
nupp fmt --write      # rewrite in place
nupp fmt --check      # report only
nupp fmt src/x.nupp   # format one file to stdout

The style is fixed. There is no configuration file and no editor setting. nupp fmt otherwise takes options about what to do with the result, plus two knobs over the style itself, described below.

Three modes#

Naming files formats each to stdout, whether or not it changed. A filter that sometimes emits nothing is a filter that sometimes empties a file.

--write rewrites in place, lists what it changed, and exits 0.

--check, or naming no files, or --json lists the files that are not formatted, writes nothing, and exits 1 if the list is non-empty. With no files the subject is every .nupp and .d.nupp under the manifest's include roots, minus build output.

--write and --check together is a usage error: one fixes the formatting and the other reports it.

--json separates a file that cannot be formatted from one that merely is not:

{"ok": true, "unformatted": ["src/a.nupp"], "written": 0, "failed": []}

What it does#

 Setting              Value
 ───────────────────  ──────────
 Indent               4 spaces
 Code width           120 columns (--width)
 Docblock text width  88 columns

Ordinary line breaks are soft: a short expression or delimited group is put on one line. Statements, comments, docblocks, blank lines, and block closers are structural boundaries. An over-long group breaks one element per line; with no group to break, and, or, .., and the arithmetic operators are the break points. An if is always broken across lines, however short it is, and an ordinary function body always has its own lines. || -> is the one-line function spelling.

Blank runs collapse to one, leading blanks are stripped, trailing whitespace goes, and a file ends with exactly one newline.

Docblocks are reflowed and set off with a blank line. @tag lines are recognized, continuations indent by five spaces, and fenced or indented verbatim blocks are left alone.

@!nofmt is a leading inner annotation that leaves one whole file untouched:

It must be the file's first annotation and has no region form.

A method call left in its sugar form gets its parentheses back:

A plain call is not a method call and keeps its sugar — f{...} and f"..." are how the language spells a record constructor, and touching those would be a much bigger, noisier rewrite than "give a method its parens back." Pass --no-method-parens to turn this off and leave every call exactly as written, or set it once for the whole project in nupp.lua:

return {
    fmt = { methodParens = false },
}

--no-method-parens wins if both are given; there is no flag to force parens back on over a manifest that turned them off.

--width N changes the code column past which a line breaks, at least 20. The default, 120, is unchanged from before this was a flag; docblock text keeps wrapping at 88 columns regardless of --width.

What it will not do#

The formatter guarantees that its output re-lexes to a token sequence identical in kind and text to the input. When a rewrite would break that, it returns the input untouched and reports NUPP4001.

That invariant is why it cannot change a quote style, add or remove a table constructor's trailing comma, or rewrite a numeric literal — not as a policy decision, but because those change the token stream.

Two rewrites are exempted, each proven safe rather than merely whitespace. A single-value annotation loses its redundant member = where the checker has proved the two spellings equivalent:

Those tokens are marked for omission and excluded from the fingerprint. The final comma in a type shape is likewise redundant and is omitted. Parenthesizing a method call's sugar-form arguments, described above, is the other rewrite: the inserted tokens are folded into the sequence checked against instead, so a bug that put a paren in the wrong place would still be caught.

Idempotence#

fmt(fmt(x)) == fmt(x), asserted over the whole test corpus. Internally the formatter iterates to a fixpoint, capped at 24 passes.

In an editor#

The language server implements textDocument/formatting and textDocument/rangeFormatting with the same formatter. Two details matter:

  • A file with parse errors produces no edits. A file that does not parse is left exactly as it is.
  • Edits are emitted per run of changed lines rather than as one whole-document replacement, which preserves cursors and folds.

Range formatting formats the whole document and then keeps the edits that fall inside the requested range.