From dec961fef11168aee1d9dfdf0349a54554755c29 Mon Sep 17 00:00:00 2001 From: nojaf Date: Sat, 31 Jul 2021 15:11:21 +0200 Subject: [PATCH] Refactor FormatDocumentResponse to DU. --- src/Fantomas.Client/Contracts.fs | 14 +- .../DaemonTests.fs | 172 ++++++++++++++---- .../TestHelpers.fs | 9 +- src/Fantomas.CoreGlobalTool/Daemon.fs | 61 +++++-- 4 files changed, 196 insertions(+), 60 deletions(-) diff --git a/src/Fantomas.Client/Contracts.fs b/src/Fantomas.Client/Contracts.fs index 54ad2620da..fa123055ad 100644 --- a/src/Fantomas.Client/Contracts.fs +++ b/src/Fantomas.Client/Contracts.fs @@ -23,10 +23,17 @@ type FormatDocumentRequest = /// File path will be used to identify the .editorconfig options /// Unless the configuration is passed FilePath: string + /// Determines the underlying F# ParsingOptions + IsLastFile: bool /// Overrides the found .editorconfig. Config: IReadOnlyDictionary option } -type FormatDocumentResponse = { Formatted: string } +[] +type FormatDocumentResponse = + | Formatted of filename: string * formattedContent: string + | Unchanged of filename: string + | Error of filename: string * formattingError: Exception + | IgnoredFile of filename: string type FormatSelectionRequest = { SourceCode: string @@ -52,7 +59,10 @@ and FormatSelectionRange = EndColumn = endColumn } end -type FormatSelectionResponse = { Formatted: string } +[] +type FormatSelectionResponse = + | Formatted of filename: string * formattedContent: string + | Error of filename: string * formattingError: Exception type FantomasOption = { Type: string; DefaultValue: string } diff --git a/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs b/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs index 7a65662ec5..1ecbf043b6 100644 --- a/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs +++ b/src/Fantomas.CoreGlobalTool.Tests/DaemonTests.fs @@ -11,45 +11,79 @@ let private assertFormatted (actual: string) (expected: string) : unit = String.normalizeNewLine actual |> should equal (String.normalizeNewLine expected) +let mutable client: FantomasService = Unchecked.defaultof + +[] +let ``create client`` () = + let processStart = getFantomasToolStartInfo "--daemon" + client <- new LSPFantomasService(processStart) + +[] +let ``dispose client`` () = client.Dispose() + [] let ``compare the version with the public api`` () = async { - let processStart = getFantomasToolStartInfo "--daemon" - use client = new LSPFantomasService(processStart) - let! { Version = version } = (client :> FantomasService).VersionAsync() + let! { Version = version } = client.VersionAsync() version |> should equal (CodeFormatter.GetVersion()) } [] -let ``format document`` () = +let ``format implementation file`` () = async { - let processStart = getFantomasToolStartInfo "--daemon" - use client = new LSPFantomasService(processStart) let sourceCode = "module Foobar" use codeFile = new TemporaryFileCodeSample(sourceCode) let request = { SourceCode = sourceCode FilePath = codeFile.Filename + IsLastFile = false Config = None } - let! response = - (client :> FantomasService) - .FormatDocumentAsync(request) + let! response = client.FormatDocumentAsync(request) + + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foobar +" + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + } + +[] +let ``format signature file`` () = + async { + let sourceCode = "module Foobar\n\nval meh : int" + + use codeFile = + new TemporaryFileCodeSample(sourceCode, extension = "fsi") + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + IsLastFile = false + Config = None } + + let! response = client.FormatDocumentAsync(request) - assertFormatted - response.Formatted - "module Foobar + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foobar + +val meh : int " + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" } + [] let ``format document respecting .editorconfig file`` () = async { - let processStart = getFantomasToolStartInfo "--daemon" - use client = new LSPFantomasService(processStart) let sourceCode = "module Foo\n\nlet a = //\n 4" use codeFile = new TemporaryFileCodeSample(sourceCode) @@ -59,26 +93,26 @@ let ``format document respecting .editorconfig file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename + IsLastFile = false Config = None } - let! response = - (client :> FantomasService) - .FormatDocumentAsync(request) + let! response = client.FormatDocumentAsync(request) - assertFormatted - response.Formatted - "module Foo + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foo let a = // 4 " + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" } [] let ``custom configuration has precedence over .editorconfig file`` () = async { - let processStart = getFantomasToolStartInfo "--daemon" - use client = new LSPFantomasService(processStart) let sourceCode = "module Foo\n\nlet a = //\n 4" use codeFile = new TemporaryFileCodeSample(sourceCode) @@ -88,19 +122,86 @@ let ``custom configuration has precedence over .editorconfig file`` () = let request = { SourceCode = sourceCode FilePath = codeFile.Filename + IsLastFile = false Config = Some(readOnlyDict [ "indent_size", "4" ]) } - let! response = - (client :> FantomasService) - .FormatDocumentAsync(request) + let! response = client.FormatDocumentAsync(request) - assertFormatted - response.Formatted - "module Foo + match response with + | FormatDocumentResponse.Formatted (_, formatted) -> + assertFormatted + formatted + "module Foo let a = // 4 " + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + } + +[] +let ``already formatted file returns unchanged`` () = + async { + let sourceCode = "let a = x\n" + + use codeFile = + new TemporaryFileCodeSample(sourceCode, extension = "fsx") + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + IsLastFile = true + Config = Some(readOnlyDict [ "end_of_line", "lf" ]) } + + let! response = client.FormatDocumentAsync(request) + + match response with + | FormatDocumentResponse.Unchanged fileName -> fileName |> should equal codeFile.Filename + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + } + +[] +let ``ignored file returns ignored`` () = + async { + let sourceCode = "let a = x\n" + + use codeFile = + new TemporaryFileCodeSample(sourceCode, extension = "fsx") + + use _ignoreFile = new FantomasIgnoreFile("*.fsx") + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + IsLastFile = true + Config = None } + + let! response = client.FormatDocumentAsync(request) + + match response with + | FormatDocumentResponse.IgnoredFile fileName -> fileName |> should equal codeFile.Filename + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" + } + +[] +let ``format invalid code`` () = + async { + let sourceCode = "module Foobar\n\nlet ziggy =" + use codeFile = new TemporaryFileCodeSample(sourceCode) + + let request = + { SourceCode = sourceCode + FilePath = codeFile.Filename + IsLastFile = false + Config = None } + + let! response = client.FormatDocumentAsync(request) + + match response with + | FormatDocumentResponse.Error (fileName, error) -> + fileName |> should equal codeFile.Filename + StringAssert.StartsWith("Parsing failed with errors:", error.Message) + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" } [] @@ -113,8 +214,6 @@ let x = 4 let y = 5 """ - let processStart = getFantomasToolStartInfo "--daemon" - use client = new LSPFantomasService(processStart) use _codeFile = new TemporaryFileCodeSample(sourceCode) let request: FormatSelectionRequest = @@ -125,14 +224,16 @@ let y = 5 Config = None Range = range } - let! response = - (client :> FantomasService) - .FormatSelectionAsync(request) + let! response = client.FormatSelectionAsync(request) - assertFormatted response.Formatted "let x = 4\n" + match response with + | FormatSelectionResponse.Formatted (fileName, formatted) -> + fileName |> should equal "tmp.fsx" + assertFormatted formatted "let x = 4\n" + | otherResponse -> Assert.Fail $"Unexpected response %A{otherResponse}" } - +(* [] let ``find fantomas tool from working directory`` () = async { @@ -161,3 +262,4 @@ let ``find fantomas tool from working directory`` () = let formattedCode = formattedResponse () } +*) diff --git a/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs b/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs index 8e5454502c..0993f129c5 100644 --- a/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs +++ b/src/Fantomas.CoreGlobalTool.Tests/TestHelpers.fs @@ -12,7 +12,8 @@ type TemporaryFileCodeSample codeSnippet: string, ?hasByteOrderMark: bool, ?fileName: string, - ?subFolder: string + ?subFolder: string, + ?extension: string ) = let hasByteOrderMark = defaultArg hasByteOrderMark false @@ -22,6 +23,8 @@ type TemporaryFileCodeSample | Some fn -> fn | None -> Guid.NewGuid().ToString() + let extension = Option.defaultValue "fs" extension + match subFolder with | Some sf -> let tempFolder = Path.Join(Path.GetTempPath(), sf) @@ -29,8 +32,8 @@ type TemporaryFileCodeSample if not (Directory.Exists(tempFolder)) then Directory.CreateDirectory(tempFolder) |> ignore - Path.Join(tempFolder, sprintf "%s.fs" name) - | None -> Path.Join(Path.GetTempPath(), sprintf "%s.fs" name) + Path.Join(tempFolder, sprintf "%s.%s" name extension) + | None -> Path.Join(Path.GetTempPath(), sprintf "%s.%s" name extension) do (if hasByteOrderMark then diff --git a/src/Fantomas.CoreGlobalTool/Daemon.fs b/src/Fantomas.CoreGlobalTool/Daemon.fs index 5b3a3994ac..1834bf2452 100644 --- a/src/Fantomas.CoreGlobalTool/Daemon.fs +++ b/src/Fantomas.CoreGlobalTool/Daemon.fs @@ -5,14 +5,26 @@ open System.Diagnostics open System.IO open System.Threading open System.Threading.Tasks +open FSharp.Compiler.SourceCodeServices open FSharp.Compiler.Text.Range open FSharp.Compiler.Text.Pos +open Fantomas.Client.Contracts open StreamJsonRpc open Fantomas open Fantomas.SourceOrigin open Fantomas.FormatConfig open Fantomas.Extras.EditorConfig -open Fantomas.Client.Contracts + +let private createParsingOptionsFromFile (isLastFile: bool) (fileName: string) : FSharpParsingOptions = + let additionFile = + if not isLastFile then + let name = Guid.NewGuid().ToString("N") + [ $"{name}.fs" ] + else + List.empty + + { FSharpParsingOptions.Default with + SourceFiles = [| fileName; yield! additionFile |] } type FantomasDaemon(sender: Stream, reader: Stream) as this = let rpc: JsonRpc = JsonRpc.Attach(sender, reader, this) @@ -41,23 +53,32 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = { Version = CodeFormatter.GetVersion() } [] - member _.FormatDocumentAsync(options: FormatDocumentRequest) : Task = + member _.FormatDocumentAsync(request: FormatDocumentRequest) : Task = async { - let config = - match options.Config with - | Some configProperties -> parseOptionsFromEditorConfig configProperties - | None -> readConfiguration options.FilePath - - let! formatted = - CodeFormatter.FormatDocumentAsync( - options.FilePath, - SourceString options.SourceCode, - config, - CodeFormatterImpl.createParsingOptionsFromFile options.FilePath, - CodeFormatterImpl.sharedChecker.Value - ) - - return ({ Formatted = formatted }: FormatDocumentResponse) + if Fantomas.Extras.IgnoreFile.isIgnoredFile request.FilePath then + return FormatDocumentResponse.IgnoredFile request.FilePath + else + let config = + match request.Config with + | Some configProperties -> parseOptionsFromEditorConfig configProperties + | None -> readConfiguration request.FilePath + + try + let! formatted = + CodeFormatter.FormatDocumentAsync( + request.FilePath, + SourceString request.SourceCode, + config, + createParsingOptionsFromFile request.IsLastFile request.FilePath, + CodeFormatterImpl.sharedChecker.Value + ) + + if formatted = request.SourceCode then + return FormatDocumentResponse.Unchanged request.FilePath + else + return FormatDocumentResponse.Formatted(request.FilePath, formatted) + with + | ex -> return FormatDocumentResponse.Error(request.FilePath, ex) } |> Async.StartAsTask @@ -75,15 +96,15 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = let! formatted = CodeFormatter.FormatSelectionAsync( - request.FilePath, + request.FilePath, // TODO: does this really work with FSI?? range, SourceString request.SourceCode, config, - CodeFormatterImpl.createParsingOptionsFromFile request.FilePath, + CodeFormatterImpl.createParsingOptionsFromFile request.FilePath, // Use safe name ?? CodeFormatterImpl.sharedChecker.Value ) - return { Formatted = formatted } + return FormatSelectionResponse.Formatted(request.FilePath, formatted) } |> Async.StartAsTask