Skip to content

Commit

Permalink
Move profile logic to Format.fs and use Spectre (#2770)
Browse files Browse the repository at this point in the history
* Move profile logic to Format.fs and use Spectre

* Rename ProfileInfos -> ProfileInfo
Don't touch Fantomas.Core

* Reuse the oks and unchanged lists from partitionResults for the call to reportProfileInfos

* Show shorter time format.

* Bundle parameters to formatContentAsync and formatFileAsync in a single record FormatParams

* Move profile logic to Format.fs and use Spectre

* Rename ProfileInfos -> ProfileInfo
Don't touch Fantomas.Core

* Remove ProcessResult and use FormatResult for everything

* centralize exception creation and fix wording

* Final nits

---------

Co-authored-by: nojaf <florian.verdonck@outlook.com>
  • Loading branch information
dawedawe and nojaf committed Feb 22, 2023
1 parent fc37811 commit ae0d6c6
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 177 deletions.
229 changes: 141 additions & 88 deletions src/Fantomas/Format.fs
Original file line number Diff line number Diff line change
@@ -1,115 +1,168 @@
module Fantomas.Format
namespace Fantomas

open System
open System.IO
open Fantomas.Core

type ProfileInfo = { LineCount: int; TimeTaken: TimeSpan }

type FormatResult =
| Formatted of filename: string * formattedContent: string
| Unchanged of filename: string
| Formatted of filename: string * formattedContent: string * profileInfo: ProfileInfo option
| Unchanged of filename: string * profileInfo: ProfileInfo option
| InvalidCode of filename: string * formattedContent: string
| Error of filename: string * formattingError: Exception
| IgnoredFile of filename: string

let private formatContentInternalAsync
(compareWithoutLineEndings: bool)
(config: FormatConfig)
(file: string)
(originalContent: string)
: Async<FormatResult> =
if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then
async { return IgnoredFile file }
else
async {
try
let isSignatureFile = Path.GetExtension(file) = ".fsi"
type FormatParams =
{ Config: FormatConfig
CompareWithoutLineEndings: bool
Profile: bool
File: string }

let! { Code = formattedContent } =
CodeFormatter.FormatDocumentAsync(isSignatureFile, originalContent, config)
static member Create(config: FormatConfig, compareWithoutLineEndings: bool, profile: bool, file: string) =
{ Config = config
CompareWithoutLineEndings = compareWithoutLineEndings
Profile = profile
File = file }

let contentChanged =
if compareWithoutLineEndings then
let stripNewlines (s: string) =
System.Text.RegularExpressions.Regex.Replace(s, @"\r", String.Empty)
static member Create(compareWithoutLineEndings: bool, profile: bool, file: string) =
{ Config = EditorConfig.readConfiguration file
CompareWithoutLineEndings = compareWithoutLineEndings
Profile = profile
File = file }

(stripNewlines originalContent) <> (stripNewlines formattedContent)
else
originalContent <> formattedContent
type CheckResult =
{ Errors: (string * exn) list
Formatted: string list }

if contentChanged then
let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isSignatureFile, formattedContent)
member this.HasErrors = List.isNotEmpty this.Errors
member this.NeedsFormatting = List.isNotEmpty this.Formatted
member this.IsValid = List.isEmpty this.Errors && List.isEmpty this.Formatted

if not isValid then
return InvalidCode(filename = file, formattedContent = formattedContent)
module Format =

let private formatContentInternalAsync
(formatParams: FormatParams)
(originalContent: string)
: Async<FormatResult> =
if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) formatParams.File then
async { return IgnoredFile formatParams.File }
else
async {
try
let isSignatureFile = Path.GetExtension(formatParams.File) = ".fsi"

let! { Code = formattedContent }, profileInfo =
if formatParams.Profile then
async {
let sw = Diagnostics.Stopwatch.StartNew()

let! res =
CodeFormatter.FormatDocumentAsync(
isSignatureFile,
originalContent,
formatParams.Config
)

sw.Stop()

let count =
originalContent.Length - originalContent.Replace(Environment.NewLine, "").Length

let profileInfo =
{ LineCount = count
TimeTaken = sw.Elapsed }

return res, Some profileInfo
}
else
async {
let! res =
CodeFormatter.FormatDocumentAsync(
isSignatureFile,
originalContent,
formatParams.Config
)

return res, None
}

let contentChanged =
if formatParams.CompareWithoutLineEndings then
let stripNewlines (s: string) =
System.Text.RegularExpressions.Regex.Replace(s, @"\r", String.Empty)

(stripNewlines originalContent) <> (stripNewlines formattedContent)
else
originalContent <> formattedContent

if contentChanged then
let! isValid = CodeFormatter.IsValidFSharpCodeAsync(isSignatureFile, formattedContent)

if not isValid then
return InvalidCode(filename = formatParams.File, formattedContent = formattedContent)
else
return
Formatted(
filename = formatParams.File,
formattedContent = formattedContent,
profileInfo = profileInfo
)
else
return Formatted(filename = file, formattedContent = formattedContent)
else
return Unchanged(filename = file)
with ex ->
return Error(file, ex)
}
return Unchanged(filename = formatParams.File, profileInfo = profileInfo)
with ex ->
return Error(formatParams.File, ex)
}

