nixfmt_rs 0.5.1

Rust implementation of nixfmt with exact Haskell compatibility
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Formatted-output regression tests
//!
//! Minimal reproducers for divergences from the reference `nixfmt` (v1.2.0)
//! discovered by the `diff_sweep` example over `nixpkgs/pkgs/`. Expected
//! output is committed as `insta` snapshots; each test names the
//! corresponding Haskell function in `Nixfmt.Pretty` / `Nixfmt.Predoc`.

use crate::{test_format, test_ir_format};

/// Layout: `Trailing` text must be dropped when a group is rendered compact.
/// Haskell: `Nixfmt.Predoc.fits` skips `Text Trailing`.
/// Fixture: fast reproducer for `tests/fixtures/nixfmt/diff/lambda/`.
#[test]
fn format_trailing_comma_compact_param_set() {
    test_format!("{ a, b }: a");
    test_format!("{\n  a,\n  b,\n}: a");
}

/// `moveTrailingCommentUp` must be applied to the opening `{` of a
/// `SetParameter` so that `{ /*c*/ a, b }: x` lifts the comment above the
/// brace and the parameter set stays single-line. Haskell: `Pretty.hs`
/// `instance Pretty Parameter`.
#[test]
fn format_set_param_open_trailing_comment_hoisted() {
    test_format!("{ /*c*/ a, b, }: x");
    test_format!("{ /*c*/ }: x");
    test_ir_format!("{ /*c*/ a, b, }: x");
}

/// Trailing comments must not count toward line width in `fits`; the
/// binding body stays on the `=` line. The second case checks that a
/// genuinely over-wide *non-comment* RHS still wraps.
/// Haskell: `Nixfmt.Predoc.fits` (`Text TrailingComment` arm).
#[test]
fn format_trailing_comment_ignored_for_width() {
    // From nixpkgs melpa.nix: well under 100 columns, must not wrap.
    test_format!("{\n  unstableVersionInNixFormat = parsed != null; # heuristics\n}\n");
    // Short binding + long trailing comment: still must not wrap.
    test_format!(concat!(
        "{\n  x = a != null; ",
        "# a trailing comment that is quite a bit longer than the binding itself but still irrelevant\n",
        "}\n",
    ));
    // List element on its own line: `ni` must reflect the indent step so
    // `fits` does not inject a second space before the comment.
    test_format!("[\n  a # c\n  b\n]\n");
    // Non-comment part alone exceeds 100 cols → must wrap regardless of comment.
    test_format!(concat!(
        "{\n  someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName = ",
        "aaaaaaaa != bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb; # c\n}\n",
    ));
}

/// A trailing comment after a one-char token must be shifted one column so
/// the lexer still classifies it as trailing on reparse (idempotency).
/// Haskell: `Nixfmt.Predoc.goOne` `TrailingComment` guard.
#[test]
fn format_trailing_comment_shift_for_idempotency() {
    test_format!("{ a # b\n= 1; }");
    test_format!("[ # c\n1\n]");
    test_format!("{ b # a\n? # a\nnull\n,}: b");
}

/// `f (x: { ... })` should absorb the parenthesised abstraction onto the
/// function line. Haskell: `Nixfmt.Pretty.absorbLast` / `isAbsorbableExpr`.
#[test]
fn format_paren_abstraction_absorbed_as_last_arg() {
    test_ir_format!("f (finalAttrs: {\n  x = 1;\n  y = 2;\n})");
}

/// A lambda chain whose body is a non-absorbable application must stay on
/// one line when the whole group fits the target width *ignoring leading
/// indentation*, matching Haskell `Nixfmt.Predoc.goGroup` (cc == 0 branch)
/// which calls `fits` with `tw - firstLineWidth rest` and does **not**
/// subtract the pending indent.
#[test]
fn format_lambda_chain_stays_one_line_when_fits() {
    // Reduced from nixpkgs `lib-override-helper.nix`.
    test_format!(
        "{\n  addPackageRequires =\n    pkg: packageRequires: addPackageRequiresWhen pkg packageRequires (finalAttrs: previousAttrs: true);\n}",
    );
    // Reduced from `cask/package.nix`: single-param lambda with string body.
    test_format!(
        "{\n  formatLoadPath =\n    loadPathItem: \"-L ${\n      if builtins.isString loadPathItem then loadPathItem else \"${loadPathItem}/share/emacs/site-lisp\"\n    }\";\n}",
    );
}

