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