Formatting
nio format rewrites Nio source in one canonical style. It takes no options,
and that is the whole point of it: a style nobody configures is a style nobody
argues about, and every diff in a project is a change to the program rather
than to somebody’s preference.
nio format . # rewrite every .nio file under here
nio format src/main.nio # or just these files
nio format -l . # name the files that are not formatted,
# change nothing, and exit 1 if anyA directory means every .nio file under it, .git and node_modules
excepted. -l is the form for a continuous-integration check: it writes
nothing, prints the paths that would change, and exits 1 when the list is not
empty.
A file that does not parse is reported and left exactly as it was. The formatter prints a tree, so it can only work from a tree it actually got.
What it decides, and what it leaves alone
The formatter settles the indent (four spaces a level), the space around every token, how many blank lines separate two statements (one, wherever the file had one or more), and the column a trailing comment sits at.
It does not decide where a construct breaks over lines. That stays the file’s decision, and it is kept as the file made it:
// Both of these are formatted. Neither becomes the other.
if (ready) { start(); }
if (ready) {
start();
}The same holds for an argument list, a literal, an operator chain and an
else chain. If you wrote it on one line it stays on one line; if you broke
it, it stays broken in the same places, re-indented. There is no line-length
limit, because a limit would have to move breaks the author chose.
Two things follow from a construct being broken. A literal whose closing bracket ends up on a line of its own keeps it there and gets a comma after its last element, so that adding another element touches one line:
byte[] const HEADER = [
0x89, 0x50, 0x4e, 0x47,
0x0d, 0x0a, 0x1a, 0x0a,
];And an operator that opens a line reads at the front of it, under the operand it continues:
return Error("pins is empty, so no peer can match it; leave it null to "
+ "verify without pinning", ErrorCode.MISCONFIGURED);Comments keep their place. Prose above a declaration stays above it, a remark trailing code stays on that line, and a run of trailing comments on neighbouring lines lines up on the longest of them:
type Unit {
String path; // resolved file path
String dir; // directory imports inside this file resolve against
String qual; // unique symbol prefix ("" for the entry module)
}What it will not change
Formatting changes whitespace. It does not change a single token, and the test
suite holds it to that over every .nio file in the Nio repository: the file
is lexed before and after, and the two token streams must match.
That is stronger than it sounds, and it rules out the mistakes a formatter is
otherwise prone to. Grouping parens are written back exactly where the file
put them, rather than re-derived from a precedence table — so (a && b) || c
keeps its parens even though it does not need them. A string is written back
with the delimiter it was written with, so a backtick string holding a program
is not requoted into " with \n escapes. A number keeps its spelling, 0x1f
included. A union is written back as a union, though the parser has by
then turned it into a sealed type and one extender per member.
The one thing the formatter adds is punctuation the grammar lets you leave
out: the ; after a statement that ends in a brace, the ; after the last
field of a body, and the , after the last element of a broken list. The
standard style always writes those.
Formatting twice
Formatting an already-formatted file changes nothing. That is not an aspiration but a test, run over the whole repository on every build, together with the requirement that every file in it is already formatted. The repository is the formatter’s own corpus.