/// Nested simple lambda parameters should stay on one line before an
/// expanded body. Haskell: `Nixfmt.Pretty.absorbAbs`.
#[test]
fn format_nested_lambda_body_absorbed() {
    test_ir_format!("final: prev: {\n  a = 1;\n  b = 2;\n}");
}

/// `with X;` followed by an attrset should keep the `{` on the same line,
/// both as a lambda body and as an assignment RHS.
/// Haskell: `Nixfmt.Pretty` `instance Pretty Expression` (With) / `absorbRHS`.
/// Fixture: fast reproducer for `tests/fixtures/nixfmt/diff/with/`.
#[test]
fn format_with_body_absorbed() {
    test_ir_format!("self: with self; {\n  a = 1;\n  b = 2;\n}");
    test_ir_format!("{\n  meta = with lib; {\n    license = mit;\n  };\n}");
}

/// `x = f \"a\" ''..'';` should keep the application on the `=` line when the
/// last argument is an absorbable multiline string.
/// Haskell: `Nixfmt.Pretty.absorbRHS` (Application case).
#[test]
fn format_assignment_rhs_app_with_string_last_arg() {
    test_ir_format!("{\n  w = writeShellScript \"n\" ''\n    echo a\n    echo b\n  '';\n}");
}

/// Chained `if ... else if ...` must always be expanded onto multiple lines.
/// Haskell: `Nixfmt.Pretty.prettyIf` emits hardlines between branches.
/// Fixture: fast reproducer for `tests/fixtures/nixfmt/diff/if_else/`.
#[test]
fn format_if_elseif_chain_forced_multiline() {
    test_ir_format!("{ x = if a then \"x\" else if b then \"y\" else \"z\"; }");
}

/// Multi-argument application that expands should keep the first argument on
/// the function line and indent continuation arguments by two spaces.
/// Haskell: `Nixfmt.Pretty.prettyApp`.
#[test]
fn format_multi_arg_application_continuation_indent() {
    test_ir_format!("runCommand \"n\"\n  {\n    a = 1;\n  }\n  ''\n    echo a\n  ''");
}

/// A line comment before the last argument forces expansion; the comment and
/// the argument must still be indented under the function head.
/// Haskell: `Nixfmt.Pretty.prettyApp` / `absorbLast` (`absorbParen` keeps the
/// argument's pre-trivia inside the same `nest` as the function chain).
#[test]
fn format_app_comment_before_last_arg_indent() {
    test_format!("(map toString\n  # comment\n  (builtins.filter f version))");
    test_format!(
        "{\n  v = lib.concat \".\" (\n    map toString\n      # comment\n      (builtins.filter f version)\n  );\n}",
    );
}

/// `//` / `++` / `+` on the RHS of an assignment must absorb onto the `=` line
/// when the LHS is an absorbable term and there is no leading trivia.
/// Haskell: `Nixfmt.Pretty.absorbRHS` Operation Case 1 / `prettyOp True`.
/// Fixture: fast reproducer for `tests/fixtures/nixfmt/diff/operation/`.
#[test]
fn format_assignment_rhs_update_concat_plus_case1() {
    test_format!("{ meta = oldAttrs.meta // { description = \"x\"; }; }");
    test_ir_format!("{ meta = oldAttrs.meta // { description = \"x\"; }; }");
    test_format!("{ x = a.b or [ ] ++ [ y ]; }");
    test_ir_format!("{ x = a.b or [ ] ++ [ y ]; }");
    test_format!("{ x = lib.optionals a [ p ] ++ lib.optionals b [ q ]; }");
    // Leading trivia on the LHS term disables Case 1 and falls through to Case 2.
    test_ir_format!("{ x = /* c */ [ a ] ++ [ b ]; }");
    test_ir_format!("{ x = [ a ] ++ [ b ] ++ [ c ]; }");
}

