diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4367b62b1a..297cd3b582 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup .NET Core - uses: actions/setup-dotnet@v1.4.0 + uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - name: Install local tools diff --git a/.github/workflows/myget.yml b/.github/workflows/myget.yml index b8c1fd4119..c0d6c25ef4 100644 --- a/.github/workflows/myget.yml +++ b/.github/workflows/myget.yml @@ -24,7 +24,7 @@ jobs: run: echo Build number is $BUILD_NUMBER - uses: actions/checkout@v2 - name: Setup .NET Core - uses: actions/setup-dotnet@v1.4.0 + uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - name: Install local tools diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4fe0c90bb1..e6690baad7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,15 @@ +### 4.3.0-alpha-005 - 11/2020 + +* Support user-provided end-of-line characters. [#1231](https://github.com/fsprojects/fantomas/issues/1231) +* Fix multiline yield bang in list should be further indented. [#1254](https://github.com/fsprojects/fantomas/issues/1254) +* Fix Or pipe in destructured record should not be splitted. [#1252](https://github.com/fsprojects/fantomas/issues/1252) +* Fix Swap order of private and inline. [#1250](https://github.com/fsprojects/fantomas/issues/1250) +* Fix Comment is lost in enum. [#1247](https://github.com/fsprojects/fantomas/issues/1247) +* Fix Nested if/else/then in short mode. [#1243](https://github.com/fsprojects/fantomas/issues/1243) +* Fix Something doesn't add up in fix for 303. [#1093](https://github.com/fsprojects/fantomas/issues/1093) + ### 4.3.0-alpha-004 - 11/2020 + * Update to FCS 38. [#1240](https://github.com/fsprojects/fantomas/pull/1240) ### 4.3.0-alpha-003 - 11/2020 diff --git a/src/Fantomas.CoreGlobalTool.Tests/ConfigTests.fs b/src/Fantomas.CoreGlobalTool.Tests/ConfigTests.fs index 78b1cc49bc..9722b53589 100644 --- a/src/Fantomas.CoreGlobalTool.Tests/ConfigTests.fs +++ b/src/Fantomas.CoreGlobalTool.Tests/ConfigTests.fs @@ -1,5 +1,6 @@ module Fantomas.CoreGlobalTool.Tests.ConfigTests +open Fantomas open NUnit.Framework open FsUnit open Fantomas.CoreGlobalTool.Tests.TestHelpers @@ -30,3 +31,49 @@ indent_size=2 |> should equal """let a = // foo 9 """ + +[] +let ``end_of_line=cr should throw an exception`` () = + use fileFixture = + new TemporaryFileCodeSample("let a = 9\n") + + use configFixture = + new ConfigurationFile(""" +[*.fs] +end_of_line=cr +""" ) + + let exitCode, output = runFantomasTool fileFixture.Filename + exitCode |> should equal 1 + StringAssert.Contains("Carriage returns are not valid for F# code, please use one of 'lf' or 'crlf'", output) + +let valid_eol_settings = [ "lf"; "crlf" ] + +[] +let ``uses end_of_line setting to write user newlines`` setting = + let newline = + (FormatConfig.EndOfLineStyle.OfConfigString setting) + .Value.NewLineString + + let sampleCode nln = + sprintf "let a = 9%s%slet b = 7%s" nln nln nln + + use fileFixture = + new TemporaryFileCodeSample(sampleCode "\n") + + use configFixture = + new ConfigurationFile(sprintf """ +[*.fs] +end_of_line = %s +""" setting) + + let (exitCode, _) = runFantomasTool fileFixture.Filename + + exitCode |> should equal 0 + + let result = + System.IO.File.ReadAllText(fileFixture.Filename) + + let expected = sampleCode newline + + result |> should equal expected diff --git a/src/Fantomas.CoreGlobalTool.Tests/Fantomas.CoreGlobalTool.Tests.fsproj b/src/Fantomas.CoreGlobalTool.Tests/Fantomas.CoreGlobalTool.Tests.fsproj index 82be489962..aca7d81223 100644 --- a/src/Fantomas.CoreGlobalTool.Tests/Fantomas.CoreGlobalTool.Tests.fsproj +++ b/src/Fantomas.CoreGlobalTool.Tests/Fantomas.CoreGlobalTool.Tests.fsproj @@ -4,7 +4,7 @@ net5.0 false false - 4.3.0-alpha-004 + 4.3.0-alpha-005 FS0988 FS0025 diff --git a/src/Fantomas.CoreGlobalTool/Fantomas.CoreGlobalTool.fsproj b/src/Fantomas.CoreGlobalTool/Fantomas.CoreGlobalTool.fsproj index 2153a76eb1..0f73f5e1c0 100644 --- a/src/Fantomas.CoreGlobalTool/Fantomas.CoreGlobalTool.fsproj +++ b/src/Fantomas.CoreGlobalTool/Fantomas.CoreGlobalTool.fsproj @@ -4,7 +4,7 @@ net5.0 fantomas True - 4.3.0-alpha-004 + 4.3.0-alpha-005 fantomas-tool FS0025 diff --git a/src/Fantomas.CoreGlobalTool/Program.fs b/src/Fantomas.CoreGlobalTool/Program.fs index 05374756e6..900167fb26 100644 --- a/src/Fantomas.CoreGlobalTool/Program.fs +++ b/src/Fantomas.CoreGlobalTool/Program.fs @@ -69,7 +69,7 @@ let isInExcludedDir (fullPath: string) = ".fable" "node_modules" |] |> Set.map (fun dir -> sprintf "%c%s%c" Path.DirectorySeparatorChar dir Path.DirectorySeparatorChar) - |> Set.exists (fun dir -> fullPath.Contains(dir)) + |> Set.exists (fullPath.Contains) let isFSharpFile (s: string) = Set.contains (Path.GetExtension s) extensions @@ -361,19 +361,23 @@ let main argv = if check then inputPath |> runCheckCommand recurse |> exit else - match inputPath, outputPath with - | InputPath.Unspecified, _ -> - eprintfn "Input path is missing..." + try + match inputPath, outputPath with + | InputPath.Unspecified, _ -> + eprintfn "Input path is missing..." + exit 1 + | InputPath.File f, _ when (IgnoreFile.isIgnoredFile f) -> printfn "'%s' was ignored" f + | InputPath.Folder p1, OutputPath.Notknown -> processFolder p1 p1 + | InputPath.File p1, OutputPath.Notknown -> processFile p1 p1 + | InputPath.File p1, OutputPath.IO p2 -> processFile p1 p2 + | InputPath.Folder p1, OutputPath.IO p2 -> processFolder p1 p2 + | InputPath.StdIn s, OutputPath.IO p -> stringToFile s p FormatConfig.Default + | InputPath.StdIn s, OutputPath.Notknown + | InputPath.StdIn s, OutputPath.StdOut -> stringToStdOut s FormatConfig.Default + | InputPath.File p, OutputPath.StdOut -> fileToStdOut p + | InputPath.Folder p, OutputPath.StdOut -> allFiles recurse p |> Seq.iter fileToStdOut + with exn -> + printfn "%s" exn.Message exit 1 - | InputPath.File f, _ when (IgnoreFile.isIgnoredFile f) -> printfn "'%s' was ignored" f - | InputPath.Folder p1, OutputPath.Notknown -> processFolder p1 p1 - | InputPath.File p1, OutputPath.Notknown -> processFile p1 p1 - | InputPath.File p1, OutputPath.IO p2 -> processFile p1 p2 - | InputPath.Folder p1, OutputPath.IO p2 -> processFolder p1 p2 - | InputPath.StdIn s, OutputPath.IO p -> stringToFile s p FormatConfig.Default - | InputPath.StdIn s, OutputPath.Notknown - | InputPath.StdIn s, OutputPath.StdOut -> stringToStdOut s FormatConfig.Default - | InputPath.File p, OutputPath.StdOut -> fileToStdOut p - | InputPath.Folder p, OutputPath.StdOut -> allFiles recurse p |> Seq.iter fileToStdOut 0 diff --git a/src/Fantomas.Extras/EditorConfig.fs b/src/Fantomas.Extras/EditorConfig.fs index b2dd2a582e..db3df814c1 100644 --- a/src/Fantomas.Extras/EditorConfig.fs +++ b/src/Fantomas.Extras/EditorConfig.fs @@ -13,7 +13,10 @@ module Reflection = let values = FSharpValue.GetRecordFields x Seq.zip names values |> Seq.toArray -let supportedProperties = [ "max_line_length"; "indent_size" ] +let supportedProperties = + [ "max_line_length" + "indent_size" + "end_of_line" ] let private toEditorConfigName value = value @@ -36,6 +39,8 @@ let private (|Number|_|) d = let private (|MultilineFormatterType|_|) mft = MultilineFormatterType.OfConfigString mft +let private (|EndOfLineStyle|_|) eol = EndOfLineStyle.OfConfigString eol + let private (|Boolean|_|) b = if b = "true" then Some(box true) elif b = "false" then Some(box false) @@ -48,6 +53,7 @@ let private parseOptionsFromEditorConfig (editorConfig: EditorConfig.Core.FileCo | true, Number n -> n | true, Boolean b -> b | true, MultilineFormatterType mft -> mft + | true, EndOfLineStyle eol -> box eol | _ -> dv) |> fun newValues -> let formatConfigType = FormatConfig.Default.GetType() diff --git a/src/Fantomas.Extras/FakeHelpers.fs b/src/Fantomas.Extras/FakeHelpers.fs index 914ae45878..6b7733f530 100644 --- a/src/Fantomas.Extras/FakeHelpers.fs +++ b/src/Fantomas.Extras/FakeHelpers.fs @@ -44,7 +44,11 @@ let createParsingOptionsFromFile fileName = { FSharpParsingOptions.Default with SourceFiles = [| fileName |] } -let formatContentAsync config (file: string) (originalContent: string) = +let private formatContentInternalAsync (compareWithoutLineEndings: bool) + (config: FormatConfig) + (file: string) + (originalContent: string) + : Async = if IgnoreFile.isIgnoredFile file then async { return IgnoredFile file } else @@ -61,11 +65,17 @@ let formatContentAsync config (file: string) (originalContent: string) = createParsingOptionsFromFile fileName, sharedChecker.Value) - let stripNewlines (s: string) = - System.Text.RegularExpressions.Regex.Replace(s, @"\n|\r", String.Empty) + let contentChanged = + if compareWithoutLineEndings then + let stripNewlines (s: string) = + System.Text.RegularExpressions.Regex.Replace(s, @"\n|\r", String.Empty) - if (stripNewlines originalContent) - <> (stripNewlines formattedContent) then + (stripNewlines originalContent) + <> (stripNewlines formattedContent) + else + originalContent <> formattedContent + + if contentChanged then let! isValid = CodeFormatter.IsValidFSharpCodeAsync (fileName, @@ -83,7 +93,9 @@ let formatContentAsync config (file: string) (originalContent: string) = with ex -> return Error(file, ex) } -let formatFileAsync (file: string) = +let formatContentAsync = formatContentInternalAsync false + +let private formatFileInternalAsync (compareWithoutLineEndings: bool) (file: string) = let config = EditorConfig.readConfiguration file if IgnoreFile.isIgnoredFile file then @@ -92,10 +104,15 @@ let formatFileAsync (file: string) = let originalContent = File.ReadAllText file async { - let! formatted = originalContent |> formatContentAsync config file + let! formatted = + originalContent + |> formatContentInternalAsync compareWithoutLineEndings config file + return formatted } +let formatFileAsync = formatFileInternalAsync false + let formatFilesAsync files = files |> Seq.map formatFileAsync |> Async.Parallel @@ -149,7 +166,7 @@ let checkCode (filenames: seq) = let! formatted = filenames |> Seq.filter (IgnoreFile.isIgnoredFile >> not) - |> Seq.map formatFileAsync + |> Seq.map (formatFileInternalAsync true) |> Async.Parallel let getChangedFile = diff --git a/src/Fantomas.Extras/Fantomas.Extras.fsproj b/src/Fantomas.Extras/Fantomas.Extras.fsproj index 55680bacc7..5c773434e3 100644 --- a/src/Fantomas.Extras/Fantomas.Extras.fsproj +++ b/src/Fantomas.Extras/Fantomas.Extras.fsproj @@ -2,7 +2,7 @@ netstandard2.0 - 4.3.0-alpha-004 + 4.3.0-alpha-005 Utility package for Fantomas FS0025 diff --git a/src/Fantomas.Tests/ContextTests.fs b/src/Fantomas.Tests/ContextTests.fs index 9042b35c65..06513bc8ce 100644 --- a/src/Fantomas.Tests/ContextTests.fs +++ b/src/Fantomas.Tests/ContextTests.fs @@ -11,7 +11,7 @@ open Fantomas let ``sepSpace should not add an additional space if the line ends with a space`` () = let expr = !- "let a = " +> sepSpace let result = dump (expr Context.Default) - result |> should equal "let a = " + result |> should equal "let a =" [] let ``sepColon should not add a space when nothing proceeds it`` () = @@ -54,7 +54,7 @@ let ``sepColon should not add a space when space proceeds it`` () = let ctx = { Context.Default with Config = config } let result = dump (expr ctx) - result |> should equal "let a : " + result |> should equal "let a :" [] let ``don't add space before block comment`` () = @@ -79,7 +79,7 @@ Long comment Long comment -*) """ +*)""" [] let ``nested exceedsMultiline expression should bubble up to parent check`` () = diff --git a/src/Fantomas.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj index 7d4278b465..19d9ca3259 100644 --- a/src/Fantomas.Tests/Fantomas.Tests.fsproj +++ b/src/Fantomas.Tests/Fantomas.Tests.fsproj @@ -1,6 +1,6 @@ - 4.3.0-alpha-004 + 4.3.0-alpha-005 FS0988 net5.0 FS0025 diff --git a/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs b/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs index 87d76bb2b7..a2d0b6d66c 100644 --- a/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs +++ b/src/Fantomas.Tests/FormatConfigEditorConfigurationFileTests.fs @@ -4,9 +4,10 @@ open System open Fantomas open Fantomas.FormatConfig open Fantomas.Extras +open Fantomas.Tests.TestHelper +open FsUnit open NUnit.Framework open System.IO -open Fantomas.Tests.TestHelper let private defaultConfig = FormatConfig.Default let private tempName () = Guid.NewGuid().ToString("N") @@ -311,3 +312,45 @@ fsharp_disable_elmish_syntax = true EditorConfig.readConfiguration fsharpFile.FSharpFile Assert.IsTrue config.DisableElmishSyntax + +[] +let ``end_of_line = cr should throw`` () = + let editorConfig = """ +[*.fs] +end_of_line = cr +""" + + use configFixture = + new ConfigurationFile(defaultConfig, content = editorConfig) + + use fsharpFile = new FSharpFile() + + let ex = + Assert.Throws(fun () -> + EditorConfig.readConfiguration fsharpFile.FSharpFile + |> ignore) + + ex.Message + == "Carriage returns are not valid for F# code, please use one of 'lf' or 'crlf'" + +let valid_eol_settings = + [ EndOfLineStyle.LF + EndOfLineStyle.CRLF ] + +[] +let can_parse_end_of_line_setting (eol: EndOfLineStyle) = + let editorConfig = + sprintf """ +[*.fs] +end_of_line = %s +""" (EndOfLineStyle.ToConfigString eol) + + use configFixture = + new ConfigurationFile(defaultConfig, content = editorConfig) + + use fsharpFile = new FSharpFile() + + let config = + EditorConfig.readConfiguration fsharpFile.FSharpFile + + config.EndOfLine == eol diff --git a/src/Fantomas.Tests/TestHelpers.fs b/src/Fantomas.Tests/TestHelpers.fs index cbd008d90e..d0d1af2b89 100644 --- a/src/Fantomas.Tests/TestHelpers.fs +++ b/src/Fantomas.Tests/TestHelpers.fs @@ -98,7 +98,6 @@ let formatSourceStringWithDefines defines (s: string) config = return CodeFormatterImpl.formatWith ast defines hashTokens formatContext config } |> Async.RunSynchronously - |> CodeFormatterImpl.addNewlineIfNeeded // merge with itself to make #if go on beginning of line String.merge result result diff --git a/src/Fantomas.Tests/TokenParserBoolExprTests.fs b/src/Fantomas.Tests/TokenParserBoolExprTests.fs index b1c5a710b6..ff722a5382 100644 --- a/src/Fantomas.Tests/TokenParserBoolExprTests.fs +++ b/src/Fantomas.Tests/TokenParserBoolExprTests.fs @@ -279,7 +279,7 @@ let ``Hash ifs source format property`` () = ==> lazy (let source = boolExprsToSource es let result = formatSourceString false source config - result |> should equal source))) + if String.isNotNullOrEmpty result then result |> should equal source))) [] let ``get define exprs from unit test with defines in triple quote string`` () = diff --git a/src/Fantomas/CodeFormatterImpl.fs b/src/Fantomas/CodeFormatterImpl.fs index 5aa4a7ad69..4e6f9edcdb 100644 --- a/src/Fantomas/CodeFormatterImpl.fs +++ b/src/Fantomas/CodeFormatterImpl.fs @@ -382,7 +382,7 @@ let formatWith ast defines hashTokens formatContext config = |> Dbg.tee (fun ctx -> printfn "%A" ctx.WriterEvents) |> Context.dump - formattedSourceCode |> String.removeTrailingSpaces + formattedSourceCode let format (checker: FSharpChecker) (parsingOptions: FSharpParsingOptions) config formatContext = async { @@ -402,25 +402,13 @@ let format (checker: FSharpChecker) (parsingOptions: FSharpParsingOptions) confi return merged } -let addNewlineIfNeeded (formattedSourceCode: string) = - // When formatting the whole document, an EOL is required - if formattedSourceCode.EndsWith(Environment.NewLine) - then formattedSourceCode - else formattedSourceCode + Environment.NewLine - /// Format a source string using given config let formatDocument (checker: FSharpChecker) (parsingOptions: FSharpParsingOptions) config formatContext = - async { - let! formattedSourceCode = format checker parsingOptions config formatContext - return addNewlineIfNeeded formattedSourceCode - } + format checker parsingOptions config formatContext /// Format an abstract syntax tree using given config let formatAST ast defines formatContext config = - let formattedSourceCode = - formatWith ast defines [] formatContext config - - addNewlineIfNeeded formattedSourceCode + formatWith ast defines [] formatContext config /// Make a range from (startLine, startCol) to (endLine, endCol) to select some text let makeRange fileName startLine startCol endLine endCol = diff --git a/src/Fantomas/CodePrinter.fs b/src/Fantomas/CodePrinter.fs index 2becb230c3..11a17bd732 100644 --- a/src/Fantomas/CodePrinter.fs +++ b/src/Fantomas/CodePrinter.fs @@ -76,10 +76,11 @@ let addSpaceBeforeParensInFunDef (astContext: ASTContext) (functionOrMethod: str | (_: string), _ -> not isLastPartUppercase | _ -> true -let rec genParsedInput astContext = - function +let rec genParsedInput astContext ast = + match ast with | ImplFile im -> genImpFile astContext im | SigFile si -> genSigFile astContext si + +> ifElseCtx lastWriteEventIsNewline sepNone sepNln (* See https://github.com/fsharp/FSharp.Compiler.Service/blob/master/src/fsharp/ast.fs#L1518 diff --git a/src/Fantomas/Context.fs b/src/Fantomas/Context.fs index f86deec252..714f5f4019 100644 --- a/src/Fantomas/Context.fs +++ b/src/Fantomas/Context.fs @@ -276,13 +276,18 @@ let internal dump (ctx: Context) = ctx.WriterModel.Lines |> List.rev - |> String.concat Environment.NewLine + |> List.skipWhile ((=) "") + |> List.map (fun line -> line.TrimEnd()) + |> String.concat ctx.Config.EndOfLine.NewLineString let internal dumpAndContinue (ctx: Context) = #if DEBUG let m = finalizeWriterModel ctx let lines = m.WriterModel.Lines |> List.rev - let code = String.concat Environment.NewLine lines + + let code = + String.concat ctx.Config.EndOfLine.NewLineString lines + printfn "%s" code #endif ctx diff --git a/src/Fantomas/Fantomas.fsproj b/src/Fantomas/Fantomas.fsproj index cf3278afbd..c721d3beb4 100644 --- a/src/Fantomas/Fantomas.fsproj +++ b/src/Fantomas/Fantomas.fsproj @@ -2,7 +2,7 @@ netstandard2.0 - 4.3.0-alpha-004 + 4.3.0-alpha-005 Source code formatter for F# FS0025 diff --git a/src/Fantomas/FormatConfig.fs b/src/Fantomas/FormatConfig.fs index bb625356fe..18e8bb1fee 100644 --- a/src/Fantomas/FormatConfig.fs +++ b/src/Fantomas/FormatConfig.fs @@ -24,6 +24,36 @@ type MultilineFormatterType = | "number_of_items" -> Some(box MultilineFormatterType.NumberOfItems) | _ -> None +[] +type EndOfLineStyle = + | LF + | CR + | CRLF + member x.NewLineString = + match x with + | LF -> "\n" + | CR -> "\r" + | CRLF -> "\r\n" + + static member FromEnvironment = + match Environment.NewLine with + | "\n" -> LF + | "\r\n" -> CRLF + | other -> failwithf "Unknown system newline string found: %s" other + + static member ToConfigString(eol: EndOfLineStyle) = + match eol with + | EndOfLineStyle.LF -> "lf" + | EndOfLineStyle.CR -> "cr" + | EndOfLineStyle.CRLF -> "crlf" + + static member OfConfigString(eolString: string) = + match eolString with + | "lf" -> Some(EndOfLineStyle.LF) + | "cr" -> failwith "Carriage returns are not valid for F# code, please use one of 'lf' or 'crlf'" + | "crlf" -> Some(EndOfLineStyle.CRLF) + | _ -> None + // NOTE: try to keep this list below in sync with the docs (e.g. Documentation.md) type FormatConfig = { /// Number of spaces for each indentation @@ -61,6 +91,7 @@ type FormatConfig = AlignFunctionSignatureToIndentation: bool AlternativeLongMemberDefinitions: bool DisableElmishSyntax: bool + EndOfLine: EndOfLineStyle /// Pretty printing based on ASTs only StrictMode: bool } @@ -98,4 +129,5 @@ type FormatConfig = AlignFunctionSignatureToIndentation = false AlternativeLongMemberDefinitions = false DisableElmishSyntax = false + EndOfLine = EndOfLineStyle.FromEnvironment StrictMode = false } diff --git a/src/Fantomas/Utils.fs b/src/Fantomas/Utils.fs index 9db67732d9..566b2ab7fc 100644 --- a/src/Fantomas/Utils.fs +++ b/src/Fantomas/Utils.fs @@ -25,12 +25,6 @@ module String = let startsWithOrdinal (prefix: string) (str: string) = str.StartsWith(prefix, StringComparison.Ordinal) - let removeTrailingSpaces (source: string) = - source.Split([| Environment.NewLine |], StringSplitOptions.None) - |> Array.map (fun line -> line.TrimEnd()) - |> fun lines -> String.Join(Environment.NewLine, lines) - |> fun code -> code.TrimStart(Environment.NewLine.ToCharArray()) - let private lengthWithoutSpaces (str: string) = normalizeNewLine str |> fun s ->