Skip to content

Commit bd28847

Browse files
authored
Merge pull request #307: Tweak operation absorbtion (ex. Unindent Concatenation)
2 parents af5529a + aed61db commit bd28847

File tree

17 files changed

+999
-655
lines changed

17 files changed

+999
-655
lines changed

src/Nixfmt/Parser.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,16 @@ opCombiner (Op Postfix TQuestion) =
487487
<$> operator TQuestion
488488
<*> selectorPath
489489
opCombiner (Op Postfix _) = undefined
490+
-- HACK: We parse TPlus as left-associative, but convert it to right-associative in the AST
491+
-- This is allowed because addition is fully associative
492+
-- This is necessary because some downstream formatting code needs to match on the first operand,
493+
-- and doing that with a left-associative chain is not possible.
494+
-- A proper solution would be to flatten all associative operators into a list, but that would be a lot harder to do
495+
opCombiner (Op InfixL TPlus) = MPExpr.InfixL $ flip mkOp <$> operator TPlus
496+
where
497+
mkOp :: Expression -> Leaf -> Expression -> Expression
498+
mkOp (Operation one op1 two) op2 three | op1 == op2 = Operation one op1 (Operation two op2 three)
499+
mkOp left op right = Operation left op right
490500
opCombiner (Op InfixL tok) = MPExpr.InfixL $ flip Operation <$> operator tok
491501
opCombiner (Op InfixN tok) = MPExpr.InfixN $ flip Operation <$> operator tok
492502
opCombiner (Op InfixR tok) = MPExpr.InfixR $ flip Operation <$> operator tok