/// `//` with an absorbable RHS term and a non-absorbable LHS keeps `// {` on
/// the LHS line and only expands the attrset.
/// Haskell: `Nixfmt.Pretty.absorbRHS` Operation Case 2.
/// Fixture: fast reproducer for `tests/fixtures/nixfmt/diff/attr_set/`.
#[test]
fn format_assignment_rhs_update_concat_plus_case2() {
    test_format!("{ x = a // { y = 1;\n z = 2;\n}; }");
    test_ir_format!("{ x = a // { y = 1;\n z = 2;\n}; }");
}

/// Layout: when the priority-expansion of one argument fails because the
/// remaining arguments cannot be rendered compactly, the layout state must be
/// restored before falling through to full expansion. Otherwise the indent
/// stack is poisoned and continuation arguments lose their 2-space indent.
/// Haskell: `Nixfmt.Predoc.layoutGreedy` (`goPriorityGroup` via `StateT _ Maybe`).
#[test]
fn format_app_multi_absorbable_args_indent() {
    // sddm/default.nix: `runCommand "name" { ... } ''script''`
    test_format!("runCommand \"n\"\n  {\n    a = 1;\n  }\n  ''\n    echo a\n  ''");
    // android-studio/common.nix: app + attrset arg + parenthesised last arg
    test_format!("f\n  {\n    a = 1;\n  }\n  (\n    b: c\n  )");
}

/// Layout: with the application nested inside a binding, the `{` must stay on
/// the function line and the body indented relative to the binding.
/// Haskell: `Nixfmt.Predoc.layoutGreedy` priority-group fallback.
#[test]
fn format_app_set_absorb_in_binding() {
    // wrapper.nix style: `x = mk { ... } ''..'';`
    test_format!("{\n  x = mk {\n    a = 1;\n  } ''\n    echo a\n  '';\n}");
}

/// A lone interpolation on an indented-string line whose body is a function
/// application must keep `${` on the string line and absorb the call body
/// (no `linebreak` between `${` and the application).
/// Haskell: `Nixfmt.Pretty.instance Pretty [StringPart]` Application arm.
#[test]
fn format_interp_lone_application_absorbed() {
    // 7zip-zstd: non-simple application (parenthesised arg / >2 args).
    test_format!("''\n  ${lib.optionalString (!isWindows) ''\n    one\n    two\n  ''}\n''");
    test_format!("''\n  ${lib.optionalString a b ''\n    one\n    two\n  ''}\n''");
    // emacs wrapper: simple application; IR must place `${` inside the group
    // and nest the call body one level deeper.
    test_ir_format!("''\n  ${lib.optionalString cond ''\n    one\n    two\n  ''}\n''");
    // With leading whitespace and a wide body that overflows the budget.
    test_format!(concat!(
        "''\n  x\n    ${lib.optionalString cond ''\n",
        "      linkPath one two three four five six seven eight nine ten eleven twelve thirteen fourteen\n",
        "    ''}\n''",
    ));
    // vscodeWithConfiguration: application whose middle arg is a parenthesised
    // abstraction that itself wraps; `${` must still hug the call head.
    test_format!(concat!(
        "''\n  ${lib.concatMapStringsSep \"n\" (\n",
        "    e: \"ln -sfn ${e}/share/vscode/extensions/aaaa/bbb/ccc/dddd\"\n",
        "  ) nixExtsDrvs}\n''",
    ));
}

/// A short single-line interpolation that is not the only thing on the
/// string line must stay inline (forced compact up to 30 columns) even when
/// the surrounding line already overflows.
/// Haskell: `Nixfmt.Pretty.instance Pretty StringPart` (`unexpandSpacing' (Just 30)`).
#[test]
fn format_interp_inline_short_forced_compact() {
    // ms-vscode.cpptools: `${lib.makeBinPath [ gdb ]}` after a long line.
    test_format!(concat!(
        "''\n  wrap a/very/long/share/vscode/extensions/ms-vscode.cpptools/debug/bin/OpenDebugAD7 ",
        "--prefix PATH : ${lib.makeBinPath [ gdb ]}\n''",
    ));
    test_ir_format!("''\n  prefix ${lib.makeBinPath [ gdb ]} suffix\n''");
}

/// `<` lexes as a search path only when the full Nix SPATH pattern
/// `<{PATH_CHAR}+(/{PATH_CHAR}+)*>` matches; otherwise it is `Less`.
/// <https://github.com/Mic92/nixfmt-rs/issues/88>
#[test]
fn format_less_than_without_space_not_env_path() {
    test_format!("let a = 1; b = 2; in a<b");
    // `a<b>c` is `a <b> c` (SPATH wins), not a comparison chain.
    test_format!("a<b>c");
    test_format!("<foo+bar/baz.nix>");
    test_format!("<.foo>");
}

