diff --git a/CHANGELOG.md b/CHANGELOG.md index a9534221d0..5b0254017b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog -## [Unreleased] +## [5.0.0-alpha-011] - 2022-07-08 ### Changed * Restore the CodeFormatter.MakeRange public API. [#2306](https://github.com/fsprojects/fantomas/pull/2306) * Update FCS to 'Add arrow to SynType.Fun trivia.', commit 5a5a5f6cd07aa4a8326baa07d4f7af1305ced6f4 * Update FCS to 'Fix setter first', commit 267d0a57f217df756d9ac33c6aa4ffbfe3b53097 * Update style of long if/match expressions (See [fslang-design#646](https://github.com/fsharp/fslang-design/issues/646)). [#2334](https://github.com/fsprojects/fantomas/pull/2334) +* `--force` will now write a file even when the result is invalid. [#2346](https://github.com/fsprojects/fantomas/issues/2346) ### Added * Add setting `fsharp_max_if_then_short_width`. [#2299](https://github.com/fsprojects/fantomas/issues/2299) @@ -14,6 +15,9 @@ ### Fixed * KeepIndentInBranch not respected inside a let and match. [1825](https://github.com/fsprojects/fantomas/issues/1825) +### Removed +* `--stdin`, `--stdout` and `--fsi` flags. [#2346](https://github.com/fsprojects/fantomas/issues/2346) + ## [5.0.0-alpha-010] - 2022-06-27 ### Changed diff --git a/docs-old/Documentation.md b/docs-old/Documentation.md index 1f19df1035..57b2821dbd 100644 --- a/docs-old/Documentation.md +++ b/docs-old/Documentation.md @@ -18,7 +18,7 @@ For the overview how to use the tool, you can type the command dotnet fantomas --help ``` -USAGE: dotnet fantomas [--help] [--recurse] [--force] [--profile] [--fsi ] [--stdin] [--stdout] [--out ] [--check] [--daemon] [--version] [...] +USAGE: dotnet fantomas [--help] [--recurse] [--force] [--profile] [--out ] [--check] [--daemon] [--version] [...] INPUT: @@ -27,12 +27,9 @@ INPUT: OPTIONS: --recurse, -r Process the input folder recursively. - --force Print the source unchanged if it cannot be parsed correctly. + --force Print the output even if it is not valid F# code. For debugging purposes only. --profile Print performance profiling information. - --fsi Read F# source from stdin as F# signatures. - --stdin Read F# source from standard input. - --stdout Write the formatted source code to standard output. - --out Give a valid path for files/folders. Files should have .fs, .fsx, .fsi, .ml or .mli extension only. + --out Give a valid path for files/folders. Files should have .fs, .fsx, .fsi, .ml or .mli extension only. Multiple files/folders are not supported. --check Don't format files, just check if they have changed. Exits with 0 if it's formatted correctly, with 1 if some files need formatting and 99 if there was an internal error --daemon Daemon mode, launches an LSP-like server to can be used by editor tooling. --version, -v Displays the version of Fantomas diff --git a/paket.dependencies b/paket.dependencies index 84ec6ab676..691b753690 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -16,6 +16,7 @@ nuget Ionide.KeepAChangelog.Tasks copy_local: true nuget Dotnet.ReproducibleBuilds copy_local: true github: fsprojects/fantomas:829faa6ba834f99afed9b4434b3a1680536474b2 src/Fantomas/CodePrinter.fs +github: dotnet/fsharp:267d0a57f217df756d9ac33c6aa4ffbfe3b53097 src/Compiler/Checking/CheckDeclarations.fs # F# compiler source github: dotnet/fsharp:267d0a57f217df756d9ac33c6aa4ffbfe3b53097 src/Compiler/FSComp.txt diff --git a/paket.lock b/paket.lock index 452eefe69f..55743c8437 100644 --- a/paket.lock +++ b/paket.lock @@ -182,6 +182,7 @@ GITHUB src/Compiler/AbstractIL/ilpars.fsy (267d0a57f217df756d9ac33c6aa4ffbfe3b53097) src/Compiler/AbstractIL/ilx.fs (267d0a57f217df756d9ac33c6aa4ffbfe3b53097) src/Compiler/AbstractIL/ilx.fsi (267d0a57f217df756d9ac33c6aa4ffbfe3b53097) + src/Compiler/Checking/CheckDeclarations.fs (267d0a57f217df756d9ac33c6aa4ffbfe3b53097) src/Compiler/Facilities/DiagnosticOptions.fs (267d0a57f217df756d9ac33c6aa4ffbfe3b53097) src/Compiler/Facilities/DiagnosticOptions.fsi (267d0a57f217df756d9ac33c6aa4ffbfe3b53097) src/Compiler/Facilities/DiagnosticsLogger.fs (267d0a57f217df756d9ac33c6aa4ffbfe3b53097) diff --git a/src/Fantomas.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj index 3da16645e6..66669f8097 100644 --- a/src/Fantomas.Tests/Fantomas.Tests.fsproj +++ b/src/Fantomas.Tests/Fantomas.Tests.fsproj @@ -27,6 +27,7 @@ + \ No newline at end of file diff --git a/src/Fantomas.Tests/Integration/ForceTests.fs b/src/Fantomas.Tests/Integration/ForceTests.fs new file mode 100644 index 0000000000..6a5734cb9c --- /dev/null +++ b/src/Fantomas.Tests/Integration/ForceTests.fs @@ -0,0 +1,44 @@ +module Fantomas.Tests.Integration.ForceTests + +open System.IO +open NUnit.Framework +open FsUnit +open Fantomas.Tests.TestHelpers + +// The day this test fails because Fantomas can format the file, is the day you can remove this file. + +[] +let ``code that was invalid should be still be written`` () = + let pwd = Path.GetDirectoryName(typeof.Assembly.Location) + + let sourceFile = + Path.Combine( + pwd, + "..", + "..", + "..", + "..", + "..", + "paket-files", + "dotnet", + "fsharp", + "src", + "Compiler", + "Checking", + "CheckDeclarations.fs" + ) + + use outputFixture = new OutputFile() + + let { ExitCode = exitCode; Output = output } = + runFantomasTool $"--force --out {outputFixture.Filename} {sourceFile}" + + exitCode |> should equal 0 + + output + |> should contain "was not valid after formatting" + + output |> should contain "has been written" + + File.Exists outputFixture.Filename + |> should equal true diff --git a/src/Fantomas.Tests/Integration/MultiplePathsTests.fs b/src/Fantomas.Tests/Integration/MultiplePathsTests.fs index ade0d97945..92035b6538 100644 --- a/src/Fantomas.Tests/Integration/MultiplePathsTests.fs +++ b/src/Fantomas.Tests/Integration/MultiplePathsTests.fs @@ -36,42 +36,6 @@ let ``format multiple paths`` () = fileContentMatches FormattedCode fileFixtureOne.Filename fileContentMatches FormattedCode fileFixtureTwo.Filename -[] -let ``format multiple paths cannot be combined with --out`` () = - use config = new ConfigurationFile("[*]\nend_of_line = lf") - - use fileFixtureOne = new TemporaryFileCodeSample(UserCode) - - use fileFixtureTwo = new TemporaryFileCodeSample(UserCode) - - let arguments = - sprintf "\"%s\" \"%s\" --out \"some Folder\"" fileFixtureOne.Filename fileFixtureTwo.Filename - - let { ExitCode = exitCode; Error = error } = runFantomasTool arguments - - exitCode |> should equal 1 - - error - |> should contain "--stdout and --out cannot be combined with multiple files." - -[] -let ``format multiple paths cannot be combined with --stdout`` () = - use config = new ConfigurationFile("[*]\nend_of_line = lf") - - use fileFixtureOne = new TemporaryFileCodeSample(UserCode) - - use fileFixtureTwo = new TemporaryFileCodeSample(UserCode) - - let arguments = - sprintf "\"%s\" \"%s\" --stdout" fileFixtureOne.Filename fileFixtureTwo.Filename - - let { ExitCode = exitCode; Error = error } = runFantomasTool arguments - - exitCode |> should equal 1 - - error - |> should contain "--stdout and --out cannot be combined with multiple files." - [] let ``format multiple paths with recursive flag`` () = use config = new ConfigurationFile("[*]\nend_of_line = lf") diff --git a/src/Fantomas/Format.fs b/src/Fantomas/Format.fs index 65222f5032..bd6c75bf29 100644 --- a/src/Fantomas/Format.fs +++ b/src/Fantomas/Format.fs @@ -32,6 +32,7 @@ exception CodeFormatException of (string * Option) array with type FormatResult = | Formatted of filename: string * formattedContent: string | Unchanged of filename: string + | InvalidCode of filename: string * formattedContent: string | Error of filename: string * formattingError: Exception | IgnoredFile of filename: string @@ -64,10 +65,9 @@ let private formatContentInternalAsync let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isSignatureFile, formattedContent) if not isValid then - raise - <| FormatException "Formatted content is not valid F# code" - - return Formatted(filename = file, formattedContent = formattedContent) + return InvalidCode(filename = file, formattedContent = formattedContent) + else + return Formatted(filename = file, formattedContent = formattedContent) else return Unchanged(filename = file) with ex -> @@ -94,37 +94,6 @@ let private formatFileInternalAsync (compareWithoutLineEndings: bool) (file: str let formatFileAsync = formatFileInternalAsync false -let formatFilesAsync files = - files |> Seq.map formatFileAsync |> Async.Parallel - -let formatCode files = - async { - let! results = formatFilesAsync files - - // Check for formatting errors: - let errors = - results - |> Array.choose (fun x -> - match x with - | Error (file, ex) -> Some(file, Some(ex)) - | _ -> None) - - if not <| Array.isEmpty errors then - raise <| CodeFormatException errors - - // Overwrite source files with formatted content - let result = - results - |> Array.choose (fun x -> - match x with - | Formatted (source, formatted) -> - File.WriteAllText(source, formatted) - Some source - | _ -> None) - - return result - } - type CheckResult = { Errors: (string * exn) list Formatted: string list } @@ -159,7 +128,8 @@ let checkCode (filenames: seq) = | FormatResult.Unchanged _ | FormatResult.IgnoredFile _ -> None | FormatResult.Formatted (f, _) - | FormatResult.Error (f, _) -> Some f + | FormatResult.Error (f, _) + | FormatResult.InvalidCode (f, _) -> Some f let changes = formatted diff --git a/src/Fantomas/Program.fs b/src/Fantomas/Program.fs index eea85f2e5d..cd810a3250 100644 --- a/src/Fantomas/Program.fs +++ b/src/Fantomas/Program.fs @@ -3,9 +3,9 @@ open System.IO open Fantomas.Core open Fantomas open Fantomas.Daemon -open Fantomas.Core.FormatConfig open Argu open System.Text +open Fantomas.Format let extensions = set @@ -19,9 +19,6 @@ type Arguments = | [] Recurse | [] Force | [] Profile - | [] Fsi of string - | [] Stdin - | [] Stdout | [] Out of string | [] Check | [] Daemon @@ -31,13 +28,10 @@ type Arguments = member s.Usage = match s with | Recurse -> "Process the input folder recursively." - | Force -> "Print the source unchanged if it cannot be parsed correctly." + | Force -> "Print the output even if it is not valid F# code. For debugging purposes only." | Out _ -> - "Give a valid path for files/folders. Files should have .fs, .fsx, .fsi, .ml or .mli extension only." + "Give a valid path for files/folders. Files should have .fs, .fsx, .fsi, .ml or .mli extension only. Multiple files/folders are not supported." | Profile -> "Print performance profiling information." - | Fsi _ -> "Read F# source from stdin as F# signatures." - | Stdin -> "Read F# source from standard input." - | Stdout -> "Write the formatted source code to standard output." | Check -> "Don't format files, just check if they have changed. Exits with 0 if it's formatted correctly, with 1 if some files need formatting and 99 if there was an internal error" | Daemon -> "Daemon mode, launches an LSP-like server to can be used by editor tooling." @@ -59,14 +53,12 @@ let time f = type InputPath = | File of string | Folder of string - | StdIn of string | Multiple of files: string list * folder: string list | Unspecified [] type OutputPath = | IO of string - | StdOut | NotKnown let isInExcludedDir (fullPath: string) = @@ -110,46 +102,44 @@ let private hasByteOrderMark file = false /// Format a source string using given config and write to a text writer -let processSourceString isFsiFile s (tw: Choice) config = - let fileName = - match tw, isFsiFile with - | Choice1Of2 _, isFsi -> - let extension = if isFsi then "fsi" else "fs" - sprintf "/tmp.%s" extension - | Choice2Of2 f, _ -> f - +let processSourceString (force: bool) s (fileName: string) config = let writeResult (formatted: string) = - match tw with - | Choice1Of2 tw -> tw.Write(formatted) - | Choice2Of2 path -> - if hasByteOrderMark path then - File.WriteAllText(path, formatted, Encoding.UTF8) - else - File.WriteAllText(path, formatted) + if hasByteOrderMark fileName then + File.WriteAllText(fileName, formatted, Encoding.UTF8) + else + File.WriteAllText(fileName, formatted) - printfn "%s has been written." path + printfn $"%s{fileName} has been written." async { let! formatted = s |> Format.formatContentAsync config fileName match formatted with | Format.FormatResult.Formatted (_, formattedContent) -> formattedContent |> writeResult - | Format.FormatResult.Unchanged file -> printfn "'%s' was unchanged" file - | Format.IgnoredFile file -> printfn "'%s' was ignored" file - | Format.FormatResult.Error (_, ex) -> raise <| ex + | Format.InvalidCode (file, formattedContent) when force -> + printfn $"%s{file} was not valid after formatting." + formattedContent |> writeResult + | Format.FormatResult.Unchanged file -> printfn $"'%s{file}' was unchanged" + | Format.IgnoredFile file -> printfn $"'%s{file}' was ignored" + | Format.FormatResult.Error (_, ex) -> raise ex + | Format.InvalidCode (file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") } |> Async.RunSynchronously /// Format inFile and write to text writer -let processSourceFile inFile (tw: TextWriter) = +let processSourceFile (force: bool) inFile (tw: TextWriter) = async { let! formatted = Format.formatFileAsync inFile match formatted with | Format.FormatResult.Formatted (_, formattedContent) -> tw.Write(formattedContent) + | Format.InvalidCode (file, formattedContent) when force -> + printfn $"%s{file} was not valid after formatting." + tw.Write(formattedContent) | Format.FormatResult.Unchanged _ -> inFile |> File.ReadAllText |> tw.Write - | Format.IgnoredFile file -> printfn "'%s' was ignored" file - | Format.FormatResult.Error (_, ex) -> raise <| ex + | Format.IgnoredFile file -> printfn $"'%s{file}' was ignored" + | Format.FormatResult.Error (_, ex) -> raise ex + | Format.InvalidCode (file, _) -> raise (exn $"Formatting {file} lead to invalid F# code") } |> Async.RunSynchronously @@ -206,8 +196,7 @@ let runCheckCommand (recurse: bool) (inputPath: InputPath) : int = if checkResult.HasErrors then 1 else 99 match inputPath with - | InputPath.Unspecified - | InputPath.StdIn _ -> + | InputPath.Unspecified _ -> eprintfn "No input path provided. Nothing to do." 0 | InputPath.File f when (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f) -> @@ -248,14 +237,9 @@ let main argv = let results = parser.ParseCommandLine argv let outputPath = - let hasStdout = results.Contains <@ Arguments.Stdout @> - - if hasStdout then - OutputPath.StdOut - else - match results.TryGetResult <@ Arguments.Out @> with - | Some output -> OutputPath.IO output - | None -> OutputPath.NotKnown + match results.TryGetResult <@ Arguments.Out @> with + | Some output -> OutputPath.IO output + | None -> OutputPath.NotKnown let inputPath = let maybeInput = results.TryGetResult <@ Arguments.Input @> @@ -287,28 +271,17 @@ let main argv = let filesAndFolders = loop inputs id InputPath.Multiple filesAndFolders - | None -> - let hasStdin = results.Contains <@ Arguments.Stdin @> - - if hasStdin then - let stdInInput = readFromStdin StdInLineLimit - - match stdInInput with - | Some input -> InputPath.StdIn input - | None -> InputPath.Unspecified - else - InputPath.Unspecified + | None -> InputPath.Unspecified let force = results.Contains <@ Arguments.Force @> let profile = results.Contains <@ Arguments.Profile @> - let fsi = results.Contains <@ Arguments.Fsi @> let recurse = results.Contains <@ Arguments.Recurse @> let version = results.TryGetResult <@ Arguments.Version @> - let fileToFile (inFile: string) (outFile: string) = + let fileToFile (force: bool) (inFile: string) (outFile: string) = try - printfn "Processing %s" inFile + printfn $"Processing %s{inFile}" let hasByteOrderMark = hasByteOrderMark inFile use buffer = @@ -325,63 +298,39 @@ let main argv = |> Seq.length |> printfn "Line count: %i" - time (fun () -> processSourceFile inFile buffer) + time (fun () -> processSourceFile force inFile buffer) else - processSourceFile inFile buffer + processSourceFile force inFile buffer buffer.Flush() printfn "%s has been written." outFile with exn -> - eprintfn "The following exception occurred while formatting %s: %O" inFile exn - - if force then - File.WriteAllText(outFile, File.ReadAllText inFile) - printfn "Force writing original contents to %s" outFile - reraise () - let stringToFile (s: string) (outFile: string) config = + let stringToFile (force: bool) (s: string) (outFile: string) config = try - let fsi = Path.GetExtension(outFile) = ".fsi" - if profile then printfn "Line count: %i" (s.Length - s.Replace(Environment.NewLine, "").Length) - time (fun () -> processSourceString fsi s (Choice2Of2 outFile) config) + time (fun () -> processSourceString force s outFile config) else - processSourceString fsi s (Choice2Of2 outFile) config + processSourceString force s outFile config with exn -> - eprintfn "The following exception occurs while formatting stdin: %O" exn - - if force then - File.WriteAllText(outFile, s) - printfn "Force writing original contents to %s." outFile - - reraise () - - let stringToStdOut s config = - try - use buffer = new StringWriter() :> TextWriter - processSourceString fsi s (Choice1Of2 buffer) config - stdout.Write(buffer.ToString()) - with exn -> - eprintfn "The following exception occurs while formatting stdin: %O" exn - if force then stdout.Write(s) reraise () - let processFile inputFile outputFile = + let processFile force inputFile outputFile = if inputFile <> outputFile then - fileToFile inputFile outputFile + fileToFile force inputFile outputFile else printfn "Processing %s" inputFile let content = File.ReadAllText inputFile let config = EditorConfig.readConfiguration inputFile - stringToFile content inputFile config + stringToFile force content inputFile config - let processFolder inputFolder outputFolder = + let processFolder force inputFolder outputFolder = if not <| Directory.Exists(outputFolder) then Directory.CreateDirectory(outputFolder) |> ignore @@ -396,39 +345,25 @@ let main argv = else i - processFile i o) - - let fileToStdOut inFile = - try - use buffer = new StringWriter() - // Don't record running time when output formatted content to console - processSourceFile inFile buffer - stdout.Write(buffer.ToString()) - with exn -> - eprintfn "The following exception occurred while formatting %s: %O" inFile exn - - if force then - stdout.Write(File.ReadAllText inFile) - - reraise () + processFile force i o) - let filesAndFolders (files: string list) (folders: string list) : unit = + let filesAndFolders force (files: string list) (folders: string list) : unit = files |> List.iter (fun file -> if (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file) then printfn "'%s' was ignored" file else - processFile file file) + processFile force file file) folders - |> List.iter (fun folder -> processFolder folder folder) + |> List.iter (fun folder -> processFolder force folder folder) let check = results.Contains <@ Arguments.Check @> let isDaemon = results.Contains <@ Arguments.Daemon @> if Option.isSome version then let version = CodeFormatter.GetVersion() - printfn "Fantomas v%s" version + printfn $"Fantomas v%s{version}" elif isDaemon then let daemon = new FantomasDaemon(Console.OpenStandardOutput(), Console.OpenStandardInput()) @@ -447,21 +382,14 @@ let main argv = exit 1 | InputPath.File f, _ when (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) 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 - | InputPath.Multiple _, - (OutputPath.StdOut - | OutputPath.IO _) -> - eprintfn "--stdout and --out cannot be combined with multiple files." + | InputPath.Folder p1, OutputPath.NotKnown -> processFolder force p1 p1 + | InputPath.File p1, OutputPath.NotKnown -> processFile force p1 p1 + | InputPath.File p1, OutputPath.IO p2 -> processFile force p1 p2 + | InputPath.Folder p1, OutputPath.IO p2 -> processFolder force p1 p2 + | InputPath.Multiple (files, folders), OutputPath.NotKnown -> filesAndFolders force files folders + | InputPath.Multiple _, OutputPath.IO _ -> + eprintfn "Multiple input files are not supported with the --out flag." exit 1 - | InputPath.Multiple (files, folders), OutputPath.NotKnown -> filesAndFolders files folders with exn -> printfn "%s" exn.Message exit 1