let formatContentAsync = formatContentInternalAsync false
let formatContentAsync = formatContentInternalAsync

let private formatFileInternalAsync (compareWithoutLineEndings: bool) (file: string) =
let config = EditorConfig.readConfiguration file
let private formatFileInternalAsync (parms: FormatParams) =
if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) parms.File then
async { return IgnoredFile parms.File }
else

if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then
async { return IgnoredFile file }
else
async {
let! originalContent = File.ReadAllTextAsync parms.File |> Async.AwaitTask

async {
let! originalContent = File.ReadAllTextAsync file |> Async.AwaitTask
let! formatted = originalContent |> formatContentInternalAsync parms

return formatted
}

let formatFileAsync = formatFileInternalAsync

/// Runs a check on the given files and reports the result to the given output:
///
/// * It shows the paths of the files that need formatting
/// * It shows the path and the error message of files that failed the format check
///
/// Returns:
///
/// A record with the file names that were formatted and the files that encounter problems while formatting.
let checkCode (filenames: seq<string>) =
async {
let! formatted =
originalContent
|> formatContentInternalAsync compareWithoutLineEndings config file
filenames
|> Seq.filter (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) >> not)
|> Seq.map (fun f -> formatFileInternalAsync (FormatParams.Create(true, false, f)))
|> Async.Parallel

return formatted
}
let getChangedFile =
function
| FormatResult.Unchanged _
| FormatResult.IgnoredFile _ -> None
| FormatResult.Formatted(f, _, _)
| FormatResult.Error(f, _)
| FormatResult.InvalidCode(f, _) -> Some f

let formatFileAsync = formatFileInternalAsync false
let changes = formatted |> Seq.choose getChangedFile |> Seq.toList

type CheckResult =
{ Errors: (string * exn) list
Formatted: string list }
let getErrors =
function
| FormatResult.Error(f, e) -> Some(f, e)
| _ -> None

member this.HasErrors = List.isNotEmpty this.Errors
member this.NeedsFormatting = List.isNotEmpty this.Formatted
member this.IsValid = List.isEmpty this.Errors && List.isEmpty this.Formatted
let errors = formatted |> Seq.choose getErrors |> Seq.toList

/// Runs a check on the given files and reports the result to the given output:
///
/// * It shows the paths of the files that need formatting
/// * It shows the path and the error message of files that failed the format check
///
/// Returns:
///
/// A record with the file names that were formatted and the files that encounter problems while formatting.
let checkCode (filenames: seq<string>) =
async {
let! formatted =
filenames
|> Seq.filter (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) >> not)
|> Seq.map (formatFileInternalAsync true)
|> Async.Parallel

let getChangedFile =
function
| FormatResult.Unchanged _
| FormatResult.IgnoredFile _ -> None
| FormatResult.Formatted(f, _)
| FormatResult.Error(f, _)
| FormatResult.InvalidCode(f, _) -> Some f

let changes = formatted |> Seq.choose getChangedFile |> Seq.toList

let getErrors =
function
| FormatResult.Error(f, e) -> Some(f, e)
| _ -> None

let errors = formatted |> Seq.choose getErrors |> Seq.toList

return { Errors = errors; Formatted = changes }
}
return { Errors = errors; Formatted = changes }
}
40 changes: 26 additions & 14 deletions src/Fantomas/Format.fsi
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
module Fantomas.Format
namespace Fantomas

open System
open Fantomas.Core

type ProfileInfo = { LineCount: int; TimeTaken: TimeSpan }

type FormatResult =
| Formatted of filename: string * formattedContent: string
| Unchanged of filename: string
| Formatted of filename: string * formattedContent: string * profileInfo: ProfileInfo option
| Unchanged of filename: string * profileInfo: ProfileInfo option
| InvalidCode of filename: string * formattedContent: string
| Error of filename: string * formattingError: Exception
| IgnoredFile of filename: string

val formatContentAsync: (FormatConfig -> string -> string -> Async<FormatResult>)
type FormatParams =
{ Config: FormatConfig
CompareWithoutLineEndings: bool
Profile: bool
File: string }

val formatFileAsync: (string -> Async<FormatResult>)
static member Create: bool * bool * string -> FormatParams
static member Create: FormatConfig * bool * bool * string -> FormatParams

type CheckResult =
{ Errors: (string * exn) list
Expand All @@ -24,12 +31,17 @@ type CheckResult =

member NeedsFormatting: bool

/// Runs a check on the given files and reports the result to the given output:
///
/// * It shows the paths of the files that need formatting
/// * It shows the path and the error message of files that failed the format check
///
/// Returns:
///
/// A record with the file names that were formatted and the files that encounter problems while formatting.
val checkCode: filenames: seq<string> -> Async<CheckResult>
module Format =
val formatContentAsync: (FormatParams -> string -> Async<FormatResult>)

val formatFileAsync: (FormatParams -> Async<FormatResult>)

/// Runs a check on the given files and reports the result to the given output:
///
/// * It shows the paths of the files that need formatting
/// * It shows the path and the error message of files that failed the format check
///
/// Returns:
///
/// A record with the file names that were formatted and the files that encounter problems while formatting.
val checkCode: filenames: seq<string> -> Async<CheckResult>
Loading

0 comments on commit ae0d6c6

Please sign in to comment.