/// Non-chainable comparison operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) use
/// `softline` before the operator and `hardspace` after, with no extra `nest`
/// on the RHS, so a short RHS stays on the LHS's last line even when the LHS
/// is multi-line.
/// Haskell: `Nixfmt.Pretty.instance Pretty Expression` `Operation` comparison arm.
#[test]
fn format_comparison_op_softline() {
    // fetchgithub: multi-line application LHS, short identifier RHS.
    test_format!(concat!(
        "{\n  x =\n    f (\n      a:\n      if cond ",
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa then\n",
        "        a\n      else\n        b\n    ) y != y;\n}",
    ));
    // nixops: comment as preTrivia on the RHS must not be nested under the op.
    test_format!("{\n  x =\n    a ==\n    # note\n    b.c (d: e);\n}");
    test_ir_format!("a == b");
    // dev-shell-tools: LHS is `//` chain, RHS is an attrset.
    test_format!("a // { b = 1; } == { c = 1; }");
}

/// A binding whose LHS is long or uses a non-ID selector (string /
/// interpolation key) gets a `linebreak` before the RHS so the value can move to
/// its own indented line when the whole binding overflows.
/// Haskell: `Nixfmt.Pretty.instance Pretty Binder` `Assignment` `rhs` guard.
#[test]
fn format_assignment_non_simple_selector_breaks_rhs() {
    test_format!(concat!(
        "{\n  \"PKG_CONFIG_GIMP_${pkgConfigMajorVersion}_0_GIMPLIBDIR\" = ",
        "\"${placeholder \"out\"}/${gimp.targetLibDir}\";\n}",
    ));
    // >4 selectors also triggers the break even when each is a plain id.
    test_format!(concat!(
        "{\n  a.b.c.d.e = \"",
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\";\n}",
    ));
}

/// Comments after the top-level expression must be preserved.
/// Haskell: `Nixfmt.Parser.file` attaches trailing trivia to `Trailed`.
#[test]
fn format_trailing_file_trivia_preserved() {
    test_format!("{ a = 1; }\n# trailing\n");
    test_format!("1\n/* block */\n");
    test_ir_format!("{ a = 1; }\n# trailing\n");
}

/// A comment between the interpolation body and `}` must be preserved as the
/// `Trailed`'s trailing trivia and force the `${ … }` onto multiple lines.
/// Haskell: `Nixfmt.Parser.interpolation` (`whole expression`).
#[test]
fn format_interp_trailing_trivia_preserved() {
    test_format!("''\n  ${ f a b\n  # c\n  }\n''");
    test_format!("\"${ x\n# c\n}\"");
}

/// A comment between `${` and the interpolation body must be preserved as
/// leading trivia of the body's first token.
/// Haskell: `Nixfmt.Lexer.lexeme` (no special-cased trailing slot for `${`).
#[test]
fn format_interp_leading_trivia_preserved() {
    test_format!("''\n  ${ # leading\n  x }\n''");
    test_format!("\"${ /* c */ x}\"");
    // Selector interpolations go through `lexeme()` for `${`, so the comment
    // is first classified as its `trail_comment` and must be re-queued.
    test_format!("{ ${ # c\nx } = 1; }");
    test_format!("a.${ # c\nx }");
}

/// Layout width must count Unicode scalars, not UTF-8 bytes, so multi-byte
/// glyphs like `«»` don't push a line over the 100-column budget.
/// Haskell: `Nixfmt.Predoc.textWidth` = `Text.length`.
/// Reproduces: nixpkgs `pkgs/stdenv/generic/check-meta.nix`.
#[test]
fn format_text_width_counts_chars_not_bytes() {
    test_format!(
        "{\n  getName =\n    attrs: attrs.name or \"${attrs.pname or \"\u{ab}name-missing\u{bb}\"}-${attrs.version or \"\u{ab}version-missing\u{bb}\"}\";\n}\n",
    );
}

