diff --git a/src/Fantomas.Tests/KeepIndentInBranchTests.fs b/src/Fantomas.Tests/KeepIndentInBranchTests.fs index 39a5a1691f..a9e2f2fac5 100644 --- a/src/Fantomas.Tests/KeepIndentInBranchTests.fs +++ b/src/Fantomas.Tests/KeepIndentInBranchTests.fs @@ -1314,3 +1314,182 @@ module Foo = failwith "" """ + +[] +let ``multiple nested Sequential expressions, 1714`` () = + formatSourceString + false + """ +namespace Foo + +module Bar = + + [] + let main argv = + let args = foo + printfn "" + printfn "" + printfn "" + let m = "" + if foo then + printfn "aborting" + 1 + else + + printfn "blah" + let m = "" + if foo then + printfn "aborting" + 1 + else + + let fs = FileSystem () + use f = fs.File.Open("") + 0 +""" + config + |> prepend newline + |> should + equal + """ +namespace Foo + +module Bar = + + [] + let main argv = + let args = foo + printfn "" + printfn "" + printfn "" + let m = "" + + if foo then + printfn "aborting" + 1 + else + + printfn "blah" + let m = "" + + if foo then + printfn "aborting" + 1 + else + + let fs = FileSystem() + use f = fs.File.Open("") + 0 +""" + +[] +let ``multiple nested LetOrUse expressions, 1717`` () = + formatSourceString + false + """ +module Foo = + let main (args : _) = + let fs = FileSystem() + let ab = ThingOne.make fs (ThingFour.defaultFoo args.Args.ThingOne) + + let thingFive = ThingFive.thingFive fs ab + + use loggerFactory = Logging.make thingFive LogEventLevel.Verbose LogEventLevel.Information + let log = loggerFactory.CreateLogger "foo" + log.LogDebug("Command line options used: {CommandLine}", args) + log.LogInformation("Thing One: {ThingOne}", ThingOne.getFoo ab) + + let thing, cd = + args.Thing + |> Args.render loggerFactory thingFive + + let skipBehaviour = + if defaultArg args.Skip then + Options.Exclude + else + Options.Include + + let thing, errors = Information.make fs ab thing + use thing = thing + + let operationToDo = + Operations.operation loggerFactory fs ab tp thing (DUCase args.Thing) args.Info skipBehaviour + + match ThingEight.defaultFun args.DryRun with + | DryRunMode.Dry -> + log.LogInformation("No changes made due to --dry-run.") + 0 + | DryRunMode.Wet -> + + match operationToDo with + | None -> + log.LogWarning("No changes required; no action taken.") + 0 + | Some thingsToDo -> + + thingsToDo + |> Operations.perform loggerFactory (errors |> Map.keys) + |> Seq.map (fun i -> i.Name) + |> String.concat "\n" + |> fun i -> log.LogInformation("Completed operation:\n{Result}", i) + 0 +""" + { config with + MultiLineLambdaClosingNewline = true + MultilineBlockBracketsOnSameColumn = true + AlternativeLongMemberDefinitions = true } + |> prepend newline + |> should + equal + """ +module Foo = + let main (args: _) = + let fs = FileSystem() + + let ab = + ThingOne.make fs (ThingFour.defaultFoo args.Args.ThingOne) + + let thingFive = ThingFive.thingFive fs ab + + use loggerFactory = + Logging.make thingFive LogEventLevel.Verbose LogEventLevel.Information + + let log = loggerFactory.CreateLogger "foo" + log.LogDebug("Command line options used: {CommandLine}", args) + log.LogInformation("Thing One: {ThingOne}", ThingOne.getFoo ab) + + let thing, cd = + args.Thing |> Args.render loggerFactory thingFive + + let skipBehaviour = + if defaultArg args.Skip then + Options.Exclude + else + Options.Include + + let thing, errors = Information.make fs ab thing + use thing = thing + + let operationToDo = + Operations.operation loggerFactory fs ab tp thing (DUCase args.Thing) args.Info skipBehaviour + + match ThingEight.defaultFun args.DryRun with + | DryRunMode.Dry -> + log.LogInformation("No changes made due to --dry-run.") + 0 + | DryRunMode.Wet -> + + match operationToDo with + | None -> + log.LogWarning("No changes required; no action taken.") + 0 + | Some thingsToDo -> + + thingsToDo + |> Operations.perform loggerFactory (errors |> Map.keys) + |> Seq.map (fun i -> i.Name) + |> String.concat "\n" + |> fun i -> log.LogInformation("Completed operation:\n{Result}", i) + + 0 +""" diff --git a/src/Fantomas.Tests/LetBindingTests.fs b/src/Fantomas.Tests/LetBindingTests.fs index c0049d59a1..2949a88333 100644 --- a/src/Fantomas.Tests/LetBindingTests.fs +++ b/src/Fantomas.Tests/LetBindingTests.fs @@ -1747,52 +1747,6 @@ module Foo = Assert.Throws runTest |> ignore """ -[] -let ``mhex xx 2`` () = - formatSourceString - false - """ -module Foo = - let bar () = - - let f1 = () - - let runTest () = - let (Thing f) = - [ a ; b ] |> Blah.tryConcat |> Option.get in f () |> ignore - - Assert.Throws runTest |> ignore -""" - { config with - MaxLineLength = 100 - SpaceBeforeUppercaseInvocation = true - SpaceBeforeClassConstructor = true - SpaceBeforeMember = true - SpaceBeforeColon = true - SpaceBeforeSemicolon = true - MultilineBlockBracketsOnSameColumn = true - NewlineBetweenTypeDefinitionAndMembers = true - KeepIfThenInSameLine = true - AlignFunctionSignatureToIndentation = true - AlternativeLongMemberDefinitions = true - MultiLineLambdaClosingNewline = true - KeepIndentInBranch = true } - |> prepend newline - |> should - equal - """ -module Foo = - let bar () = - - let f1 = () - - let runTest () = - let (Thing f) = - [ a ; b ] |> Blah.tryConcat |> Option.get in f () |> ignore - - Assert.Throws runTest |> ignore -""" - [] let ``multiline return type followed by type declaration, 1624`` () = formatSourceString @@ -1907,3 +1861,18 @@ module PoorlyIndented = cmd.AsyncExecute(id = thingId) """ + +[] +let ``short let in`` () = + formatSourceString + false + """ +let a = x in foo x +""" + config + |> prepend newline + |> should + equal + """ +let a = x in foo x +""" diff --git a/src/Fantomas/CodePrinter.fs b/src/Fantomas/CodePrinter.fs index cec764bc3d..ad02d6fa61 100644 --- a/src/Fantomas/CodePrinter.fs +++ b/src/Fantomas/CodePrinter.fs @@ -1977,44 +1977,19 @@ and genExpr astContext synExpr ctx = genExpr astContext e +> genGenericTypeParameters astContext ts | LetOrUses (bs, e) -> - let inKeyWordTrivia (binding: SynBinding) (ctx: Context) = - let inRange = - ctx.MkRange binding.RangeOfBindingAndRhs.End e.Range.Start - - Map.tryFindOrEmptyList IN ctx.TriviaTokenNodes - |> TriviaHelpers.``keyword token after start column and on same line`` inRange - |> List.tryHead - - let isInSameLine ctx = - match bs with - | [ _, (LetBinding _ as binding) ] -> - Option.isSome (inKeyWordTrivia binding ctx) - && not (futureNlnCheck (genExpr astContext e) ctx) - | _ -> false - fun ctx -> - if isInSameLine ctx then - // short expression with in keyword - // f.ex. let a in () - atCurrentColumn - (optSingle - (fun (p, b) -> - genLetBinding - { astContext with - IsFirstChild = p <> "and" } - p - b - +> genInKeyword b e) - (List.tryHead bs) - +> genExpr astContext e) - ctx - else - let items = - collectMultilineItemForLetOrUses astContext bs e - @ collectMultilineItemForSynExpr astContext e - - atCurrentColumn (colWithNlnWhenItemIsMultilineUsingConfig items) ctx + let items = + let inKeywords = + Map.tryFindOrEmptyList IN ctx.TriviaTokenNodes + + collectMultilineItemForLetOrUses + astContext + inKeywords + bs + e + (collectMultilineItemForSynExpr astContext inKeywords e) + atCurrentColumn (colWithNlnWhenItemIsMultilineUsingConfig items) ctx // Could customize a bit if e is single line | TryWith (e, cs) -> let prefix = @@ -2091,11 +2066,14 @@ and genExpr astContext synExpr ctx = | SequentialSimple es | Sequentials es -> - let items = - List.collect (collectMultilineItemForSynExpr astContext) es + fun ctx -> + let inKeywords = + Map.tryFindOrEmptyList IN ctx.TriviaTokenNodes - atCurrentColumn (colWithNlnWhenItemIsMultilineUsingConfig items) + let items = + List.collect (collectMultilineItemForSynExpr astContext inKeywords) es + atCurrentColumn (colWithNlnWhenItemIsMultilineUsingConfig items) ctx // A generalization of IfThenElse | ElIf ((e1, e2, _, _, _) :: es, enOpt) -> // https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-if-expressions @@ -3354,42 +3332,88 @@ and genAppWithParenthesis app astContext = | Choice1Of2 t -> genAppWithTupledArgument t astContext | Choice2Of2 s -> genAppWithSingleParenthesisArgument s astContext -and collectMultilineItemForSynExpr (astContext: ASTContext) (e: SynExpr) : ColMultilineItem list = +and collectMultilineItemForSynExpr + (astContext: ASTContext) + (inKeyWordTrivia: TriviaNode list) + (e: SynExpr) + : ColMultilineItem list = match e with | LetOrUses (bs, e) -> - collectMultilineItemForLetOrUses astContext bs e - @ collectMultilineItemForSynExpr astContext e + collectMultilineItemForLetOrUses + astContext + inKeyWordTrivia + bs + e + (collectMultilineItemForSynExpr astContext inKeyWordTrivia e) | Sequentials s -> s - |> List.collect (collectMultilineItemForSynExpr astContext) + |> List.collect (collectMultilineItemForSynExpr astContext inKeyWordTrivia) | _ -> let t, r = synExprToFsAstType e - [ ColMultilineItem(genExpr astContext e, sepNlnConsideringTriviaContentBeforeForMainNode t r) ] and collectMultilineItemForLetOrUses (astContext: ASTContext) + (inKeyWordTrivia: TriviaNode list) (bs: (string * SynBinding) list) (e: SynExpr) + (itemsForExpr: ColMultilineItem list) : ColMultilineItem list = - bs - |> List.map - (fun (p, x) -> - let expr = - enterNodeFor (synBindingToFsAstType x) x.RangeOfBindingAndRhs - +> genLetBinding - { astContext with - IsFirstChild = p <> "and" } - p - x - +> genInKeyword x e + // It be nice if the `in` keyword was part of the AST tree as suggested in + // https://github.com/dotnet/fsharp/issues/10198 + let bindingHasInKeyword (binding: SynBinding) : bool = + let inRange = + Range.mkRange binding.RangeOfBindingAndRhs.FileName binding.RangeOfBindingAndRhs.End e.Range.Start - let range = x.RangeOfBindingAndRhs + inKeyWordTrivia + |> TriviaHelpers.``keyword token after start column and on same line`` inRange + |> List.isNotEmpty - let sepNln = - sepNlnConsideringTriviaContentBeforeForMainNode (synBindingToFsAstType x) range + let multilineBinding p x = + let expr = + enterNodeFor (synBindingToFsAstType x) x.RangeOfBindingAndRhs + +> genLetBinding + { astContext with + IsFirstChild = p <> "and" } + p + x + +> genInKeyword x e + + let range = x.RangeOfBindingAndRhs + + let sepNln = + sepNlnConsideringTriviaContentBeforeForMainNode (synBindingToFsAstType x) range - ColMultilineItem(expr, sepNln)) + ColMultilineItem(expr, sepNln) + + let multipleOrLongBs bs = + bs + |> List.map (fun (p, x) -> multilineBinding p x) + + match bs, itemsForExpr with + | [], _ -> itemsForExpr + | [ p, b ], [ ColMultilineItem (expr, sepNlnForExpr) ] -> + // This is a trickier case + // maybe the let binding and expression are short so they form one ColMultilineItem + // Something like: let a = 1 in () + + let range = b.RangeOfBindingAndRhs + + let sepNlnForBinding = + sepNlnConsideringTriviaContentBeforeForMainNode (synBindingToFsAstType b) range + + if bindingHasInKeyword b then + // single multiline item + let expr = + enterNodeFor (synBindingToFsAstType b) b.RangeOfBindingAndRhs + +> genLetBinding astContext p b + +> genInKeyword b e + +> expressionFitsOnRestOfLine expr (sepNln +> sepNlnForExpr +> expr) + + [ ColMultilineItem(expr, sepNlnForBinding) ] + else + multipleOrLongBs bs @ itemsForExpr + | bs, _ -> multipleOrLongBs bs @ itemsForExpr and genInKeyword (binding: SynBinding) (e: SynExpr) (ctx: Context) = let inKeyWordTrivia (binding: SynBinding) = @@ -5285,81 +5309,52 @@ and genParenTupleWithIndentAndNewlines ps astContext = +> sepNln +> sepCloseT -and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> Context = - let keepIndentExpr = - let genOtherExprItem (e: SynExpr) : ColMultilineItem = - ColMultilineItem( - genExpr astContext e, - (let t, r = synExprToFsAstType e in sepNlnConsideringTriviaContentBeforeForMainNode t r) - ) - - let genBindingItems (bs: (string * SynBinding) list) : ColMultilineItem list = - List.map - (fun (s, synBinding: SynBinding) -> - let range = synBinding.RangeOfBindingAndRhs - - let expr = - enterNodeFor (synBindingToFsAstType synBinding) range - +> genLetBinding astContext s synBinding - - - let sepNln = - sepNlnConsideringTriviaContentBeforeForMainNode (synBindingToFsAstType synBinding) range - - ColMultilineItem(expr, sepNln)) - bs - - let genKeepIndentMatchItem - ((me, clauses, matchRange, matchTriviaType): SynExpr * SynMatchClause list * Range * FsAstType) - : ColMultilineItem = - ColMultilineItem( - genKeepIndentMatch astContext me clauses matchRange matchTriviaType, - sepNlnConsideringTriviaContentBeforeForMainNode matchTriviaType matchRange - ) +and collectMultilineItemForSynExprKeepIndent + (astContext: ASTContext) + (inKeyWordTrivia: TriviaNode list) + (e: SynExpr) + : ColMultilineItem list = + match e with + | LetOrUses (bs, e) -> + collectMultilineItemForLetOrUses + astContext + inKeyWordTrivia + bs + e + (collectMultilineItemForSynExprKeepIndent astContext inKeyWordTrivia e) + | Sequentials es -> + let lastIndex = es.Length - 1 + + es + |> List.mapi + (fun idx e -> + if idx = lastIndex then + collectMultilineItemForSynExprKeepIndent astContext inKeyWordTrivia e + else + collectMultilineItemForSynExpr astContext inKeyWordTrivia e) + |> List.collect id + | KeepIndentMatch (me, clauses, matchRange, matchTriviaType) -> + ColMultilineItem( + genKeepIndentMatch astContext me clauses matchRange matchTriviaType, + sepNlnConsideringTriviaContentBeforeForMainNode matchTriviaType matchRange + ) + |> List.singleton + | KeepIndentIfThenElse (branches, elseBranch, ifElseRange) -> + ColMultilineItem( + genKeepIdentIf astContext branches elseBranch ifElseRange, + sepNlnConsideringTriviaContentBeforeForMainNode SynExpr_IfThenElse ifElseRange + ) + |> List.singleton + | _ -> + let t, r = synExprToFsAstType e + [ ColMultilineItem(genExpr astContext e, sepNlnConsideringTriviaContentBeforeForMainNode t r) ] - let genKeepIndentIfThenElseItem - ((branches, elseBranch, ifElseRange): (SynExpr * SynExpr * Range * Range * SynExpr) list * SynExpr * Range) - : ColMultilineItem = - ColMultilineItem( - genKeepIdentIf astContext branches elseBranch ifElseRange, - sepNlnConsideringTriviaContentBeforeForMainNode SynExpr_IfThenElse ifElseRange - ) +and genExprKeepIndentInBranch (astContext: ASTContext) (e: SynExpr) : Context -> Context = + let keepIndentExpr (ctx: Context) = + let items = + collectMultilineItemForSynExprKeepIndent astContext (Map.tryFindOrEmptyList IN ctx.TriviaTokenNodes) e - match e with - | Sequential (e1, KeepIndentMatch kim, true) -> - colWithNlnWhenItemIsMultilineUsingConfig [ genOtherExprItem e1 - genKeepIndentMatchItem kim ] - | Sequential (e1, LetOrUses (bs, KeepIndentMatch kim), true) -> - colWithNlnWhenItemIsMultilineUsingConfig [ yield genOtherExprItem e1 - yield! genBindingItems bs - yield genKeepIndentMatchItem kim ] - | LetOrUses (bs, KeepIndentMatch kim) -> - colWithNlnWhenItemIsMultilineUsingConfig [ yield! genBindingItems bs - yield genKeepIndentMatchItem kim ] - | LetOrUses (bs, Sequential (e1, KeepIndentMatch kim, true)) -> - colWithNlnWhenItemIsMultilineUsingConfig [ yield! genBindingItems bs - yield genOtherExprItem e1 - yield genKeepIndentMatchItem kim ] - | KeepIndentMatch (me, clauses, matchRange, matchTriviaType) -> - genKeepIndentMatch astContext me clauses matchRange matchTriviaType - | Sequential (e1, KeepIndentIfThenElse kii, true) -> - colWithNlnWhenItemIsMultilineUsingConfig [ genOtherExprItem e1 - genKeepIndentIfThenElseItem kii ] - | Sequential (e1, LetOrUses (bs, KeepIndentIfThenElse kii), true) -> - colWithNlnWhenItemIsMultilineUsingConfig [ yield genOtherExprItem e1 - yield! genBindingItems bs - yield genKeepIndentIfThenElseItem kii ] - | LetOrUses (bs, KeepIndentIfThenElse kii) -> - colWithNlnWhenItemIsMultilineUsingConfig [ yield! genBindingItems bs - yield genKeepIndentIfThenElseItem kii ] - | LetOrUses (bs, Sequential (e1, KeepIndentIfThenElse kii, true)) -> - - colWithNlnWhenItemIsMultilineUsingConfig [ yield! genBindingItems bs - yield genOtherExprItem e1 - yield genKeepIndentIfThenElseItem kii ] - | KeepIndentIfThenElse (branches, elseBranch, ifElseRange) -> - genKeepIdentIf astContext branches elseBranch ifElseRange - | _ -> genExpr astContext e + colWithNlnWhenItemIsMultilineUsingConfig items ctx ifElseCtx (fun ctx -> ctx.Config.KeepIndentInBranch) keepIndentExpr (genExpr astContext e)