From 173e0847aa587465c52847257c1ca1e8ee6aeb6f Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Thu, 31 Jul 2025 21:18:01 +0100 Subject: [PATCH 1/9] Add tests --- .../CompilerDirectivesTests.fs | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs index 7d646d7176..305cff6c8b 100644 --- a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs +++ b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs @@ -3233,3 +3233,285 @@ let CombineImportedAssembliesTask () """ + +[] +let ``should keep attributes on mutually recursive class, no defines`` () = + formatSourceStringWithDefines + [] + """ +type X = int + and +#if NET5_0_OR_GREATER + [] +#endif + [] + [] Y = int + +""" + config + |> prepend newline + |> should + equal + """ +type X = int +and +#if NET5_0_OR_GREATER +#endif +[] Y = int +""" + +[] +let ``should keep attributes on mutually recursive class, NET5_0_OR_GREATER`` () = + formatSourceStringWithDefines + [ "NET5_0_OR_GREATER" ] + """ +type X = int + and +#if NET5_0_OR_GREATER + [] +#endif + [] + [] Y = int + +""" + config + |> prepend newline + |> should + equal + """ +type X = int +and +#if NET5_0_OR_GREATER +[] +#endif +[] Y = int +""" + +[] +let ``should keep attributes on mutually recursive class, 3174`` () = + formatSourceString + """ +type X = int + and +#if NET5_0_OR_GREATER + [] +#endif + [] + [] Y = int + +""" + config + |> prepend newline + |> should + equal + """ +type X = int +and +#if NET5_0_OR_GREATER +[] +#endif +[] Y = int +""" + +[] +let ``directive and attributes after keyword should indent correctly, no defines`` () = + formatSourceStringWithDefines + [] + """ +type +#if FOO + [] D = Object +#else + E = struct end +#endif + +#if FOO +type A = int +and +#else +type +#endif + [] D = struct end + +""" + config + |> prepend newline + |> should + equal + """ +type +#if FOO +#else + E = struct end +#endif + +#if FOO +and +#else +type +#endif + [] D = struct end +""" + + +[] +let ``directive and attributes after keyword should indent correctly, FOO`` () = + formatSourceStringWithDefines + ["FOO"] + """ +type +#if FOO + [] D = Object +#else + E = struct end +#endif + +#if FOO +type A = int +and +#else +type +#endif + [] D = struct end + +""" + config + |> prepend newline + |> should + equal + """ +type +#if FOO +#else +#endif + +#if FOO +type A = int +and +#else +#endif + [] D = struct end +""" + + + +[] +let ``directive and attributes after keyword should indent correctly, 3174`` () = + formatSourceString + """ +type +#if FOO + [] D = Object +#else + E = struct end +#endif + +#if FOO +type A = int +and +#else +type +#endif + [] D = struct end + +""" + config + |> prepend newline + |> should + equal + """ +type +#if FOO + [] D = Object +#else +#endif + +#if FOO +type A = int +and +#else +#endif + [] D = struct end +""" + + +[] +let ``directive and attributes after keyword should not be moved out, no defines`` () = + formatSourceStringWithDefines + [] + """ +#if FOO +type A = int +and +#else +type +#endif + [] D = struct end + +""" + config + |> prepend newline + |> should + equal + """ +#if FOO +#else +type +#endif + [] D = struct end + +""" +[] +let ``directive and attributes after keyword should not be moved out, FOO`` () = + formatSourceStringWithDefines + ["FOO"] + """ +#if FOO +type A = int +and +#else +type +#endif + [] D = struct end + +""" + config + |> prepend newline + |> should + equal + """ +#if FOO +type A = int +and +#else +#endif + [] D = struct end + +""" +[] +let ``directive and attributes after keyword should not be moved out, 3174`` () = + formatSourceString + """ +#if FOO +type A = int +and +#else +type +#endif + [] D = struct end + +""" + config + |> prepend newline + |> should + equal + """ +#if FOO +type A = int +and +#else +type +#endif + [] D = struct end + +""" From 476c84d5c1af45c92de5ba69e750915730ee2080 Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Thu, 31 Jul 2025 21:39:53 +0100 Subject: [PATCH 2/9] Update tests --- .../CompilerDirectivesTests.fs | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs index 305cff6c8b..e8f5555fb5 100644 --- a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs +++ b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs @@ -3318,39 +3318,38 @@ let ``directive and attributes after keyword should indent correctly, no defines formatSourceStringWithDefines [] """ -type +type C = Object +and #if FOO - [] D = Object + [] D = Object #else - E = struct end + D = Object #endif +type #if FOO -type A = int -and + [] D = struct end #else -type + E = struct end #endif - [] D = struct end - """ config |> prepend newline |> should equal """ -type +type C = Object +and #if FOO #else - E = struct end + D = Object #endif +type #if FOO -and #else -type + E = struct end #endif - [] D = struct end """ @@ -3359,38 +3358,38 @@ let ``directive and attributes after keyword should indent correctly, FOO`` () = formatSourceStringWithDefines ["FOO"] """ -type +type C = Object +and #if FOO - [] D = Object + [] D = Object #else - E = struct end + D = Object #endif +type #if FOO -type A = int -and + [] D = struct end #else -type + E = struct end #endif - [] D = struct end - """ config |> prepend newline |> should equal """ -type +type C = Object +and #if FOO + [] D = Object #else #endif +type #if FOO -type A = int -and + [] D = struct end #else #endif - [] D = struct end """ @@ -3399,40 +3398,42 @@ and let ``directive and attributes after keyword should indent correctly, 3174`` () = formatSourceString """ -type +type C = Object +and #if FOO - [] D = Object + [] D = Object #else - E = struct end + D = Object #endif +type #if FOO -type A = int -and + [] D = struct end #else -type + E = struct end #endif - [] D = struct end - """ config |> prepend newline |> should equal """ -type +type C = Object +and #if FOO - [] D = Object + [] D = Object #else + D = Object #endif +type #if FOO -type A = int -and + [] D = struct end #else + E = struct end #endif - [] D = struct end """ + [] From c9159dc42a4ccd0881a360faa195cb63b9d20892 Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Fri, 1 Aug 2025 20:59:08 +0100 Subject: [PATCH 3/9] Fix typedef range --- src/Fantomas.Core/ASTTransformer.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fantomas.Core/ASTTransformer.fs b/src/Fantomas.Core/ASTTransformer.fs index 58450725c3..2039691a55 100644 --- a/src/Fantomas.Core/ASTTransformer.fs +++ b/src/Fantomas.Core/ASTTransformer.fs @@ -2557,7 +2557,7 @@ let mkTypeDefn else match ats with | [] -> leadingKeyword.Range - | firstAttr :: _ -> firstAttr.Range + | firstAttr :: _ -> unionRanges leadingKeyword.Range firstAttr.Range let endRange = match trivia.EqualsRange with From 22a1d42ed68dec20b75f2292056c2cf5fdaf7e71 Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Fri, 1 Aug 2025 22:19:20 +0100 Subject: [PATCH 4/9] Partially working adjustments to detecting leading keyword vs attribute order --- .../CompilerDirectivesTests.fs | 31 +++++++++---------- src/Fantomas.Core/CodePrinter.fs | 29 ++++++++++++++--- src/Fantomas.Core/SyntaxOak.fs | 2 +- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs index e8f5555fb5..2aa95605bc 100644 --- a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs +++ b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs @@ -3257,7 +3257,7 @@ type X = int and #if NET5_0_OR_GREATER #endif -[] Y = int + [] Y = int """ [] @@ -3282,9 +3282,9 @@ type X = int type X = int and #if NET5_0_OR_GREATER -[] + [] #endif -[] Y = int + [] Y = int """ [] @@ -3308,9 +3308,9 @@ type X = int type X = int and #if NET5_0_OR_GREATER -[] + [] #endif -[] Y = int + [] Y = int """ [] @@ -3338,14 +3338,14 @@ type |> should equal """ -type C = Object +type C = Object and #if FOO #else D = Object #endif -type +type #if FOO #else E = struct end @@ -3378,14 +3378,14 @@ type |> should equal """ -type C = Object +type C = Object and #if FOO [] D = Object #else #endif -type +type #if FOO [] D = struct end #else @@ -3406,7 +3406,7 @@ and D = Object #endif -type +type #if FOO [] D = struct end #else @@ -3418,7 +3418,7 @@ type |> should equal """ -type C = Object +type C = Object and #if FOO [] D = Object @@ -3426,7 +3426,7 @@ and D = Object #endif -type +type #if FOO [] D = struct end #else @@ -3457,10 +3457,9 @@ type """ #if FOO #else -type +type #endif [] D = struct end - """ [] let ``directive and attributes after keyword should not be moved out, FOO`` () = @@ -3471,7 +3470,7 @@ let ``directive and attributes after keyword should not be moved out, FOO`` () = type A = int and #else -type +type #endif [] D = struct end @@ -3487,7 +3486,6 @@ and #else #endif [] D = struct end - """ [] let ``directive and attributes after keyword should not be moved out, 3174`` () = @@ -3514,5 +3512,4 @@ and type #endif [] D = struct end - """ diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 71b9ee65a5..ac8fffbf49 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -311,6 +311,17 @@ let genAttributes (node: MultipleAttributeListNode option) = |> genNode a) |> genNode node +let genAttributesNoNewline (node: MultipleAttributeListNode option) = + match node with + | None -> sepNone + | Some node -> + col sepNlnUnlessLastEventIsNewline node.AttributeLists (fun a -> + genSingleTextNode a.Opening + +> (genAttributesCore a.Attributes) + +> genSingleTextNode a.Closing + +> sepNlnWhenWriteBeforeNewlineNotEmpty + |> genNode a) + |> genNode node // The inherit keyword should already be printed by the caller let genInheritConstructor (ic: InheritConstructor) = match ic with @@ -3463,22 +3474,30 @@ let genTypeDefn (td: TypeDefn) = let header = let implicitConstructor = typeName.ImplicitConstructor - let hasAndKeyword = typeName.LeadingKeyword.Text = "and" + // let hasAndKeyword = typeName.LeadingKeyword.Text = "and" // Workaround for https://github.com/fsprojects/fantomas/issues/628 + let hasAttributesAfterLeadingKeyword = + match typeName.Attributes with + | Some attributes -> Fantomas.FCS.Text.Range.rangeBeforePos typeName.LeadingKeyword.Range attributes.Range.Start + | None -> false let hasTriviaAfterLeadingKeyword = hasTriviaAfterLeadingKeyword typeName.Identifier typeName.Accessibility + let hasTriviaBeforeAttributes = + match typeName.Attributes with + | Some attributes -> attributes.HasContentBefore + | None -> false genXml typeName.XmlDoc - +> onlyIfNot hasAndKeyword (genAttributes typeName.Attributes) + +> onlyIfNot hasAttributesAfterLeadingKeyword (genAttributes typeName.Attributes) +> genSingleTextNode typeName.LeadingKeyword - +> onlyIf hasTriviaAfterLeadingKeyword indent - +> onlyIf hasAndKeyword (sepSpace +> genOnelinerAttributes typeName.Attributes) + +> onlyIf (hasTriviaAfterLeadingKeyword || hasTriviaBeforeAttributes) indent + +> onlyIf hasAttributesAfterLeadingKeyword (sepSpace +> genAttributesNoNewline typeName.Attributes) +> sepSpace +> genAccessOpt typeName.Accessibility +> genTypeAndParam (genIdentListNode typeName.Identifier) typeName.TypeParameters +> onlyIfNot typeName.Constraints.IsEmpty (sepSpace +> genTypeConstraints typeName.Constraints) - +> onlyIf hasTriviaAfterLeadingKeyword unindent + +> onlyIf (hasTriviaAfterLeadingKeyword || hasTriviaBeforeAttributes) unindent +> leadingExpressionIsMultiline (optSingle (fun imCtor -> sepSpaceBeforeClassConstructor +> genImplicitConstructor imCtor) diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index c15dc6f0da..d93aa56ae3 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -2145,8 +2145,8 @@ type TypeNameNode override val Children: Node array = [| yield! noa xmlDoc - yield! noa attributes yield leadingKeyword + yield! noa attributes yield! noa ao yield identifier yield! noa (Option.map TyparDecls.Node typeParams) From 2ffaf8dde86892d753c21df9d76dab7ab5baa88d Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Sat, 2 Aug 2025 10:35:10 +0100 Subject: [PATCH 5/9] Minimal working --- src/Fantomas.Core/CodePrinter.fs | 88 +++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index ac8fffbf49..9e6c227837 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -298,6 +298,49 @@ let genOnelinerAttributes (n: MultipleAttributeListNode option) = |> genNode n ifElse ats.IsEmpty sepNone (genAttrs +> sepSpace) +let partitionOn splitBefore splitAfter items = + let folder acc item = + match acc with + | [] -> + [[item]] + | (lastItem :: _ as currentGroup) :: restGroups when splitAfter lastItem -> + [item] :: currentGroup :: restGroups + | currentGroup :: restGroups when splitBefore item -> + [item] :: currentGroup :: restGroups + | currentGroup :: restGroups -> + (item :: currentGroup) :: restGroups + + items + |> List.fold folder [] + |> List.map List.rev + |> List.rev + +// Like genOnelinerAtrtibutes, but splits the attributelist into chunks if there are contentBefore/After, +// Then properly renders those in between +let genOnelinerAttributesWithTrivia (n: MultipleAttributeListNode option) = + match n with + | None -> sepNone + | Some n -> + let attributeLists = + n.AttributeLists |> partitionOn (_.HasContentBefore) (_.HasContentAfter) + + col sepNone attributeLists (fun (al) -> + let ats = al |> List.collect _.Attributes + let openingToken = + List.tryHead al + |> Option.map (fun (a: AttributeListNode) -> a.Opening) + + let closingToken = + List.tryLast al + |> Option.map (fun (a: AttributeListNode) -> a.Closing) + optSingle genSingleTextNode openingToken + +> (genAttributesCore ats) + +> optSingle genSingleTextNode closingToken + +> sepNlnWhenWriteBeforeNewlineNotEmpty + |> genNode (al |> List.head) + ) |> genNode n + + let genAttributes (node: MultipleAttributeListNode option) = match node with @@ -311,17 +354,17 @@ let genAttributes (node: MultipleAttributeListNode option) = |> genNode a) |> genNode node -let genAttributesNoNewline (node: MultipleAttributeListNode option) = - match node with - | None -> sepNone - | Some node -> - col sepNlnUnlessLastEventIsNewline node.AttributeLists (fun a -> - genSingleTextNode a.Opening - +> (genAttributesCore a.Attributes) - +> genSingleTextNode a.Closing - +> sepNlnWhenWriteBeforeNewlineNotEmpty - |> genNode a) - |> genNode node +// let genAttributesNoNewline (node: MultipleAttributeListNode option) = +// match node with +// | None -> sepNone +// | Some node -> +// col sepNlnUnlessLastEventIsNewline node.AttributeLists (fun a -> +// genSingleTextNode a.Opening +// +> (genAttributesCore a.Attributes) +// +> genSingleTextNode a.Closing +// +> sepNlnWhenWriteBeforeNewlineNotEmpty +// |> genNode a) +// |> genNode node // The inherit keyword should already be printed by the caller let genInheritConstructor (ic: InheritConstructor) = match ic with @@ -3459,14 +3502,18 @@ let genImplicitConstructor (node: ImplicitConstructorNode) = +> sepSpace) node.Self -let hasTriviaAfterLeadingKeyword (identifier: IdentListNode) (accessibility: SingleTextNode option) = +let hasTriviaAfterLeadingKeyword (identifier: IdentListNode) (accessibility: SingleTextNode option) (attributes: MultipleAttributeListNode option) = let beforeAccess = match accessibility with | Some n -> n.HasContentBefore | _ -> false let beforeIdentifier = identifier.HasContentBefore - beforeAccess || beforeIdentifier + let anyAttributeTrivia = + match attributes with + | Some n -> n.HasContentBefore || n.HasContentAfter || (n.AttributeLists |> List.exists (fun a -> a.HasContentBefore || a.HasContentAfter)) + | _ -> false + beforeAccess || beforeIdentifier || anyAttributeTrivia let genTypeDefn (td: TypeDefn) = let typeDefnNode = TypeDefn.TypeDefnNode td @@ -3474,25 +3521,28 @@ let genTypeDefn (td: TypeDefn) = let header = let implicitConstructor = typeName.ImplicitConstructor - // let hasAndKeyword = typeName.LeadingKeyword.Text = "and" - // Workaround for https://github.com/fsprojects/fantomas/issues/628 let hasAttributesAfterLeadingKeyword = match typeName.Attributes with | Some attributes -> Fantomas.FCS.Text.Range.rangeBeforePos typeName.LeadingKeyword.Range attributes.Range.Start | None -> false + // Workaround for https://github.com/fsprojects/fantomas/issues/628 let hasTriviaAfterLeadingKeyword = - hasTriviaAfterLeadingKeyword typeName.Identifier typeName.Accessibility + hasTriviaAfterLeadingKeyword typeName.Identifier typeName.Accessibility typeName.Attributes let hasTriviaBeforeAttributes = match typeName.Attributes with | Some attributes -> attributes.HasContentBefore | None -> false + let shouldAttributesBeAfterLeadingKeyword = + typeName.LeadingKeyword.Text = "and" + || (hasAttributesAfterLeadingKeyword && hasTriviaAfterLeadingKeyword) + genXml typeName.XmlDoc - +> onlyIfNot hasAttributesAfterLeadingKeyword (genAttributes typeName.Attributes) + +> onlyIfNot (shouldAttributesBeAfterLeadingKeyword) (genAttributes typeName.Attributes) +> genSingleTextNode typeName.LeadingKeyword +> onlyIf (hasTriviaAfterLeadingKeyword || hasTriviaBeforeAttributes) indent - +> onlyIf hasAttributesAfterLeadingKeyword (sepSpace +> genAttributesNoNewline typeName.Attributes) + +> onlyIf (shouldAttributesBeAfterLeadingKeyword) (sepSpace +> genOnelinerAttributesWithTrivia typeName.Attributes) +> sepSpace +> genAccessOpt typeName.Accessibility +> genTypeAndParam (genIdentListNode typeName.Identifier) typeName.TypeParameters @@ -4012,7 +4062,7 @@ let genModuleDecl (md: ModuleDecl) = | ModuleDecl.NestedModule node -> // Workaround for https://github.com/fsprojects/fantomas/issues/2867 let hasTriviaAfterLeadingKeyword = - hasTriviaAfterLeadingKeyword node.Identifier node.Accessibility + hasTriviaAfterLeadingKeyword node.Identifier node.Accessibility node.Attributes genXml node.XmlDoc +> genAttributes node.Attributes From 077b420801d3d63b832508b82aae885a0855859d Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Sat, 2 Aug 2025 11:18:39 +0100 Subject: [PATCH 6/9] Format --- .../CompilerDirectivesTests.fs | 17 +++-- src/Fantomas.Core/CodePrinter.fs | 63 ++++++++++--------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs index 2aa95605bc..cf872d9074 100644 --- a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs +++ b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs @@ -3312,7 +3312,7 @@ and #endif [] Y = int """ - + [] let ``directive and attributes after keyword should indent correctly, no defines`` () = formatSourceStringWithDefines @@ -3351,12 +3351,11 @@ type E = struct end #endif """ - - + [] let ``directive and attributes after keyword should indent correctly, FOO`` () = formatSourceStringWithDefines - ["FOO"] + [ "FOO" ] """ type C = Object and @@ -3391,9 +3390,7 @@ type #else #endif """ - - - + [] let ``directive and attributes after keyword should indent correctly, 3174`` () = formatSourceString @@ -3434,8 +3431,6 @@ type #endif """ - - [] let ``directive and attributes after keyword should not be moved out, no defines`` () = formatSourceStringWithDefines @@ -3461,10 +3456,11 @@ type #endif [] D = struct end """ + [] let ``directive and attributes after keyword should not be moved out, FOO`` () = formatSourceStringWithDefines - ["FOO"] + [ "FOO" ] """ #if FOO type A = int @@ -3487,6 +3483,7 @@ and #endif [] D = struct end """ + [] let ``directive and attributes after keyword should not be moved out, 3174`` () = formatSourceString diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 9e6c227837..6a593a6473 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -298,22 +298,17 @@ let genOnelinerAttributes (n: MultipleAttributeListNode option) = |> genNode n ifElse ats.IsEmpty sepNone (genAttrs +> sepSpace) + let partitionOn splitBefore splitAfter items = let folder acc item = match acc with - | [] -> - [[item]] - | (lastItem :: _ as currentGroup) :: restGroups when splitAfter lastItem -> - [item] :: currentGroup :: restGroups - | currentGroup :: restGroups when splitBefore item -> - [item] :: currentGroup :: restGroups - | currentGroup :: restGroups -> - (item :: currentGroup) :: restGroups - - items - |> List.fold folder [] - |> List.map List.rev - |> List.rev + | [] -> [ [ item ] ] + | (lastItem :: _ as currentGroup) :: restGroups when splitAfter lastItem -> + [ item ] :: currentGroup :: restGroups + | currentGroup :: restGroups when splitBefore item -> [ item ] :: currentGroup :: restGroups + | currentGroup :: restGroups -> (item :: currentGroup) :: restGroups + + items |> List.fold folder [] |> List.map List.rev |> List.rev // Like genOnelinerAtrtibutes, but splits the attributelist into chunks if there are contentBefore/After, // Then properly renders those in between @@ -323,24 +318,22 @@ let genOnelinerAttributesWithTrivia (n: MultipleAttributeListNode option) = | Some n -> let attributeLists = n.AttributeLists |> partitionOn (_.HasContentBefore) (_.HasContentAfter) - + col sepNone attributeLists (fun (al) -> - let ats = al |> List.collect _.Attributes + let ats = al |> List.collect _.Attributes + let openingToken = - List.tryHead al - |> Option.map (fun (a: AttributeListNode) -> a.Opening) + List.tryHead al |> Option.map (fun (a: AttributeListNode) -> a.Opening) let closingToken = - List.tryLast al - |> Option.map (fun (a: AttributeListNode) -> a.Closing) + List.tryLast al |> Option.map (fun (a: AttributeListNode) -> a.Closing) + optSingle genSingleTextNode openingToken +> (genAttributesCore ats) +> optSingle genSingleTextNode closingToken +> sepNlnWhenWriteBeforeNewlineNotEmpty - |> genNode (al |> List.head) - ) |> genNode n - - + |> genNode (al |> List.head)) + |> genNode n let genAttributes (node: MultipleAttributeListNode option) = match node with @@ -3502,17 +3495,27 @@ let genImplicitConstructor (node: ImplicitConstructorNode) = +> sepSpace) node.Self -let hasTriviaAfterLeadingKeyword (identifier: IdentListNode) (accessibility: SingleTextNode option) (attributes: MultipleAttributeListNode option) = +let hasTriviaAfterLeadingKeyword + (identifier: IdentListNode) + (accessibility: SingleTextNode option) + (attributes: MultipleAttributeListNode option) + = let beforeAccess = match accessibility with | Some n -> n.HasContentBefore | _ -> false let beforeIdentifier = identifier.HasContentBefore + let anyAttributeTrivia = match attributes with - | Some n -> n.HasContentBefore || n.HasContentAfter || (n.AttributeLists |> List.exists (fun a -> a.HasContentBefore || a.HasContentAfter)) + | Some n -> + n.HasContentBefore + || n.HasContentAfter + || (n.AttributeLists + |> List.exists (fun a -> a.HasContentBefore || a.HasContentAfter)) | _ -> false + beforeAccess || beforeIdentifier || anyAttributeTrivia let genTypeDefn (td: TypeDefn) = @@ -3524,25 +3527,29 @@ let genTypeDefn (td: TypeDefn) = let hasAttributesAfterLeadingKeyword = match typeName.Attributes with - | Some attributes -> Fantomas.FCS.Text.Range.rangeBeforePos typeName.LeadingKeyword.Range attributes.Range.Start + | Some attributes -> + Fantomas.FCS.Text.Range.rangeBeforePos typeName.LeadingKeyword.Range attributes.Range.Start | None -> false // Workaround for https://github.com/fsprojects/fantomas/issues/628 let hasTriviaAfterLeadingKeyword = hasTriviaAfterLeadingKeyword typeName.Identifier typeName.Accessibility typeName.Attributes + let hasTriviaBeforeAttributes = match typeName.Attributes with | Some attributes -> attributes.HasContentBefore | None -> false + let shouldAttributesBeAfterLeadingKeyword = typeName.LeadingKeyword.Text = "and" || (hasAttributesAfterLeadingKeyword && hasTriviaAfterLeadingKeyword) - genXml typeName.XmlDoc +> onlyIfNot (shouldAttributesBeAfterLeadingKeyword) (genAttributes typeName.Attributes) +> genSingleTextNode typeName.LeadingKeyword +> onlyIf (hasTriviaAfterLeadingKeyword || hasTriviaBeforeAttributes) indent - +> onlyIf (shouldAttributesBeAfterLeadingKeyword) (sepSpace +> genOnelinerAttributesWithTrivia typeName.Attributes) + +> onlyIf + (shouldAttributesBeAfterLeadingKeyword) + (sepSpace +> genOnelinerAttributesWithTrivia typeName.Attributes) +> sepSpace +> genAccessOpt typeName.Accessibility +> genTypeAndParam (genIdentListNode typeName.Identifier) typeName.TypeParameters From 341c9fe6f1e871beebaaf7ae217cbc9cf8ffcee0 Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Sat, 2 Aug 2025 11:25:30 +0100 Subject: [PATCH 7/9] Undo change to children order --- src/Fantomas.Core/SyntaxOak.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index d93aa56ae3..c15dc6f0da 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -2145,8 +2145,8 @@ type TypeNameNode override val Children: Node array = [| yield! noa xmlDoc - yield leadingKeyword yield! noa attributes + yield leadingKeyword yield! noa ao yield identifier yield! noa (Option.map TyparDecls.Node typeParams) From f34ea7cecbaeb91d9afa1a24b8f856289207ac70 Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Sat, 2 Aug 2025 11:33:08 +0100 Subject: [PATCH 8/9] Rename function and remove commented code --- src/Fantomas.Core/CodePrinter.fs | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 6a593a6473..4767a30813 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -310,9 +310,7 @@ let partitionOn splitBefore splitAfter items = items |> List.fold folder [] |> List.map List.rev |> List.rev -// Like genOnelinerAtrtibutes, but splits the attributelist into chunks if there are contentBefore/After, -// Then properly renders those in between -let genOnelinerAttributesWithTrivia (n: MultipleAttributeListNode option) = +let genCompactedAttributes (n: MultipleAttributeListNode option) = match n with | None -> sepNone | Some n -> @@ -347,17 +345,6 @@ let genAttributes (node: MultipleAttributeListNode option) = |> genNode a) |> genNode node -// let genAttributesNoNewline (node: MultipleAttributeListNode option) = -// match node with -// | None -> sepNone -// | Some node -> -// col sepNlnUnlessLastEventIsNewline node.AttributeLists (fun a -> -// genSingleTextNode a.Opening -// +> (genAttributesCore a.Attributes) -// +> genSingleTextNode a.Closing -// +> sepNlnWhenWriteBeforeNewlineNotEmpty -// |> genNode a) -// |> genNode node // The inherit keyword should already be printed by the caller let genInheritConstructor (ic: InheritConstructor) = match ic with @@ -3544,12 +3531,10 @@ let genTypeDefn (td: TypeDefn) = || (hasAttributesAfterLeadingKeyword && hasTriviaAfterLeadingKeyword) genXml typeName.XmlDoc - +> onlyIfNot (shouldAttributesBeAfterLeadingKeyword) (genAttributes typeName.Attributes) + +> onlyIfNot shouldAttributesBeAfterLeadingKeyword (genAttributes typeName.Attributes) +> genSingleTextNode typeName.LeadingKeyword +> onlyIf (hasTriviaAfterLeadingKeyword || hasTriviaBeforeAttributes) indent - +> onlyIf - (shouldAttributesBeAfterLeadingKeyword) - (sepSpace +> genOnelinerAttributesWithTrivia typeName.Attributes) + +> onlyIf shouldAttributesBeAfterLeadingKeyword (sepSpace +> genCompactedAttributes typeName.Attributes) +> sepSpace +> genAccessOpt typeName.Accessibility +> genTypeAndParam (genIdentListNode typeName.Identifier) typeName.TypeParameters From 4e60dae22b9696eda2be34cd8fa7bf398eddf760 Mon Sep 17 00:00:00 2001 From: Thomas Boby Date: Sat, 2 Aug 2025 13:06:20 +0100 Subject: [PATCH 9/9] Handle only the logic for `and` --- .../CompilerDirectivesTests.fs | 198 ------------------ src/Fantomas.Core/CodePrinter.fs | 48 ++--- 2 files changed, 17 insertions(+), 229 deletions(-) diff --git a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs index cf872d9074..3a31147190 100644 --- a/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs +++ b/src/Fantomas.Core.Tests/CompilerDirectivesTests.fs @@ -3312,201 +3312,3 @@ and #endif [] Y = int """ - -[] -let ``directive and attributes after keyword should indent correctly, no defines`` () = - formatSourceStringWithDefines - [] - """ -type C = Object -and -#if FOO - [] D = Object -#else - D = Object -#endif - -type -#if FOO - [] D = struct end -#else - E = struct end -#endif -""" - config - |> prepend newline - |> should - equal - """ -type C = Object -and -#if FOO -#else - D = Object -#endif - -type -#if FOO -#else - E = struct end -#endif -""" - -[] -let ``directive and attributes after keyword should indent correctly, FOO`` () = - formatSourceStringWithDefines - [ "FOO" ] - """ -type C = Object -and -#if FOO - [] D = Object -#else - D = Object -#endif - -type -#if FOO - [] D = struct end -#else - E = struct end -#endif -""" - config - |> prepend newline - |> should - equal - """ -type C = Object -and -#if FOO - [] D = Object -#else -#endif - -type -#if FOO - [] D = struct end -#else -#endif -""" - -[] -let ``directive and attributes after keyword should indent correctly, 3174`` () = - formatSourceString - """ -type C = Object -and -#if FOO - [] D = Object -#else - D = Object -#endif - -type -#if FOO - [] D = struct end -#else - E = struct end -#endif -""" - config - |> prepend newline - |> should - equal - """ -type C = Object -and -#if FOO - [] D = Object -#else - D = Object -#endif - -type -#if FOO - [] D = struct end -#else - E = struct end -#endif -""" - -[] -let ``directive and attributes after keyword should not be moved out, no defines`` () = - formatSourceStringWithDefines - [] - """ -#if FOO -type A = int -and -#else -type -#endif - [] D = struct end - -""" - config - |> prepend newline - |> should - equal - """ -#if FOO -#else -type -#endif - [] D = struct end -""" - -[] -let ``directive and attributes after keyword should not be moved out, FOO`` () = - formatSourceStringWithDefines - [ "FOO" ] - """ -#if FOO -type A = int -and -#else -type -#endif - [] D = struct end - -""" - config - |> prepend newline - |> should - equal - """ -#if FOO -type A = int -and -#else -#endif - [] D = struct end -""" - -[] -let ``directive and attributes after keyword should not be moved out, 3174`` () = - formatSourceString - """ -#if FOO -type A = int -and -#else -type -#endif - [] D = struct end - -""" - config - |> prepend newline - |> should - equal - """ -#if FOO -type A = int -and -#else -type -#endif - [] D = struct end -""" diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 4767a30813..de9490919d 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -3482,11 +3482,7 @@ let genImplicitConstructor (node: ImplicitConstructorNode) = +> sepSpace) node.Self -let hasTriviaAfterLeadingKeyword - (identifier: IdentListNode) - (accessibility: SingleTextNode option) - (attributes: MultipleAttributeListNode option) - = +let hasTriviaAfterLeadingKeyword (identifier: IdentListNode) (accessibility: SingleTextNode option) = let beforeAccess = match accessibility with | Some n -> n.HasContentBefore @@ -3494,16 +3490,7 @@ let hasTriviaAfterLeadingKeyword let beforeIdentifier = identifier.HasContentBefore - let anyAttributeTrivia = - match attributes with - | Some n -> - n.HasContentBefore - || n.HasContentAfter - || (n.AttributeLists - |> List.exists (fun a -> a.HasContentBefore || a.HasContentAfter)) - | _ -> false - - beforeAccess || beforeIdentifier || anyAttributeTrivia + beforeAccess || beforeIdentifier let genTypeDefn (td: TypeDefn) = let typeDefnNode = TypeDefn.TypeDefnNode td @@ -3511,35 +3498,34 @@ let genTypeDefn (td: TypeDefn) = let header = let implicitConstructor = typeName.ImplicitConstructor + let hasAndKeyword = typeName.LeadingKeyword.Text = "and" - let hasAttributesAfterLeadingKeyword = - match typeName.Attributes with - | Some attributes -> - Fantomas.FCS.Text.Range.rangeBeforePos typeName.LeadingKeyword.Range attributes.Range.Start - | None -> false // Workaround for https://github.com/fsprojects/fantomas/issues/628 let hasTriviaAfterLeadingKeyword = - hasTriviaAfterLeadingKeyword typeName.Identifier typeName.Accessibility typeName.Attributes + hasTriviaAfterLeadingKeyword typeName.Identifier typeName.Accessibility - let hasTriviaBeforeAttributes = + let hasTriviaInAttributes = match typeName.Attributes with - | Some attributes -> attributes.HasContentBefore + | Some attributes -> + attributes.HasContentBefore + || attributes.HasContentAfter + || attributes.AttributeLists + |> List.exists (fun a -> a.HasContentBefore || a.HasContentAfter) | None -> false - let shouldAttributesBeAfterLeadingKeyword = - typeName.LeadingKeyword.Text = "and" - || (hasAttributesAfterLeadingKeyword && hasTriviaAfterLeadingKeyword) + let shouldIndent = + hasTriviaAfterLeadingKeyword || (hasAndKeyword && hasTriviaInAttributes) genXml typeName.XmlDoc - +> onlyIfNot shouldAttributesBeAfterLeadingKeyword (genAttributes typeName.Attributes) + +> onlyIfNot hasAndKeyword (genAttributes typeName.Attributes) +> genSingleTextNode typeName.LeadingKeyword - +> onlyIf (hasTriviaAfterLeadingKeyword || hasTriviaBeforeAttributes) indent - +> onlyIf shouldAttributesBeAfterLeadingKeyword (sepSpace +> genCompactedAttributes typeName.Attributes) + +> onlyIf shouldIndent indent + +> onlyIf hasAndKeyword (sepSpace +> genCompactedAttributes typeName.Attributes) +> sepSpace +> genAccessOpt typeName.Accessibility +> genTypeAndParam (genIdentListNode typeName.Identifier) typeName.TypeParameters +> onlyIfNot typeName.Constraints.IsEmpty (sepSpace +> genTypeConstraints typeName.Constraints) - +> onlyIf (hasTriviaAfterLeadingKeyword || hasTriviaBeforeAttributes) unindent + +> onlyIf shouldIndent unindent +> leadingExpressionIsMultiline (optSingle (fun imCtor -> sepSpaceBeforeClassConstructor +> genImplicitConstructor imCtor) @@ -4054,7 +4040,7 @@ let genModuleDecl (md: ModuleDecl) = | ModuleDecl.NestedModule node -> // Workaround for https://github.com/fsprojects/fantomas/issues/2867 let hasTriviaAfterLeadingKeyword = - hasTriviaAfterLeadingKeyword node.Identifier node.Accessibility node.Attributes + hasTriviaAfterLeadingKeyword node.Identifier node.Accessibility genXml node.XmlDoc +> genAttributes node.Attributes