/// An empty set whose `{` carries pre-trivia (so the `LoneAnn` fast path is
/// skipped) must still honour the source line break between `{` and `}`.
/// Haskell: `Nixfmt.Pretty.prettySet` second clause, `sep` condition.
/// Reproduces: nixpkgs `nixos/modules/system/service/systemd/user.nix`.
#[test]
fn format_empty_set_with_pretrivia_keeps_linebreak() {
    test_format!("# c\n{\n}\n");
}

/// `Vec<StringPart>::pretty` second `[Interpolation(_)]` arm: an `''…''` line
/// that is exactly one `${…}` whose `Trailed` carries trailing trivia (the `# c`
/// before `}`). The first arm only matches when that trivia is empty.
#[test]
fn format_indented_string_lone_interpolation_with_trailing_trivia() {
    test_format!("''\n${x\n# c\n}\n''\n");
    test_format!("''\n  ${ x\n  # multi\n  # line\n  }\n''\n");
}

/// `/*nixfmt:disable*/`…`/*nixfmt:enable*/` keeps the region byte-for-byte;
/// the parser still tokenises it, the renderer just splices the original
/// source over the formatted output.
/// Fixture: `tests/fixtures/nixfmt/diff/directive_*`.
#[test]
fn format_directive_disabled_region_verbatim() {
    test_format!(
        "{\n  a   =   1;\n/*nixfmt:disable*/\n  b    =    2;\n/*nixfmt:enable*/\n  c   =   3;\n}\n"
    );
}

/// Mid-line directive is not on its own line, so it parses as a regular block
/// comment and demotes to `# nixfmt:disable`.
#[test]
fn format_directive_mid_line_demotes_to_line_comment() {
    test_format!("{\n  a = 1; /*nixfmt:disable*/\n  b   =   2;\n}\n");
}

/// Lone `/*nixfmt:enable*/` passes through unchanged so re-formatting is
/// idempotent.
#[test]
fn format_directive_lone_enable_passes_through() {
    test_format!("{\n  a   =   1;\n/*nixfmt:enable*/\n  b   =   2;\n}\n");
}

/// Unclosed disable swallows the closing `}`. Still parses (the region is
/// tokenised normally); only the output is verbatim.
#[test]
fn format_directive_region_crosses_brace() {
    test_format!("{\n  foo   =   1;\n/*nixfmt:disable*/\n  bar    =    2;\n}\n");
}

/// `/*nixfmt:enable*/` inside a string literal in the disabled region must
/// not close it; the parser never runs the trivia path in string bodies.
#[test]
fn format_directive_enable_in_string_does_not_close() {
    test_format!(
        "{\n/*nixfmt:disable*/\n  a = ''\n/*nixfmt:enable*/\n  '';\n/*nixfmt:enable*/\n  b   =   1;\n}\n"
    );
}

/// Unclosed disable inside a `${}` would partially overlap the `''…''` body
/// and make its indent stripping drift by `indent_width` per pass; such a
/// directive is demoted to a plain comment. Found by `fuzz_idempotent`.
#[test]
fn format_directive_unclosed_in_interpolation_demoted() {
    test_format!("{\nx =''/n\"${\n/*nixfmt:disable*/\n   2\n}\";\nv  '';\n}\n");
}

/// Suppression must hold across `try_render_group`'s `fits` write path and
/// the directive cursor must roll back on a failed priority-group attempt.
/// Found by `fuzz_idempotent`.
#[test]
fn format_directive_in_priority_group_post_segment() {
    test_format!("z{\n/*nixfmt:disable*/\n}c");
}

/// `moveTrailingCommentUp` must be applied to chained binary operators
/// (`+`, `++`, `//`) so that a trailing comment on the operator is rendered
/// as a line comment *before* the operator line, matching Haskell nixfmt.
/// Found via `diff_sweep` on nixpkgs (issue #88).
#[test]
fn format_operator_trailing_comment_hoisted() {
    test_format!("\"a\" +  # comment\n\"b\"");
    test_format!("x ++ # comment\ny");
    test_format!("a // # comment\nb");
}

/// Hoisting the head's trail comment must not pull a `/* lang */` annotation
/// off the string it labels, or pass 2 demotes it to `# c`. Found by
/// `fuzz_idempotent`.
#[test]
fn format_language_annotation_with_trail_comment() {
    test_format!("/*c*/''''#}\nA");
}