src/Nixfmt/Pretty.hs

Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ import Nixfmt.Types (
5656
Trivium (..),
5757
Whole (..),
5858
ann,
59+
hasPreTrivia,
5960
hasTrivia,
6061
mapFirstToken,
6162
mapFirstToken',
6263
mapLastToken',
64+
matchFirstToken,
6365
tokenText,
6466
)
6567
import Nixfmt.Util (isSpaces)
@@ -151,7 +153,7 @@ instance Pretty Binder where
151153
pretty (Assignment selectors assign expr semicolon) =
152154
group $
153155
hcat selectors
154-
<> nest (hardspace <> pretty assign <> nest rhs)
156+
<> nest (hardspace <> pretty assign <> rhs)
155157
<> pretty semicolon
156158
where
157159
rhs =
@@ -262,7 +264,7 @@ instance Pretty ParamAttr where
262264
group $
263265
pretty name
264266
<> hardspace
265-
<> nest (pretty qmark <> nest (absorbRHS def))
267+
<> nest (pretty qmark <> absorbRHS def)
266268
<> pretty maybeComma
267269
-- `...`
268270
pretty (ParamEllipsis ellipsis) =
@@ -500,6 +502,34 @@ prettyApp indentFunction pre hasPost f a =
500502
(\fRendered -> group' RegularG $ fRendered <> line <> absorbLast a <> post)
501503
<> (if hasPost && not (null comment') then hardline else mempty)
502504

505+
prettyOp :: Bool -> Expression -> Leaf -> Doc
506+
prettyOp forceFirstTermWide operation op =
507+
let -- Walk the operation tree and put a list of things on the same level.
508+
-- We still need to keep the operators around because they might have comments attached to them.
509+
-- An operator is put together with its succeeding expression. Only the first operand has none.
510+
flatten :: Maybe Leaf -> Expression -> [(Maybe Leaf, Expression)]
511+
flatten opL (Operation a opR b) | opR == op = flatten opL a ++ flatten (Just opR) b
512+
flatten opL x = [(opL, x)]
513+
514+
-- Called on every operand except the first one (a.k.a. RHS)
515+
absorbOperation :: Expression -> Doc
516+
absorbOperation (Term t) | isAbsorbable t = hardspace <> pretty t
517+
-- Force nested operations to start on a new line
518+
absorbOperation x@(Operation{}) = group' RegularG $ line <> pretty x
519+
-- Force applications to start on a new line if more than the last argument is multiline
520+
absorbOperation (Application f a) = group $ prettyApp False line False f a
521+
absorbOperation x = hardspace <> pretty x
522+
523+
prettyOperation :: (Maybe Leaf, Expression) -> Doc
524+
-- First element
525+
prettyOperation (Nothing, Term t) | isAbsorbableTerm t && forceFirstTermWide = prettyTermWide t
526+
prettyOperation (Nothing, expr) = pretty expr
527+
-- The others
528+
prettyOperation (Just op', expr) =
529+
line <> pretty (moveTrailingCommentUp op') <> nest (absorbOperation expr)
530+
in group' RegularG $
531+
(concatMap prettyOperation . flatten Nothing) operation
532+
503533
prettyWith :: Bool -> Expression -> Doc
504534
-- absorb the body
505535
prettyWith True (With with expr0 semicolon (Term expr1)) =
@@ -588,50 +618,48 @@ absorbRHS expr = case expr of
588618
-- Exception to the case below: Don't force-expand attrsets if they only contain a single inherit statement
589619
(Term (Set _ _ binders _))
590620
| case unItems binders of [Item (Inherit{})] -> True; _ -> False ->
591-
hardspace <> group (absorbExpr False expr)
621+
nest $ hardspace <> group (absorbExpr False expr)
592622
-- Absorbable expression. Always start on the same line, and force-expand attrsets
593-
_ | isAbsorbableExpr expr -> hardspace <> group (absorbExpr True expr)
623+
_ | isAbsorbableExpr expr -> nest $ hardspace <> group (absorbExpr True expr)
594624
-- Parenthesized expression. Same thing as the special case for parenthesized last argument in function calls.
595-
(Term (Parenthesized open expr' close)) -> hardspace <> absorbParen open expr' close
625+
(Term (Parenthesized open expr' close)) -> nest $ hardspace <> absorbParen open expr' close
596626
-- Not all strings are absorbable, but in this case we always want to keep them attached.
597627
-- Because there's nothing to gain from having them start on a new line.
598-
(Term (SimpleString _)) -> hardspace <> group expr
599-
(Term (IndentedString _)) -> hardspace <> group expr
628+
(Term (SimpleString _)) -> nest $ hardspace <> group expr
629+
(Term (IndentedString _)) -> nest $ hardspace <> group expr
600630
-- Same for path
601-
(Term (Path _)) -> hardspace <> group expr
631+
(Term (Path _)) -> nest $ hardspace <> group expr
602632
-- Non-absorbable term
603633
-- If it is multi-line, force it to start on a new line with indentation
604-
(Term _) -> group' RegularG (line <> pretty expr)
634+
(Term _) -> nest $ group' RegularG (line <> pretty expr)
605635
-- Function call
606636
-- Absorb if all arguments except the last fit into the line, start on new line otherwise
607-
(Application f a) -> prettyApp False line False f a
608-
(With{}) -> group' RegularG $ line <> pretty expr
609-
-- Special case `//` and `++` operations to be more compact in some cases
610-
-- Case 1: two arguments, LHS is absorbable term, RHS fits onto the last line
611-
(Operation (Term t) (LoneAnn op) b)
612-
| isAbsorbable t
613-
&& isUpdateOrConcat op
614-
-- Exclude further operations on the RHS
615-
-- Hotfix for https://github.com/NixOS/nixfmt/issues/198
616-
&& case b of (Operation{}) -> False; _ -> True ->
617-
group' RegularG $ line <> group' Priority (prettyTermWide t) <> line <> pretty op <> hardspace <> pretty b
618-
-- Case 2a: LHS fits onto first line, RHS is an absorbable term
637+
(Application f a) -> nest $ prettyApp False line False f a
638+
(With{}) -> nest $ group' RegularG $ line <> pretty expr
639+
-- Special case `//` and `++` and `+` operations to be more compact in some cases
640+
-- The following code assumes all of these operators are parsed with right-handed associativity
641+
-- (even though in Nix, addition technically is considered left-associative)
642+
-- Case 1: LHS is absorbable term, unindent concatenations
643+
-- https://github.com/NixOS/nixfmt/issues/228
644+
(Operation (Term t) op@(Ann{value}) _)
645+
| isAbsorbableTerm t
646+
&& matchFirstToken (not . hasPreTrivia) t
647+
&& isUpdateConcatPlus value ->
648+
hardspace <> prettyOp True expr op
649+
-- Case 2: LHS fits onto first line, RHS is an absorbable term
619650
(Operation l (LoneAnn op) (Term t))
620-
| isAbsorbable t && isUpdateOrConcat op ->
621-
group' RegularG $ line <> pretty l <> line <> group' Transparent (pretty op <> hardspace <> group' Priority (prettyTermWide t))
622-
-- Case 2b: LHS fits onto first line, RHS is a function application
623-
(Operation l (LoneAnn op) (Application f a))
624-
| isUpdateOrConcat op ->
625-
line <> group l <> line <> prettyApp False (pretty op <> hardspace) False f a
651+
| isAbsorbable t && isUpdateConcatPlus op ->
652+
nest $ group' RegularG $ line <> pretty l <> line <> group' Transparent (pretty op <> hardspace <> group' Priority (prettyTermWide t))
626653
-- Everything else:
627654
-- If it fits on one line, it fits
628655
-- If it fits on one line but with a newline after the `=`, it fits (including semicolon)
629656
-- Otherwise, start on new line, expand fully (including the semicolon)
630-
_ -> line <> group expr
657+
_ -> nest $ line <> group expr
631658
where
632-
isUpdateOrConcat TUpdate = True
633-
isUpdateOrConcat TConcat = True
634-
isUpdateOrConcat _ = False
659+
isUpdateConcatPlus TUpdate = True
660+
isUpdateConcatPlus TConcat = True
661+
isUpdateConcatPlus TPlus = True
662+
isUpdateConcatPlus _ = False
635663

636664
instance Pretty Expression where
637665
pretty (Term t) = pretty t
@@ -712,31 +740,7 @@ instance Pretty Expression where
712740
| op' == TLess || op' == TGreater || op' == TLessEqual || op' == TGreaterEqual || op' == TEqual || op' == TUnequal =
713741
pretty a <> softline <> pretty op <> hardspace <> pretty b
714742
-- all other operators
715-
pretty operation@(Operation _ op _) =
716-
let -- Walk the operation tree and put a list of things on the same level.
717-
-- We still need to keep the operators around because they might have comments attached to them.
718-
-- An operator is put together with its succeeding expression. Only the first operand has none.
719-
flatten :: Maybe Leaf -> Expression -> [(Maybe Leaf, Expression)]
720-
flatten opL (Operation a opR b) | opR == op = flatten opL a ++ flatten (Just opR) b
721-
flatten opL x = [(opL, x)]
722-
723-
-- Called on every operand except the first one (a.k.a. RHS)
724-
absorbOperation :: Expression -> Doc
725-
absorbOperation (Term t) | isAbsorbable t = hardspace <> pretty t
726-
-- Force nested operations to start on a new line
727-
absorbOperation x@(Operation{}) = group' RegularG $ line <> pretty x
728-
-- Force applications to start on a new line if more than the last argument is multiline
729-
absorbOperation (Application f a) = group $ prettyApp False line False f a
730-
absorbOperation x = hardspace <> pretty x
731-
732-
prettyOperation :: (Maybe Leaf, Expression) -> Doc
733-
-- First element
734-
prettyOperation (Nothing, expr) = pretty expr
735-
-- The others
736-
prettyOperation (Just op', expr) =
737-
line <> pretty (moveTrailingCommentUp op') <> nest (absorbOperation expr)
738-
in group' RegularG $
739-
(concatMap prettyOperation . flatten Nothing) operation
743+
pretty operation@(Operation _ op _) = prettyOp False operation op
740744
pretty (MemberCheck expr qmark sel) =
741745
pretty expr
742746
<> softline

src/Nixfmt/Types.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ module Nixfmt.Types (
3434
Trivium (..),
3535
removeLineInfo,
3636
hasTrivia,
37+
hasPreTrivia,
3738
LanguageElement,
3839
mapFirstToken,
3940
mapFirstToken',
41+
matchFirstToken,
4042
mapLastToken',
4143
mapAllTokens,
4244
operators,
@@ -96,6 +98,9 @@ hasTrivia :: Ann a -> Bool
9698
hasTrivia (LoneAnn _) = False
9799
hasTrivia _ = True
98100

101+
hasPreTrivia :: Ann a -> Bool
102+
hasPreTrivia Ann{preTrivia} = preTrivia /= []
103+
99104
-- | Create a new annotated value without any annotations
100105
ann :: Pos -> a -> Ann a
101106
ann l v =
@@ -242,6 +247,10 @@ class LanguageElement a where
242247
-- returned. This is useful for getting/extracting values
243248
mapFirstToken' :: (forall b. Ann b -> (Ann b, c)) -> a -> (a, c)
244249

250+
-- Convenience method. Special case of `mapFirstToken'` that takes a predicate and doesn't modify the token
251+
matchFirstToken :: (forall b. Ann b -> Bool) -> a -> Bool
252+
matchFirstToken f = snd . mapFirstToken' (\b -> (b, f b))
253+
245254
-- Same as mapLastToken, but the mapping function also yields a value that may be
246255
-- returned. This is useful for getting/extracting values
247256
mapLastToken' :: (forall b. Ann b -> (Ann b, c)) -> a -> (a, c)

standard.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,27 @@ Notable special cases are:
12821282
dependencies
12831283
];
12841284
```
1285+
- Operator chains (specifically, `++`, `//` and `+`) where the first element is absorbable are treated specially to avoid diff churn. Assuming the following input:
1286+
```nix
1287+
foo = [
1288+
bar
1289+
];
1290+
```
1291+
Normally (see "otherwise newline and indent" above), concatenating items onto that list would be formatted like this, causing the entire list to re-indent:
1292+
```nix
1293+
foo =
1294+
[
1295+
bar
1296+
]
1297+
++ baz;
1298+
```
1299+
Instead, we format it as such:
1300+
```nix
1301+
foo = [
1302+
bar
1303+
]
1304+
++ baz;
1305+
```
12851306

12861307
**Alternatives**
12871308

test/diff/attr_set/in.nix

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,101 @@
291291
] [
292292
];
293293
}
294+
# https://github.com/NixOS/nixfmt/issues/228
295+
{
296+
foo = aaaaaaaaaaaaaaaaaaaaaaaaa
297+
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbb
298+
+ ccccccccccccccccccccccccccccc
299+
+ ddddddddddddddddddddddddddddd;
300+
301+
boot.kernelParams = [ aaaaaaaaaaaaaa ]
302+
++ optionals config.boot.vesa [
303+
"vga=0x317"
304+
"nomodeset"
305+
];
306+
307+
foo2 = [ bar ]
308+
++ baz # newline!
309+
++ meow;
310+
311+
foo3 =
312+
some function application that kinda is long /* and */ multiline
313+
++ [
314+
a
315+
list
316+
];
317+
some.long.attribute # with a comment
318+
= [ stuff ]
319+
++ more stuff;
320+
some.long.attribute1 # with a comment
321+
= [ stuff a b c ]
322+
++ more stuff;
323+
324+
foo4 # nasty
325+
= # comments
326+
[ bar ]
327+
++ baz # newline!
328+
++ meow;
329+
330+
environment.systemPackages =
331+
# Include the PAM modules in the system path mostly for the manpages.
332+
[ package ]
333+
++ lib.optional config.users.ldap.enable pam_ldap;
334+
335+
environment.systemPackages2 =
336+
# Include the PAM modules in the system path mostly for the manpages.
337+
[ package ]
338+
++ lib.optional config.users.ldap.enable pam_ldap
339+
++ lib.optional config.services.kanidm.enablePam config.services.kanidm.package
340+
++ lib.optional config.services.sssd.enable pkgs.sssd
341+
++ lib.optionals config.security.pam.krb5.enable [
342+
pam_krb5
343+
pam_ccreds
344+
];
345+
346+
buildInputs1 = lib.optionals onePlatform [
347+
a
348+
b
349+
c
350+
];
351+
352+
buildInputs2 = lib.optionals onePlatform [
353+
a
354+
b
355+
c
356+
]
357+
++ lib.optionals anotherPlatform [
358+
d
359+
e
360+
f
361+
];
362+
363+
programs.ssh.knownHosts =
364+
someStuff functionArg
365+
// lib.mapAttrs (host_name: publicKey: {
366+
inherit publicKey;
367+
extraHostNames = [
368+
"${host_name}.m-0.eu"
369+
"${host_name}.vpn.m-0.eu"
370+
"${host_name}.lo.m-0.eu"
371+
];
372+
}) secret-config.ssh-hosts
373+
// {
374+
foo = "bar";
375+
};
376+
377+
programs.ssh.knownHosts2 = [] ++ (with expr;
378+
someStuff functionArg)
379+
// lib.mapAttrs (host_name: publicKey: {
380+
inherit publicKey;
381+
extraHostNames = [
382+
"${host_name}.m-0.eu"
383+
"${host_name}.vpn.m-0.eu"
384+
"${host_name}.lo.m-0.eu"
385+
];
386+
}) secret-config.ssh-hosts
387+
// {
388+
foo = "bar";
389+
};
390+
}
294391
]

0 commit comments

Comments
 (0)