Skip to content

Commit a18d2a7

Browse files
committed
Use comma-leading layout for tuples with open-ended elements
Replace parenthesis wrapping with comma-leading multiline layout when a non-last tuple element is open-ended (lambda, if-then-else, match, try-with, try-finally). This removes wrapInfixAppRhsInParenIfNeeded and the ad-hoc containsLambdaOrMatchExpr check in genTupleMultiline, replacing both with requiresMultilineToPreserveSemantics. Also extends isOpenEndedExpression to include match, function, try-with, and try-finally expressions.
1 parent 93dc7a1 commit a18d2a7

File tree

4 files changed

+148
-85
lines changed

4 files changed

+148
-85
lines changed

src/Fantomas.Core.Tests/ControlStructureTests.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ elif true then ()
417417
"""
418418

419419
[<Test>]
420-
let ``multiline if in tuple`` () =
420+
let ``multiline if in tuple uses comma-leading layout`` () =
421421
formatSourceString
422422
"""
423423
(if true then 1 else 2
@@ -428,7 +428,8 @@ let ``multiline if in tuple`` () =
428428
|> should
429429
equal
430430
"""
431-
((if true then 1 else 2), 3)
431+
(if true then 1 else 2
432+
, 3)
432433
"""
433434

434435
// https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-if-expressions

src/Fantomas.Core.Tests/RequiresMultilineToPreserveSemanticsTests.fs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,91 @@ x = fun y -> y
141141
|> g
142142
|> h
143143
"""
144+
145+
[<Test>]
146+
let ``open-ended expression in middle of chained pipe stays multiline`` () =
147+
formatSourceString
148+
"""
149+
a
150+
|> fun x -> x + 1
151+
|> h
152+
"""
153+
config
154+
|> prepend newline
155+
|> should
156+
equal
157+
"""
158+
a
159+
|> fun x -> x + 1
160+
|> h
161+
"""
162+
163+
// Expr.Tuple (open-ended non-last element)
164+
165+
[<Test>]
166+
let ``lambda as non-last tuple element stays multiline`` () =
167+
formatSourceString
168+
"""
169+
fun x -> x
170+
, y
171+
"""
172+
config
173+
|> prepend newline
174+
|> should
175+
equal
176+
"""
177+
fun x -> x
178+
, y
179+
"""
180+
181+
[<Test>]
182+
let ``nested open-ended as non-last tuple element stays multiline`` () =
183+
formatSourceString
184+
"""
185+
x = fun y -> y
186+
, z
187+
"""
188+
config
189+
|> prepend newline
190+
|> should
191+
equal
192+
"""
193+
x = fun y -> y
194+
, z
195+
"""
196+
197+
[<Test>]
198+
let ``if-then-else as non-last tuple element stays multiline`` () =
199+
formatSourceString
200+
"""
201+
if a then b else c
202+
, y
203+
"""
204+
config
205+
|> prepend newline
206+
|> should
207+
equal
208+
"""
209+
if a then b else c
210+
, y
211+
"""
212+
213+
[<Test>]
214+
let ``match as non-last tuple element stays multiline`` () =
215+
formatSourceString
216+
"""
217+
match x with
218+
| true -> 1
219+
| false -> 2
220+
, y
221+
"""
222+
config
223+
|> prepend newline
224+
|> should
225+
equal
226+
"""
227+
match x with
228+
| true -> 1
229+
| false -> 2
230+
, y
231+
"""

src/Fantomas.Core.Tests/TupleTests.fs

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ open FsUnit
66
open Fantomas.Core.Tests.TestHelpers
77

88
[<Test>]
9-
let ``tuple with lambda should add parenthesis`` () =
9+
let ``tuple with lambda stays multiline to preserve semantics`` () =
1010
formatSourceString
1111
"""
1212
let private carouselSample =
@@ -19,7 +19,10 @@ let private carouselSample =
1919
|> should
2020
equal
2121
"""let private carouselSample =
22-
FunctionComponent.Of<obj>((fun _ -> fragment [] []), "CarouselSample")
22+
FunctionComponent.Of<obj>(
23+
fun _ -> fragment [] []
24+
, "CarouselSample"
25+
)
2326
"""
2427

2528
[<Test>]
@@ -231,7 +234,7 @@ let (var1withAVeryLongLongLongLongLongLongName,
231234
"""
232235

233236
[<Test>]
234-
let ``tuple with if/then/else, 1319`` () =
237+
let ``tuple with if/then/else uses comma-leading layout, 1319`` () =
235238
formatSourceString
236239
"""
237240
let y =
@@ -248,18 +251,18 @@ let y =
248251
equal
249252
"""
250253
let y =
251-
(if String.IsNullOrWhiteSpace(args) then
252-
""
253-
elif args.StartsWith("(") then
254-
args
255-
elif
256-
v.CurriedParameterGroups.Count > 1
257-
&& (not verboseMode)
258-
then
259-
" " + args
260-
else
261-
sprintf "(%s)" args),
262-
namesWithIndices
254+
if String.IsNullOrWhiteSpace(args) then
255+
""
256+
elif args.StartsWith("(") then
257+
args
258+
elif
259+
v.CurriedParameterGroups.Count > 1
260+
&& (not verboseMode)
261+
then
262+
" " + args
263+
else
264+
sprintf "(%s)" args
265+
, namesWithIndices
263266
"""
264267

265268
[<Test>]
@@ -406,7 +409,7 @@ let shiftTimes localDate (start: Utc, duration) =
406409
"""
407410

408411
[<Test>]
409-
let ``if then expression inside object instantiation breaks when formatted, 2819`` () =
412+
let ``if then expression in object instantiation uses comma-leading layout, 2819`` () =
410413
formatSourceString
411414
"""
412415
type Chapter() =
@@ -433,11 +436,15 @@ type Chapter() =
433436
member val Title: string option = Unchecked.defaultof<_> with get, set
434437
member val Url: string = Unchecked.defaultof<_> with get, set
435438
436-
let c = Chapter(Title = (if true then Some "" else None), Url = "")
439+
let c =
440+
Chapter(
441+
Title = if true then Some "" else None
442+
, Url = ""
443+
)
437444
"""
438445

439446
[<Test>]
440-
let ``object instantiation with ifthenelse in tuple`` () =
447+
let ``object instantiation with ifthenelse in tuple uses comma-leading layout`` () =
441448
formatSourceString
442449
"""
443450
type Chapter() =
@@ -463,11 +470,15 @@ type Chapter() =
463470
member val Title: string option = Unchecked.defaultof<_> with get, set
464471
member val Url: string = Unchecked.defaultof<_> with get, set
465472
466-
let c = Chapter((if true then Some "" else None), "")
473+
let c =
474+
Chapter(
475+
if true then Some "" else None
476+
, ""
477+
)
467478
"""
468479

469480
[<Test>]
470-
let ``infixapp with ifthen rhs in tuple`` () =
481+
let ``infixapp with ifthen rhs in tuple uses comma-leading layout to preserve semantics`` () =
471482
formatSourceString
472483
"""
473484
let a = 0
@@ -495,13 +506,14 @@ let d = 2
495506
496507
let _ =
497508
try
498-
a <> (if b then c else d), b
509+
a <> if b then c else d
510+
, b
499511
with ex ->
500512
false, false
501513
"""
502514

503515
[<Test>]
504-
let ``infixapp with lambda rhs in tuple`` () =
516+
let ``infixapp with lambda rhs in tuple uses comma-leading layout to preserve semantics`` () =
505517
formatSourceString
506518
"""
507519
a |> fun b -> if b then 0 else 1
@@ -513,7 +525,8 @@ a |> fun b -> if b then 0 else 1
513525
|> should
514526
equal
515527
"""
516-
a |> (fun b -> if b then 0 else 1), 2
528+
a |> fun b -> if b then 0 else 1
529+
, 2
517530
"""
518531

519532
[<Test>]

src/Fantomas.Core/CodePrinter.fs

Lines changed: 21 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,12 @@ let rec isOpenEndedExpression (e: Expr) =
363363
| Expr.Lambda _
364364
| Expr.IfThen _
365365
| Expr.IfThenElse _
366-
| Expr.IfThenElif _ -> true
366+
| Expr.IfThenElif _
367+
| Expr.Match _
368+
| Expr.MatchLambda _
369+
| Expr.TryWith _
370+
| Expr.TryWithSingleClause _
371+
| Expr.TryFinally _ -> true
367372
| Expr.InfixApp node -> isOpenEndedExpression node.RightHandSide
368373
| Expr.SameInfixApps node ->
369374
match List.tryLast node.SubsequentExpressions with
@@ -1940,81 +1945,37 @@ let genMultilineFunctionApplicationArguments (argExpr: Expr) =
19401945
| _ -> genExpr argExpr
19411946

19421947
let genTupleExpr (node: ExprTupleNode) =
1943-
// if a tuple element is an InfixApp with a lambda or if-then-else expression on the rhs,
1944-
// we need to wrap the rhs in parenthesis to avoid a parse error caused by the higher precedence of "," over the rhs expression.
1945-
// see 2819
1946-
let wrapInfixAppRhsInParenIfNeeded expr =
1947-
match expr with
1948-
| Expr.InfixApp exprInfixAppNode ->
1949-
match exprInfixAppNode.RightHandSide with
1950-
| IsLambdaOrIfThenElse e ->
1951-
let parenNode =
1952-
mkExprParenNode
1953-
(SingleTextNode("(", Fantomas.FCS.Text.Range.range0))
1954-
e
1955-
(SingleTextNode(")", Fantomas.FCS.Text.Range.range0))
1956-
Fantomas.FCS.Text.Range.range0
1957-
1958-
ExprInfixAppNode(
1959-
exprInfixAppNode.LeftHandSide,
1960-
exprInfixAppNode.Operator,
1961-
parenNode,
1962-
Fantomas.FCS.Text.Range.range0
1963-
)
1964-
|> Expr.InfixApp
1965-
| _ -> expr
1966-
| _ -> expr
1967-
19681948
let shortExpression =
1969-
let lastIndex = Array.length node.Children - 1
1970-
1971-
coli sepNone node.Items (fun i c ->
1949+
coli sepNone node.Items (fun _i c ->
19721950
match c with
1973-
| Choice1Of2 e ->
1974-
match e with
1975-
| IsLambdaOrIfThenElse e when i <> lastIndex -> sepOpenT +> genExpr e +> sepCloseT
1976-
| e -> genExpr (wrapInfixAppRhsInParenIfNeeded e)
1951+
| Choice1Of2 e -> genExpr e
19771952
| Choice2Of2 comma -> genSingleTextNode comma +> addSpaceIfSpaceAfterComma)
19781953

19791954
let longExpression = genTupleMultiline node
19801955

1981-
atCurrentColumn (expressionFitsOnRestOfLine shortExpression longExpression)
1982-
|> genNode node
1956+
let exprs = node.Items |> List.choose (function Choice1Of2 e -> Some e | _ -> None)
1957+
1958+
if requiresMultilineToPreserveSemantics exprs then
1959+
atCurrentColumn longExpression |> genNode node
1960+
else
1961+
atCurrentColumn (expressionFitsOnRestOfLine shortExpression longExpression)
1962+
|> genNode node
19831963

19841964
let genTupleMultiline (node: ExprTupleNode) =
1985-
let containsLambdaOrMatchExpr =
1986-
// If the any items (expect the last) is a match/lambda
1987-
node.Items
1988-
|> List.chunkBySize 2
1989-
|> List.exists (fun pair ->
1990-
match pair with
1991-
| [ Choice1Of2 e; Choice2Of2 _ ] ->
1992-
match e with
1993-
| Expr.Match _
1994-
| Expr.Lambda _ -> true
1995-
| Expr.InfixApp node ->
1996-
match node.RightHandSide with
1997-
| Expr.Match _
1998-
| Expr.Lambda _ -> true
1999-
| _ -> false
2000-
| Expr.SameInfixApps node ->
2001-
match List.last node.SubsequentExpressions with
2002-
| _, Expr.Lambda _ -> true
2003-
| _ -> false
2004-
| _ -> false
2005-
| _ -> false)
1965+
let exprs = node.Items |> List.choose (function Choice1Of2 e -> Some e | _ -> None)
20061966

2007-
let lastIndex = List.length node.Items - 1
1967+
// When a non-last element is open-ended (lambda, if-then-else, match, ...),
1968+
// the comma must start a new line so that it isn't swallowed by the preceding expression.
1969+
let commaLeading = requiresMultilineToPreserveSemantics exprs
20081970

2009-
let genItem idx =
1971+
let genItem _idx =
20101972
function
20111973
| Choice1Of2 e ->
20121974
match e with
2013-
| IsIfThenElse _ when (idx < lastIndex) -> autoParenthesisIfExpressionExceedsPageWidth (genExpr e)
20141975
| Expr.InfixApp node when (node.Operator.Text = "=") -> genNamedArgumentExpr node
20151976
| _ -> genExpr e
20161977
| Choice2Of2 comma ->
2017-
if containsLambdaOrMatchExpr then
1978+
if commaLeading then
20181979
sepNln +> genSingleTextNode comma +> sepSpace
20191980
else
20201981
genSingleTextNode comma +> sepNln

0 commit comments

Comments
 (0)