diff --git a/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs b/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs index fed44432d9..8c7ce1e0d0 100644 --- a/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs +++ b/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs @@ -4,7 +4,6 @@ open System open System.Diagnostics open System.IO open System.Text -open Fantomas type TemporaryFileCodeSample internal (codeSnippet: string, ?hasByteOrderMark: bool, ?fileName: string) = let hasByteOrderMark = defaultArg hasByteOrderMark false diff --git a/src/Fantomas.Tests/AttributeTests.fs b/src/Fantomas.Tests/AttributeTests.fs index 96ccab4cf0..a55bc5962e 100644 --- a/src/Fantomas.Tests/AttributeTests.fs +++ b/src/Fantomas.Tests/AttributeTests.fs @@ -346,3 +346,105 @@ let ``should preserve multiple return type attributes`` () = formatSourceString false """let f x : [] int = x""" config |> should equal """let f x: [] int = x """ + +[] +let ``attribute, new line, let binding`` () = + formatSourceString false """ + [] + +let bar = 7 +""" config + |> prepend newline + |> should equal """ +[] + +let bar = 7 +""" + +[] +let ``attribute, new line, type declaration`` () = + formatSourceString false """ +[] + +type Bar = Bar of string +""" config + |> prepend newline + |> should equal """ +[] + +type Bar = Bar of string +""" + +[] +let ``attribute, new line, attribute, newline, let binding`` () = + formatSourceString false """ +[] + +[] + +let bar = 7 +""" config + |> prepend newline + |> should equal """ +[] + +[] + +let bar = 7 +""" + +[] +let ``attribute, new line, attribute, line comment, type declaration`` () = + formatSourceString false """ +[] + +[] +// foo +type Text = string +""" config + |> prepend newline + |> should equal """ +[] + +[] +// foo +type Text = string +""" + +[] +let ``attribute, hash directive, attribute, hash directive, type declaration`` () = + formatSourceString false """ +[] +#if FOO +[] +#endif +type Text = string +""" config + |> prepend newline + |> should equal """ +[] +#if FOO +[] +#endif +type Text = string +""" + +[] +let ``attribute, line comment, attribute, new line, record definition field`` () = + formatSourceString false """ +type Commenter = + { [] + // foo + [] + + DisplayName: string } +""" config + |> prepend newline + |> should equal """ +type Commenter = + { [] + // foo + [] + + DisplayName: string } +""" diff --git a/src/Fantomas.Tests/CheckTests.fs b/src/Fantomas.Tests/CheckTests.fs index 3791e3da76..9bb202fdad 100644 --- a/src/Fantomas.Tests/CheckTests.fs +++ b/src/Fantomas.Tests/CheckTests.fs @@ -2,7 +2,6 @@ module Fantomas.Tests.CheckTests open NUnit.Framework open FsUnit -open Fantomas.FormatConfig open Fantomas.FakeHelpers open Fantomas.Tests.TestHelper diff --git a/src/Fantomas.Tests/QueueTests.fs b/src/Fantomas.Tests/QueueTests.fs index 697a36d4c9..9b212a03a5 100644 --- a/src/Fantomas.Tests/QueueTests.fs +++ b/src/Fantomas.Tests/QueueTests.fs @@ -6,7 +6,7 @@ open FsUnit open FsCheck module Queue = - let ofLists xss = (Queue.empty, xss) ||> List.fold (fun q xs -> Queue.append q xs) + let ofLists xss = (Queue.empty, xss) ||> List.fold Queue.append [] let ``Queue.append``() = diff --git a/src/Fantomas.Tests/TypeDeclarationTests.fs b/src/Fantomas.Tests/TypeDeclarationTests.fs index c91ef7aece..5af12b0ba7 100644 --- a/src/Fantomas.Tests/TypeDeclarationTests.fs +++ b/src/Fantomas.Tests/TypeDeclarationTests.fs @@ -1413,3 +1413,43 @@ type OuterType = abstract Apply<'r> : InnerType<'r> -> 'r when 'r : comparison """ + +[] +let ``attribute on type and abstract member followed by type, 949`` () = + formatSourceString false """ +[] +type TimelineOptionsGroupCallbackFunction = + [] + abstract Invoke: group:TimelineGroup * callback:(TimelineGroup option -> unit) -> unit + +type TimelineOptionsGroupEditableType = U2 +""" config + |> prepend newline + |> should equal """ +[] +type TimelineOptionsGroupCallbackFunction = + [] + abstract Invoke: group:TimelineGroup * callback:(TimelineGroup option -> unit) -> unit + +type TimelineOptionsGroupEditableType = U2 +""" + +[] +let ``attribute on type and abstract member followed by let binding`` () = + formatSourceString false """ +[] +type TimelineOptionsGroupCallbackFunction = + [] + abstract Invoke: group:TimelineGroup * callback:(TimelineGroup option -> unit) -> unit + +let myBinding a = 7 +""" config + |> prepend newline + |> should equal """ +[] +type TimelineOptionsGroupCallbackFunction = + [] + abstract Invoke: group:TimelineGroup * callback:(TimelineGroup option -> unit) -> unit + +let myBinding a = 7 +""" diff --git a/src/Fantomas/AstTransformer.fs b/src/Fantomas/AstTransformer.fs index 20453351ef..fa10485a54 100644 --- a/src/Fantomas/AstTransformer.fs +++ b/src/Fantomas/AstTransformer.fs @@ -69,7 +69,7 @@ module private Ast = FsAstNode = modOrNs Childs = [yield! (if isModule = SynModuleOrNamespaceKind.DeclaredNamespace then collectIdents longIdent else []) - yield! attrs |> List.map (visitSynAttributeList modOrNs) + yield! (visitSynAttributeLists range attrs) yield! (decls |> List.map visitSynModuleDecl)]} and visitSynModuleDecl(ast: SynModuleDecl) : Node = @@ -125,7 +125,7 @@ module private Ast = Range = r range Properties = p [] FsAstNode = ast - Childs = attrs |> List.map (visitSynAttributeList ast)} + Childs = visitSynAttributeLists range attrs } | SynModuleDecl.HashDirective(hash,range) -> {Type = "SynModuleDecl.HashDirective" Range = r range @@ -810,7 +810,7 @@ module private Ast = if access.IsSome then yield "access" ==> (access.Value |> visitSynAccess)] FsAstNode = mbrDef Childs = - [yield! attrs |> List.map (visitSynAttributeList mbrDef) + [yield! (visitSynAttributeLists range attrs) yield visitSynSimplePats ctorArgs]} | SynMemberDefn.ImplicitInherit(inheritType,inheritArgs,inheritAlias,range) -> {Type = "SynMemberDefn.ImplicitInherit" @@ -871,7 +871,7 @@ module private Ast = if getSetRange.IsSome then yield "getSetRange" ==> (getSetRange.Value |> r)] FsAstNode = mbrDef Childs = - [yield! attrs |> List.map (visitSynAttributeList mbrDef) + [yield! (visitSynAttributeLists range attrs) if typeOpt.IsSome then yield visitSynType typeOpt.Value yield visitSynExpr synExpr]} @@ -902,7 +902,7 @@ module private Ast = FsAstNode = sp Childs = [yield visitSynSimplePat simplePat - yield! attrs |> List.map (visitSynAttributeList sp)]} + yield! (visitSynAttributeLists range attrs)]} and visitSynSimplePats(sp: SynSimplePats): Node = match sp with @@ -933,7 +933,7 @@ module private Ast = if access.IsSome then yield "access" ==> (access.Value |> visitSynAccess)] FsAstNode = binding Childs = - [yield! attrs |> List.map (visitSynAttributeList binding) + [yield! (visitSynAttributeLists range attrs) yield visitSynValData valData yield visitSynPat headPat if returnInfo.IsSome then yield visitSynBindingReturnInfo returnInfo.Value @@ -960,7 +960,7 @@ module private Ast = if access.IsSome then yield "access" ==> (access.Value |> visitSynAccess)] FsAstNode = svs Childs = - [yield! attrs |> List.map (visitSynAttributeList svs) + [yield! (visitSynAttributeLists range attrs) yield visitSynValTyparDecls explicitValDecls yield visitSynType synType yield visitSynValInfo arity @@ -983,7 +983,7 @@ module private Ast = Properties = p [] FsAstNode = std Childs = - [yield! attrs |> List.map (visitSynAttributeList std) + [yield! (visitSynAttributeLists typar.Range attrs) yield visitSynTypar typar]} and visitSynTypar(typar: SynTypar): Node = @@ -1012,7 +1012,7 @@ module private Ast = FsAstNode = returnInfo Childs = [yield visitSynType typeName - yield! (attrs |> List.map (visitSynAttributeList returnInfo))]} + yield! (visitSynAttributeLists range attrs)]} and visitSynPat(sp: SynPat): Node = match sp with @@ -1052,7 +1052,7 @@ module private Ast = FsAstNode = sp Childs = [yield visitSynPat synPat - yield! attrs |> List.map (visitSynAttributeList sp)]} + yield! (visitSynAttributeLists range attrs)]} | SynPat.Or(synPat,synPat2,range) -> {Type = "SynPat.Or" Range = r range @@ -1177,7 +1177,7 @@ module private Ast = if access.IsSome then yield "access" ==> (access.Value |> visitSynAccess)] FsAstNode = sci Childs = - [yield! (attribs |> List.map (visitSynAttributeList sci)) + [yield! (visitSynAttributeLists range attribs) yield! (typeParams |> List.map(visitSynTyparDecl))]} and visitSynTypeDefnRepr(stdr: SynTypeDefnRepr): Node = @@ -1346,7 +1346,7 @@ module private Ast = if access.IsSome then yield "access" ==> (access.Value |> visitSynAccess)] FsAstNode = sedr Childs = - [yield! attrs |> List.map (visitSynAttributeList sedr) + [yield! (visitSynAttributeLists range attrs) yield visitSynUnionCase unionCase]} and visitSynAttribute(attr: SynAttribute): Node = @@ -1360,10 +1360,22 @@ module private Ast = FsAstNode = attr Childs = [visitSynExpr attr.ArgExpr]} - and visitSynAttributeList (parent:obj) (attrs: SynAttributeList): Node = + and visitSynAttributeLists (parentRange:range) (attrs: SynAttributeList list) : Node list = + match attrs with + | [h] -> visitSynAttributeList parentRange h |> List.singleton + | _::tail -> + let aRanges = + tail + |> List.map (fun a -> a.Range) + |> fun r -> r @ [parentRange] + List.zip attrs aRanges + |> List.map (fun (a,r) -> visitSynAttributeList r a) + | [] -> [] + + and visitSynAttributeList (parentRange:range) (attrs: SynAttributeList): Node = {Type = "SynAttributeList" Range = r attrs.Range - Properties = p ["parent", parent] + Properties = p ["linesBetweenParent", box (parentRange.StartLine - attrs.Range.EndLine - 1)] FsAstNode = attrs Childs = attrs.Attributes |> List.map visitSynAttribute } @@ -1379,7 +1391,7 @@ module private Ast = FsAstNode = uc Childs = [yield visitSynUnionCaseType uct - yield! attrs |> List.map (visitSynAttributeList uc)]} + yield! (visitSynAttributeLists range attrs)]} and visitSynUnionCaseType(uct: SynUnionCaseType) = match uct with @@ -1405,11 +1417,15 @@ module private Ast = Range = r range Properties = p [] FsAstNode = sec - Childs = [yield! attrs |> List.map (visitSynAttributeList sec); yield visitIdent ident]} + Childs = [yield! (visitSynAttributeLists range attrs) + yield visitIdent ident]} and visitSynField(sfield: SynField): Node = match sfield with | Field(attrs,isStatic,ident,typ,_,_,access,range) -> + let parentRange = + Option.map (fun (i:Ident) -> i.idRange) ident |> Option.defaultValue range + {Type = "Field" Range = r range Properties = @@ -1418,7 +1434,7 @@ module private Ast = if access.IsSome then yield "access" ==> (access.Value |> visitSynAccess)] FsAstNode = sfield Childs = - [yield! attrs |> List.map (visitSynAttributeList sfield) + [yield! (visitSynAttributeLists parentRange attrs) yield visitSynType typ]} and visitSynType(st: SynType) = @@ -1560,13 +1576,18 @@ module private Ast = and visitSynArgInfo(sai: SynArgInfo) = match sai with | SynArgInfo(attrs,optional,ident) -> + let parentRange = + ident + |> Option.map (fun i -> i.idRange) + |> Option.defaultValue range.Zero + {Type = "SynArgInfo" Range = noRange Properties = p [if ident.IsSome then yield "ident" ==> i ident.Value yield "optional" ==> optional] FsAstNode = sai - Childs = [yield! attrs |> List.map (visitSynAttributeList sai)]} + Childs = [yield! (visitSynAttributeLists parentRange attrs)]} and visitSynAccess(a: SynAccess) = match a with @@ -1613,7 +1634,7 @@ module private Ast = FsAstNode = modOrNs Childs = [yield! (if isModule = SynModuleOrNamespaceKind.DeclaredNamespace then visitLongIdent longIdent else []) - yield! attrs |> List.map (visitSynAttributeList modOrNs) + yield! (visitSynAttributeLists range attrs) yield! (decls |> List.map visitSynModuleSigDecl)]} and visitSynModuleSigDecl(ast: SynModuleSigDecl) : Node = diff --git a/src/Fantomas/CodePrinter.fs b/src/Fantomas/CodePrinter.fs index de31b2691f..4ad98ff7b0 100644 --- a/src/Fantomas/CodePrinter.fs +++ b/src/Fantomas/CodePrinter.fs @@ -427,7 +427,7 @@ and genAttributes astContext (ats: SynAttributes) = let dontAddNewline = TriviaHelpers.``has content after that ends with`` (fun t -> t.Range = a.Range) - (function | Directive(_) | Newline -> true | _ -> false) + (function | Directive(_) | Newline | Comment(LineCommentOnSingleLine(_)) -> true | _ -> false) ctx.Trivia let chain = acc +> diff --git a/src/Fantomas/Queue.fs b/src/Fantomas/Queue.fs index 64a8cd0613..9b3b9feed3 100644 --- a/src/Fantomas/Queue.fs +++ b/src/Fantomas/Queue.fs @@ -61,7 +61,7 @@ type Queue<'T> (data : list<'T[]>, length : int) = let rec exists xs = match xs with | (arr: _[]) :: tl -> - while r = false && i < arr.Length do + while not r && i < arr.Length do if f arr.[i] then r <- true i <- i + 1 i <- 0 diff --git a/src/Fantomas/Trivia.fs b/src/Fantomas/Trivia.fs index 6e2e0b9f6f..749662f2f8 100644 --- a/src/Fantomas/Trivia.fs +++ b/src/Fantomas/Trivia.fs @@ -69,11 +69,7 @@ let private findFirstNodeAfterLine |> List.tryFind (fun tn -> if tn.Range.StartLine > lineNumber && isNotMemberKeyword tn then - if hasAnonModulesAndOpenStatements - && mainNodeIs "SynModuleOrNamespace.AnonModule" tn then - false - else - true + not (hasAnonModulesAndOpenStatements && mainNodeIs "SynModuleOrNamespace.AnonModule" tn) else false) @@ -147,8 +143,16 @@ let private findConstNodeAfter (nodes: TriviaNodeAssigner list) (range: range) = let private mapNodeToTriviaNode (node: Node) = node.Range |> Option.map (fun range -> - let attributeParent = Map.tryFind "parent" node.Properties - TriviaNodeAssigner(MainNode(node.Type), range, attributeParent)) + let attributeParent = + Map.tryFind "linesBetweenParent" node.Properties + |> Option.bind (fun v -> + match v with + | :? int as i when (i > 0) -> Some i + | _ -> None) + + match attributeParent with + | Some i -> TriviaNodeAssigner(MainNode(node.Type), range, i) + | None -> TriviaNodeAssigner(MainNode(node.Type), range)) let private commentIsAfterLastTriviaNode (triviaNodes: TriviaNodeAssigner list) (range: range) = match List.filter isMainNodeButNotAnonModule triviaNodes with @@ -201,39 +205,12 @@ let private findParsedHashOnLineAndEndswith (triviaNodes: TriviaNodeAssigner lis // The reason for this is that the range of the attribute is not part of the range of the parent binding. // This can lead to weird results when used in CodePrinter. let private triviaBetweenAttributeAndParentBinding (triviaNodes: TriviaNodeAssigner list) line = - let filteredNodes = - triviaNodes - |> List.filter (fun t -> - match t.Type with - | MainNode("SynAttributeList") - | MainNode("SynModuleDecl.Let") - | MainNode("SynModuleDecl.Types") - | MainNode("SynModuleSigDecl.NestedModule") - | MainNode("ValSpfn") - | MainNode("SynMemberDefn.Member") - | MainNode("SynMemberDefn.LetBindings") - | MainNode("Field") -> true - | _ -> false - ) - |> List.pairwise - - filteredNodes |> List.tryFind (function - | f, a when (f.Type = MainNode("Field") - && a.Type = MainNode("SynAttributeList") - && f.Range.StartLine = a.Range.StartLine - && a.Range.StartLine + 1 = f.Range.EndLine) -> true - | a, p when (a.Type = MainNode("SynAttributeList") && a.Range.StartLine < line && a.Range.StartLine = a.Range.EndLine) -> - match p.Type with - | MainNode("SynModuleDecl.Let") when (p.Range.StartLine > line) -> true - | MainNode("SynAttributeList") when (p.Range.StartLine > line) -> - // This is an edge case scenario where the trivia needs to fit between two attributes of the same parent node. - match p.AttributeParent, a.AttributeParent with - | Some pp, Some ap -> pp = ap - | _ -> false - | MainNode("SynModuleDecl.Types") when (p.Range.StartLine > line) -> true - | _ -> false + triviaNodes + |> List.tryFind (fun tn -> + match tn.AttributeLinesBetweenParent with + | Some linesBetween when (linesBetween + tn.Range.EndLine >= line && line > tn.Range.EndLine) -> + true | _ -> false) - |> Option.bind (fun (a,_) -> if a.Type = MainNode("SynAttributeList") then Some a else None) let private findASTNodeOfTypeThatContains (nodes: TriviaNodeAssigner list) typeName range = nodes @@ -257,8 +234,12 @@ let private addTriviaToTriviaNode |> updateTriviaNode (fun tn -> tn.ContentAfter.Add(comment)) triviaNodes | { Item = Comment(LineCommentOnSingleLine(_) as comment); Range = range } -> - findFirstNodeAfterLine triviaNodes range.StartLine hasAnonModulesAndOpenStatements - |> updateTriviaNode (fun tn -> tn.ContentBefore.Add (Comment(comment))) triviaNodes + match triviaBetweenAttributeAndParentBinding triviaNodes range.StartLine with + | Some _ as node -> + updateTriviaNode (fun tn -> tn.ContentAfter.Add(Comment(comment))) triviaNodes node + | None -> + findFirstNodeAfterLine triviaNodes range.StartLine hasAnonModulesAndOpenStatements + |> updateTriviaNode (fun tn -> tn.ContentBefore.Add (Comment(comment))) triviaNodes | { Item = Comment(BlockComment(comment, _,_)); Range = range } -> let nodeAfter = findNodeAfterLineAndColumn triviaNodes range.StartLine range.StartColumn @@ -400,8 +381,8 @@ let collectTrivia tokens lineCount (ast: ParsedInput) = |> filterNodes |> List.choose mapNodeToTriviaNode - let hasAnyAttributes = List.exists (fun (tn:TriviaNodeAssigner) -> match tn.Type with | MainNode("SynAttributeList") -> true | _ -> false) triviaNodesFromAST - let triviaBetweenAttributeAndParentBinding = if hasAnyAttributes then triviaBetweenAttributeAndParentBinding else (fun _ _ -> None) + let hasAnyAttributesWithLinesBetweenParent = List.exists (fun (tn:TriviaNodeAssigner) -> Option.isSome tn.AttributeLinesBetweenParent) triviaNodesFromAST + let triviaBetweenAttributeAndParentBinding = if hasAnyAttributesWithLinesBetweenParent then triviaBetweenAttributeAndParentBinding else (fun _ _ -> None) let triviaNodesFromTokens = TokenParser.getTriviaNodesFromTokens tokens let triviaNodes = triviaNodesFromAST @ triviaNodesFromTokens |> List.sortBy (fun n -> n.Range.Start.Line, n.Range.Start.Column) let hasAnonModulesAndOpenStatements = nodesContainsBothAnonModuleAndOpen triviaNodes diff --git a/src/Fantomas/TriviaTypes.fs b/src/Fantomas/TriviaTypes.fs index 7065da46f6..8bdb20696a 100644 --- a/src/Fantomas/TriviaTypes.fs +++ b/src/Fantomas/TriviaTypes.fs @@ -57,10 +57,10 @@ type TriviaNode = ContentAfter: TriviaContent list Range: range } -type internal TriviaNodeAssigner(nodeType: TriviaNodeType, range: range, ?attributeParent: obj) = +type internal TriviaNodeAssigner(nodeType: TriviaNodeType, range: range, ?linesBetweenParent: int) = member this.Type = nodeType member this.Range = range - member this.AttributeParent = attributeParent + member this.AttributeLinesBetweenParent = linesBetweenParent member val ContentBefore = ResizeArray() with get,set member val ContentItself = Option.None with get,set member val ContentAfter = ResizeArray() with get,set \ No newline at end of file