From 32d5dfb8b945958f07c87e346ab92c6d27193088 Mon Sep 17 00:00:00 2001 From: Smaug123 Date: Mon, 14 Feb 2022 23:31:07 +0000 Subject: [PATCH 01/10] Only load the .fantomasignore file once, except for the daemon --- src/Fantomas.CoreGlobalTool/Daemon.fs | 3 +- src/Fantomas.CoreGlobalTool/Program.fs | 9 ++-- src/Fantomas.Extras/FakeHelpers.fs | 9 ++-- src/Fantomas.Extras/IgnoreFile.fs | 65 ++++++++++++++++++-------- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/Fantomas.CoreGlobalTool/Daemon.fs b/src/Fantomas.CoreGlobalTool/Daemon.fs index 075d78bf51..977cb60893 100644 --- a/src/Fantomas.CoreGlobalTool/Daemon.fs +++ b/src/Fantomas.CoreGlobalTool/Daemon.fs @@ -15,6 +15,7 @@ open Fantomas open Fantomas.SourceOrigin open Fantomas.FormatConfig open Fantomas.Extras.EditorConfig +open Fantomas.Extras type FantomasDaemon(sender: Stream, reader: Stream) as this = let rpc: JsonRpc = JsonRpc.Attach(sender, reader, this) @@ -44,7 +45,7 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = [] member _.FormatDocumentAsync(request: FormatDocumentRequest) : Task = async { - if Fantomas.Extras.IgnoreFile.isIgnoredFile request.FilePath then + if IgnoreFile.isIgnoredFile (IgnoreFile.find request.FilePath) request.FilePath then return FormatDocumentResponse.IgnoredFile request.FilePath else let config = diff --git a/src/Fantomas.CoreGlobalTool/Program.fs b/src/Fantomas.CoreGlobalTool/Program.fs index 3f6cc3bac7..8cfe02e20a 100644 --- a/src/Fantomas.CoreGlobalTool/Program.fs +++ b/src/Fantomas.CoreGlobalTool/Program.fs @@ -90,7 +90,7 @@ let rec allFiles isRec path = |> Seq.filter (fun f -> isFSharpFile f && not (isInExcludedDir f) - && not (IgnoreFile.isIgnoredFile f)) + && not (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f)) /// Fantomas assumes the input files are UTF-8 /// As is stated in F# language spec: https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf#page=25 @@ -209,7 +209,7 @@ let runCheckCommand (recurse: bool) (inputPath: InputPath) : int = | InputPath.StdIn _ -> eprintfn "No input path provided. Nothing to do." 0 - | InputPath.File f when (IgnoreFile.isIgnoredFile f) -> + | InputPath.File f when (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) f) -> printfn "'%s' was ignored" f 0 | InputPath.File path -> @@ -418,7 +418,7 @@ let main argv = let filesAndFolders (files: string list) (folders: string list) : unit = files |> List.iter (fun file -> - if (IgnoreFile.isIgnoredFile file) then + if (IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file) then printfn "'%s' was ignored" file else processFile file file) @@ -448,7 +448,8 @@ let main argv = | InputPath.Unspecified, _ -> eprintfn "Input path is missing..." exit 1 - | InputPath.File f, _ when (IgnoreFile.isIgnoredFile f) -> printfn "'%s' was ignored" f + | 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 diff --git a/src/Fantomas.Extras/FakeHelpers.fs b/src/Fantomas.Extras/FakeHelpers.fs index cab8d9d041..618c9473dc 100644 --- a/src/Fantomas.Extras/FakeHelpers.fs +++ b/src/Fantomas.Extras/FakeHelpers.fs @@ -44,7 +44,7 @@ let private formatContentInternalAsync (file: string) (originalContent: string) : Async = - if IgnoreFile.isIgnoredFile file then + if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then async { return IgnoredFile file } else async { @@ -99,7 +99,7 @@ let formatContentAsync = formatContentInternalAsync false let private formatFileInternalAsync (compareWithoutLineEndings: bool) (file: string) = let config = EditorConfig.readConfiguration file - if IgnoreFile.isIgnoredFile file then + if IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) file then async { return IgnoredFile file } else let originalContent = File.ReadAllText file @@ -167,7 +167,10 @@ let checkCode (filenames: seq) = async { let! formatted = filenames - |> Seq.filter (IgnoreFile.isIgnoredFile >> not) + |> Seq.filter ( + IgnoreFile.isIgnoredFile (IgnoreFile.current.Force()) + >> not + ) |> Seq.map (formatFileInternalAsync true) |> Async.Parallel diff --git a/src/Fantomas.Extras/IgnoreFile.fs b/src/Fantomas.Extras/IgnoreFile.fs index a3bbeb23a5..8271cdb58f 100644 --- a/src/Fantomas.Extras/IgnoreFile.fs +++ b/src/Fantomas.Extras/IgnoreFile.fs @@ -1,27 +1,39 @@ namespace Fantomas.Extras +open System.IO +open MAB.DotIgnore + +type IgnoreFile = + { Location: FileInfo + IgnoreList: IgnoreList } + [] module IgnoreFile = - open System.IO - open MAB.DotIgnore - [] let IgnoreFileName = ".fantomasignore" - let rec private findIgnoreFile (filePath: string) : string option = - let allParents = - let rec addParent (di: DirectoryInfo) (finalContinuation: string list -> string list) = - if isNull di.Parent then - finalContinuation [ di.FullName ] - else - addParent di.Parent (fun parents -> di.FullName :: parents |> finalContinuation) + /// Find the `.fantomasignore` file above the given filepath, if one exists. + /// Note that this is intended for use only in the daemon; the command-line tool + /// does not support `.fantomasignore` files anywhere other than the current + /// working directory. + let find (filePath: string) : IgnoreFile option = + let rec walkUp (currentDirectory: DirectoryInfo) : IgnoreFile option = + if isNull currentDirectory then + None + else + let potentialFile = + Path.Combine(currentDirectory.FullName, IgnoreFileName) + |> FileInfo - addParent (Directory.GetParent filePath) id + if potentialFile.Exists then + { Location = potentialFile + IgnoreList = IgnoreList(potentialFile.FullName) } + |> Some + else + walkUp currentDirectory.Parent - allParents - |> List.tryFind (fun p -> Path.Combine(p, IgnoreFileName) |> File.Exists) - |> Option.map (fun p -> Path.Combine(p, IgnoreFileName)) + walkUp (FileInfo(filePath).Directory) let private relativePathPrefix = sprintf ".%c" Path.DirectorySeparatorChar @@ -31,17 +43,30 @@ module IgnoreFile = else path - let isIgnoredFile (file: string) = - let fullPath = Path.GetFullPath(file) + /// When executed from the command line, Fantomas only supports a `.fantomasignore` file in the + /// current working directory. This is that `.fantomasignore` file. + let current: Lazy = + lazy + let path = + Path.Combine(System.Environment.CurrentDirectory, IgnoreFileName) + |> FileInfo + + if path.Exists then + { Location = path + IgnoreList = IgnoreList(path.FullName) } + |> Some + else + None - match findIgnoreFile fullPath with + let isIgnoredFile (ignoreFile: IgnoreFile option) (file: string) : bool = + match ignoreFile with | None -> false | Some ignoreFile -> - let ignores = IgnoreList(ignoreFile) + let fullPath = Path.GetFullPath(file) try - let path = removeRelativePathPrefix file - ignores.IsIgnored(path, false) + let path = removeRelativePathPrefix fullPath + ignoreFile.IgnoreList.IsIgnored(path, false) with | ex -> printfn "%A" ex From 2fde842707d1a670973a9df47d6ac0dd07ed2ab5 Mon Sep 17 00:00:00 2001 From: Smaug123 Date: Mon, 14 Feb 2022 23:36:15 +0000 Subject: [PATCH 02/10] Update documentation --- docs/Documentation.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Documentation.md b/docs/Documentation.md index 68290ea6e8..8b10e68e67 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -1264,6 +1264,9 @@ Exclusion applies both to formatting and the format checking. *.fsx ``` +Note that Fantomas only reads the `.fantomasignore` file in its current working directory, if one exists; unlike Git, it does not traverse the filesystem to find an appropriate ignore file. +(Technically this is not true of the Fantomas daemon. The daemon can't rely on being invoked from the right place, and indeed there may not even be a well-defined notion of "right place" for the formatting tasks the daemon is required to perform, so it does search the filesystem.) + ## Using the API See [CodeFormatter.fsi](../src/Fantomas/CodeFormatter.fsi) to view the public API of Fantomas. From 11629de0691407198f31f8177874d5057e63c592 Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sat, 26 Mar 2022 13:22:13 +0000 Subject: [PATCH 03/10] Also traverse upwards in default case --- src/Fantomas.Extras/IgnoreFile.fs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Fantomas.Extras/IgnoreFile.fs b/src/Fantomas.Extras/IgnoreFile.fs index 8271cdb58f..f582904a12 100644 --- a/src/Fantomas.Extras/IgnoreFile.fs +++ b/src/Fantomas.Extras/IgnoreFile.fs @@ -47,16 +47,7 @@ module IgnoreFile = /// current working directory. This is that `.fantomasignore` file. let current: Lazy = lazy - let path = - Path.Combine(System.Environment.CurrentDirectory, IgnoreFileName) - |> FileInfo - - if path.Exists then - { Location = path - IgnoreList = IgnoreList(path.FullName) } - |> Some - else - None + find System.Environment.CurrentDirectory let isIgnoredFile (ignoreFile: IgnoreFile option) (file: string) : bool = match ignoreFile with From a44afaf4f9a83f5cf60ef97ef0a2cb12adb173c3 Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sat, 26 Mar 2022 13:24:56 +0000 Subject: [PATCH 04/10] Make comment less confusing --- src/Fantomas.Extras/IgnoreFile.fs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Fantomas.Extras/IgnoreFile.fs b/src/Fantomas.Extras/IgnoreFile.fs index f582904a12..71e4013338 100644 --- a/src/Fantomas.Extras/IgnoreFile.fs +++ b/src/Fantomas.Extras/IgnoreFile.fs @@ -43,8 +43,9 @@ module IgnoreFile = else path - /// When executed from the command line, Fantomas only supports a `.fantomasignore` file in the - /// current working directory. This is that `.fantomasignore` file. + /// When executed from the command line, Fantomas will not dynamically locate + /// the most appropriate `.fantomasignore` for each input file; it only finds + /// a single `.fantomasignore` file. This is that file. let current: Lazy = lazy find System.Environment.CurrentDirectory From 0d14a80eb2fd19f9f6e9fa91213f2b628cf79e3f Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sat, 26 Mar 2022 13:54:38 +0000 Subject: [PATCH 05/10] Format --- src/Fantomas.Extras/IgnoreFile.fs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Fantomas.Extras/IgnoreFile.fs b/src/Fantomas.Extras/IgnoreFile.fs index 71e4013338..0c25f5438c 100644 --- a/src/Fantomas.Extras/IgnoreFile.fs +++ b/src/Fantomas.Extras/IgnoreFile.fs @@ -46,9 +46,7 @@ module IgnoreFile = /// When executed from the command line, Fantomas will not dynamically locate /// the most appropriate `.fantomasignore` for each input file; it only finds /// a single `.fantomasignore` file. This is that file. - let current: Lazy = - lazy - find System.Environment.CurrentDirectory + let current: Lazy = lazy find System.Environment.CurrentDirectory let isIgnoredFile (ignoreFile: IgnoreFile option) (file: string) : bool = match ignoreFile with From 11ae382f73ea77247e6294e1ab53136e0311a02a Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sat, 26 Mar 2022 14:15:45 +0000 Subject: [PATCH 06/10] Fix logic to relativise to ignorefile location --- src/Fantomas.Extras/IgnoreFile.fs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Fantomas.Extras/IgnoreFile.fs b/src/Fantomas.Extras/IgnoreFile.fs index 0c25f5438c..6ff234b489 100644 --- a/src/Fantomas.Extras/IgnoreFile.fs +++ b/src/Fantomas.Extras/IgnoreFile.fs @@ -37,16 +37,11 @@ module IgnoreFile = let private relativePathPrefix = sprintf ".%c" Path.DirectorySeparatorChar - let private removeRelativePathPrefix (path: string) = - if path.StartsWith(relativePathPrefix) then - path.Substring(2) - else - path - /// When executed from the command line, Fantomas will not dynamically locate /// the most appropriate `.fantomasignore` for each input file; it only finds /// a single `.fantomasignore` file. This is that file. - let current: Lazy = lazy find System.Environment.CurrentDirectory + let current: Lazy = + lazy find (Path.Combine(System.Environment.CurrentDirectory, "_")) let isIgnoredFile (ignoreFile: IgnoreFile option) (file: string) : bool = match ignoreFile with @@ -55,7 +50,12 @@ module IgnoreFile = let fullPath = Path.GetFullPath(file) try - let path = removeRelativePathPrefix fullPath + let path = + if fullPath.StartsWith ignoreFile.Location.Directory.FullName then + fullPath.[ignoreFile.Location.Directory.FullName.Length + 1 ..] + else + fullPath + ignoreFile.IgnoreList.IsIgnored(path, false) with | ex -> From 9732d5f769c71e3371cf0be85bc5b3d54e4d5a80 Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sat, 26 Mar 2022 15:28:27 +0000 Subject: [PATCH 07/10] Refactor to add unit tests --- paket.dependencies | 5 +- paket.lock | 9 ++ src/Fantomas.CoreGlobalTool/Daemon.fs | 5 +- src/Fantomas.Extras/AssemblyInfo.fs | 7 + src/Fantomas.Extras/Fantomas.Extras.fsproj | 1 + src/Fantomas.Extras/IgnoreFile.fs | 35 +++-- src/Fantomas.Tests/Fantomas.Tests.fsproj | 1 + src/Fantomas.Tests/IgnoreFileTests.fs | 154 +++++++++++++++++++++ src/Fantomas/paket.references | 4 +- 9 files changed, 205 insertions(+), 16 deletions(-) create mode 100644 src/Fantomas.Extras/AssemblyInfo.fs create mode 100644 src/Fantomas.Tests/IgnoreFileTests.fs diff --git a/paket.dependencies b/paket.dependencies index 032a95d907..d5f4e6706e 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -7,6 +7,9 @@ nuget FSharp.Core content: none nuget FSharp.Compiler.Service == 41.0.3 nuget FsUnit nuget FsCheck +nuget System.IO.Abstractions 16.1.10 +nuget System.IO.Abstractions == 16.1.10 +nuget System.IO.Abstractions.TestingHelpers 16.1.10 nuget Microsoft.NET.Test.Sdk nuget nunit nuget NUnit3TestAdapter @@ -55,4 +58,4 @@ group client nuget FSharp.Core 5 content: none nuget StreamJsonRpc - nuget SemanticVersioning + nuget SemanticVersioning \ No newline at end of file diff --git a/paket.lock b/paket.lock index 2941eb4869..951a0a694b 100644 --- a/paket.lock +++ b/paket.lock @@ -327,6 +327,10 @@ NUGET System.Runtime (>= 4.3) System.Text.Encoding (>= 4.3) System.Threading.Tasks (>= 4.3) + System.IO.Abstractions (16.1.10) + System.IO.FileSystem.AccessControl (>= 4.7) - restriction: || (&& (== net6.0) (< net5.0)) (&& (== net6.0) (< netstandard2.1)) (== netcoreapp3.1) (== netstandard2.0) + System.IO.Abstractions.TestingHelpers (16.1.10) + System.IO.Abstractions (>= 16.1.10) System.IO.FileSystem (4.3) Microsoft.NETCore.Platforms (>= 1.1) Microsoft.NETCore.Targets (>= 1.1) @@ -336,6 +340,11 @@ NUGET System.Runtime.Handles (>= 4.3) System.Text.Encoding (>= 4.3) System.Threading.Tasks (>= 4.3) + System.IO.FileSystem.AccessControl (5.0) - restriction: || (&& (== net6.0) (< net5.0)) (&& (== net6.0) (< netstandard2.1)) (== netcoreapp3.1) (== netstandard2.0) + System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monoandroid) (< netstandard1.3)) (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (&& (== netcoreapp3.1) (>= monoandroid) (< netstandard1.3)) (&& (== netcoreapp3.1) (>= monotouch)) (&& (== netcoreapp3.1) (< netcoreapp2.0)) (&& (== netcoreapp3.1) (>= xamarinios)) (&& (== netcoreapp3.1) (>= xamarinmac)) (&& (== netcoreapp3.1) (>= xamarintvos)) (&& (== netcoreapp3.1) (>= xamarinwatchos)) (== netstandard2.0) + System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (>= uap10.1)) (&& (== netcoreapp3.1) (< netcoreapp2.0)) (&& (== netcoreapp3.1) (< netcoreapp2.1)) (&& (== netcoreapp3.1) (>= uap10.1)) (== netstandard2.0) + System.Security.AccessControl (>= 5.0) + System.Security.Principal.Windows (>= 5.0) System.IO.FileSystem.Primitives (4.3) System.Runtime (>= 4.3) System.Linq (4.3) diff --git a/src/Fantomas.CoreGlobalTool/Daemon.fs b/src/Fantomas.CoreGlobalTool/Daemon.fs index 977cb60893..12fb098bf9 100644 --- a/src/Fantomas.CoreGlobalTool/Daemon.fs +++ b/src/Fantomas.CoreGlobalTool/Daemon.fs @@ -3,6 +3,7 @@ open System open System.Diagnostics open System.IO +open System.IO.Abstractions open System.Threading open System.Threading.Tasks open FSharp.Compiler.Text.Range @@ -31,6 +32,8 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = let exit () = disconnectEvent.Set() |> ignore + let fs = FileSystem() + do rpc.Disconnected.Add(fun _ -> exit ()) interface IDisposable with @@ -45,7 +48,7 @@ type FantomasDaemon(sender: Stream, reader: Stream) as this = [] member _.FormatDocumentAsync(request: FormatDocumentRequest) : Task = async { - if IgnoreFile.isIgnoredFile (IgnoreFile.find request.FilePath) request.FilePath then + if IgnoreFile.isIgnoredFile (IgnoreFile.find fs IgnoreFile.loadIgnoreList request.FilePath) request.FilePath then return FormatDocumentResponse.IgnoredFile request.FilePath else let config = diff --git a/src/Fantomas.Extras/AssemblyInfo.fs b/src/Fantomas.Extras/AssemblyInfo.fs new file mode 100644 index 0000000000..a9d9814e8f --- /dev/null +++ b/src/Fantomas.Extras/AssemblyInfo.fs @@ -0,0 +1,7 @@ +namespace Fantomas.Extras.AssemblyInfo + +open System.Runtime.CompilerServices + +[] + +do () diff --git a/src/Fantomas.Extras/Fantomas.Extras.fsproj b/src/Fantomas.Extras/Fantomas.Extras.fsproj index aa1732ec28..2013bdb6b8 100644 --- a/src/Fantomas.Extras/Fantomas.Extras.fsproj +++ b/src/Fantomas.Extras/Fantomas.Extras.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Fantomas.Extras/IgnoreFile.fs b/src/Fantomas.Extras/IgnoreFile.fs index 6ff234b489..5dbe3e5974 100644 --- a/src/Fantomas.Extras/IgnoreFile.fs +++ b/src/Fantomas.Extras/IgnoreFile.fs @@ -1,11 +1,13 @@ namespace Fantomas.Extras -open System.IO +open System.IO.Abstractions open MAB.DotIgnore +type IsPathIgnored = IFileInfo -> bool + type IgnoreFile = - { Location: FileInfo - IgnoreList: IgnoreList } + { Location: IFileInfo + IsIgnored: IsPathIgnored } [] module IgnoreFile = @@ -17,37 +19,43 @@ module IgnoreFile = /// Note that this is intended for use only in the daemon; the command-line tool /// does not support `.fantomasignore` files anywhere other than the current /// working directory. - let find (filePath: string) : IgnoreFile option = - let rec walkUp (currentDirectory: DirectoryInfo) : IgnoreFile option = + let find (fs: IFileSystem) (loadIgnoreList: string -> IsPathIgnored) (filePath: string) : IgnoreFile option = + let rec walkUp (currentDirectory: IDirectoryInfo) : IgnoreFile option = if isNull currentDirectory then None else let potentialFile = - Path.Combine(currentDirectory.FullName, IgnoreFileName) - |> FileInfo + fs.Path.Combine(currentDirectory.FullName, IgnoreFileName) + |> fs.FileInfo.FromFileName if potentialFile.Exists then { Location = potentialFile - IgnoreList = IgnoreList(potentialFile.FullName) } + IsIgnored = loadIgnoreList potentialFile.FullName } |> Some else walkUp currentDirectory.Parent - walkUp (FileInfo(filePath).Directory) + walkUp (fs.FileInfo.FromFileName(filePath).Directory) + + let loadIgnoreList (path: string) : IsPathIgnored = + let list = IgnoreList(path) + fun path -> list.IsIgnored(path.FullName, false) - let private relativePathPrefix = sprintf ".%c" Path.DirectorySeparatorChar + let internal current' (fs: IFileSystem) (currentDirectory: string) (loadIgnoreList: string -> IsPathIgnored) = + lazy find fs loadIgnoreList (fs.Path.Combine(currentDirectory, "_")) /// When executed from the command line, Fantomas will not dynamically locate /// the most appropriate `.fantomasignore` for each input file; it only finds /// a single `.fantomasignore` file. This is that file. let current: Lazy = - lazy find (Path.Combine(System.Environment.CurrentDirectory, "_")) + current' (FileSystem()) System.Environment.CurrentDirectory loadIgnoreList let isIgnoredFile (ignoreFile: IgnoreFile option) (file: string) : bool = match ignoreFile with | None -> false | Some ignoreFile -> - let fullPath = Path.GetFullPath(file) + let fs = ignoreFile.Location.FileSystem + let fullPath = fs.Path.GetFullPath(file) try let path = @@ -55,8 +63,9 @@ module IgnoreFile = fullPath.[ignoreFile.Location.Directory.FullName.Length + 1 ..] else fullPath + |> fs.FileInfo.FromFileName - ignoreFile.IgnoreList.IsIgnored(path, false) + ignoreFile.IsIgnored path with | ex -> printfn "%A" ex diff --git a/src/Fantomas.Tests/Fantomas.Tests.fsproj b/src/Fantomas.Tests/Fantomas.Tests.fsproj index a93bd910ea..ad4a7e5c6f 100644 --- a/src/Fantomas.Tests/Fantomas.Tests.fsproj +++ b/src/Fantomas.Tests/Fantomas.Tests.fsproj @@ -92,6 +92,7 @@ + diff --git a/src/Fantomas.Tests/IgnoreFileTests.fs b/src/Fantomas.Tests/IgnoreFileTests.fs new file mode 100644 index 0000000000..7a96c16c95 --- /dev/null +++ b/src/Fantomas.Tests/IgnoreFileTests.fs @@ -0,0 +1,154 @@ +module Fantomas.Tests.IgnoreFileTests + +open System.Collections.Generic +open NUnit.Framework +open FsUnitTyped +open Fantomas.Extras +open System.IO.Abstractions +open System.IO.Abstractions.TestingHelpers + +let private makeFileHierarchy (fs: IFileSystem) (filePaths: string list) : unit = + for path in filePaths do + let fileInfo = fs.FileInfo.FromFileName path + fileInfo.Directory.Create() + fs.File.WriteAllText(fileInfo.FullName, "some text") + +/// A helper method to create a `loadIgnoreList` function for injection into IgnoreFile; +/// this `loadIgnoreList` function will throw if it tries to load the same file twice. +let private oneShotLoader (isIgnored: IsPathIgnored) : (string -> IsPathIgnored) * (unit -> string Set) = + let loadedFiles = HashSet() + + let load ignoreFilePath = + let added = lock loadedFiles (fun () -> loadedFiles.Add ignoreFilePath) + + if added then + isIgnored + else + failwithf "Attempted duplicate file load: %s" ignoreFilePath + + let freeze () = + lock loadedFiles (fun () -> loadedFiles |> Set.ofSeq) + + load, freeze + +[] +let ``IgnoreFile.find returns None if it can't find an ignorefile`` () = + let fs = MockFileSystem() + let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot + + let source = fs.Path.Combine(root, "folder1", "folder2", "SomeSource.fs") + + [ source ] |> makeFileHierarchy fs + + match IgnoreFile.find fs (fun _ -> failwith "not called") source with + | None -> () + | Some ignoreFile -> failwithf "Unexpectedly found an ignorefile: %s" ignoreFile.Location.FullName + +[] +let ``IgnoreFile.find does not crash at the root, ignore file present`` () = + let fs = MockFileSystem() + let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot + + let fileAtRoot = fs.Path.Combine(root, "SomeFile.fs") + + let loadIgnoreList, getLoads = oneShotLoader (fun _ -> failwith "never called") + + let target = fs.Path.Combine(root, ".fantomasignore") + fs.File.WriteAllText(target, "some text") + + let ignoreFile = IgnoreFile.find fs loadIgnoreList fileAtRoot + + match ignoreFile with + | None -> failwith "Failed to find the fantomasignore file at the root" + | Some ignoreFile -> ignoreFile.Location.FullName |> shouldEqual target + + getLoads () |> shouldEqual (Set.ofList [ target ]) + +[] +let ``IgnoreFile.find does not crash at the root, no ignore file present`` () = + let fs = MockFileSystem() + let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot + + let fileAtRoot = fs.Path.Combine(root, "SomeFile.fs") + + let loadIgnoreList, getLoads = oneShotLoader (fun _ -> failwith "never called") + + let ignoreFile = IgnoreFile.find fs loadIgnoreList fileAtRoot + + match ignoreFile with + | Some ignoreFile -> + failwithf "Somehow found a fantomasignore file even though none was present: %s" ignoreFile.Location.FullName + | None -> () + + getLoads () |> shouldBeEmpty + +[] +let ``IgnoreFile.find preferentially finds the fantomasignore next to the source file`` () = + let fs = MockFileSystem() + let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot + + let source = fs.Path.Combine(root, "folder1", "folder2", "SomeSource.fs") + let target = fs.Path.Combine(root, "folder1", "folder2", ".fantomasignore") + + [ source + target + // Another couple, at higher levels of the hierarchy + fs.Path.Combine(root, "folder1", ".fantomasignore") + fs.Path.Combine(root, ".fantomasignore") ] + |> makeFileHierarchy fs + + let loadIgnoreList, getLoads = oneShotLoader (fun _ -> failwith "never called") + + let ignoreFile = + IgnoreFile.find fs loadIgnoreList source + |> Option.get + + ignoreFile.Location.FullName |> shouldEqual target + getLoads () |> shouldEqual (Set.ofList [ target ]) + +[] +let ``IgnoreFile.find can find the fantomasignore one layer up from the source file`` () = + let fs = MockFileSystem() + let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot + + let source = fs.Path.Combine(root, "folder1", "folder2", "SomeSource.fs") + let target = fs.Path.Combine(root, "folder1", ".fantomasignore") + + [ source + target + // Another one, at a higher level of the hierarchy + fs.Path.Combine(root, ".fantomasignore") ] + |> makeFileHierarchy fs + + let loadIgnoreList, getLoads = oneShotLoader (fun _ -> failwith "never called") + + let ignoreFile = + IgnoreFile.find fs loadIgnoreList source + |> Option.get + + ignoreFile.Location.FullName |> shouldEqual target + getLoads () |> shouldEqual (Set.ofList [ target ]) + +[] +let ``IgnoreFile.current' does not load more than once`` () = + let fs = MockFileSystem() + let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot + + let source = fs.Path.Combine(root, "folder1", "folder2", "SomeSource.fs") + let target = fs.Path.Combine(root, "folder1", ".fantomasignore") + + [ source; target ] |> makeFileHierarchy fs + + let loadIgnoreList, getLoads = oneShotLoader (fun _ -> failwith "never called") + + let ignoreFile = + IgnoreFile.current' fs (fs.Path.GetDirectoryName target) loadIgnoreList + + getLoads () |> shouldBeEmpty + + for _ in 1..2 do + let forced = ignoreFile.Force() |> Option.get + forced.Location.FullName |> shouldEqual target + // The second invocation would throw if we were somehow getting the + // singleton wrong and re-invoking the find-and-load. + getLoads () |> shouldEqual (Set.ofList [ target ]) diff --git a/src/Fantomas/paket.references b/src/Fantomas/paket.references index d2919492e9..4d92d1df9e 100644 --- a/src/Fantomas/paket.references +++ b/src/Fantomas/paket.references @@ -1,4 +1,6 @@ FSharp.Compiler.Service FSharp.Core Ionide.KeepAChangelog.Tasks copy_local: true -Dotnet.ReproducibleBuilds copy_local: true \ No newline at end of file +Dotnet.ReproducibleBuilds copy_local: true +System.IO.Abstractions +System.IO.Abstractions.TestingHelpers \ No newline at end of file From 6149af89fbb7dd95028430607792f2af785f1bf1 Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sat, 26 Mar 2022 15:40:03 +0000 Subject: [PATCH 08/10] Fix relative path logic again --- src/Fantomas.Extras/IgnoreFile.fs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Fantomas.Extras/IgnoreFile.fs b/src/Fantomas.Extras/IgnoreFile.fs index 5dbe3e5974..468167d0ab 100644 --- a/src/Fantomas.Extras/IgnoreFile.fs +++ b/src/Fantomas.Extras/IgnoreFile.fs @@ -3,7 +3,9 @@ namespace Fantomas.Extras open System.IO.Abstractions open MAB.DotIgnore -type IsPathIgnored = IFileInfo -> bool +/// The string argument is taken relative to the location +/// of the ignore-file. +type IsPathIgnored = string -> bool type IgnoreFile = { Location: IFileInfo @@ -39,7 +41,7 @@ module IgnoreFile = let loadIgnoreList (path: string) : IsPathIgnored = let list = IgnoreList(path) - fun path -> list.IsIgnored(path.FullName, false) + fun path -> list.IsIgnored(path, false) let internal current' (fs: IFileSystem) (currentDirectory: string) (loadIgnoreList: string -> IsPathIgnored) = lazy find fs loadIgnoreList (fs.Path.Combine(currentDirectory, "_")) @@ -62,8 +64,10 @@ module IgnoreFile = if fullPath.StartsWith ignoreFile.Location.Directory.FullName then fullPath.[ignoreFile.Location.Directory.FullName.Length + 1 ..] else + // This scenario is a bit unexpected - it suggests that we are + // trying to ask an ignorefile whether a file that is outside + // its dependency tree is ignored. fullPath - |> fs.FileInfo.FromFileName ignoreFile.IsIgnored path with From 40cd08c910cad2df2d0937125799889f4839c4eb Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sun, 27 Mar 2022 12:34:34 +0100 Subject: [PATCH 09/10] Move references to minimal locations --- paket.dependencies | 5 +---- src/Fantomas.Extras/paket.references | 3 ++- src/Fantomas.Tests/paket.references | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/paket.dependencies b/paket.dependencies index d5f4e6706e..032a95d907 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -7,9 +7,6 @@ nuget FSharp.Core content: none nuget FSharp.Compiler.Service == 41.0.3 nuget FsUnit nuget FsCheck -nuget System.IO.Abstractions 16.1.10 -nuget System.IO.Abstractions == 16.1.10 -nuget System.IO.Abstractions.TestingHelpers 16.1.10 nuget Microsoft.NET.Test.Sdk nuget nunit nuget NUnit3TestAdapter @@ -58,4 +55,4 @@ group client nuget FSharp.Core 5 content: none nuget StreamJsonRpc - nuget SemanticVersioning \ No newline at end of file + nuget SemanticVersioning diff --git a/src/Fantomas.Extras/paket.references b/src/Fantomas.Extras/paket.references index deb2132611..743f5d9601 100644 --- a/src/Fantomas.Extras/paket.references +++ b/src/Fantomas.Extras/paket.references @@ -2,4 +2,5 @@ FSharp.Core Ionide.KeepAChangelog.Tasks copy_local: true Dotnet.ReproducibleBuilds copy_local: true editorconfig -MAB.DotIgnore \ No newline at end of file +MAB.DotIgnore +System.IO.Abstractions diff --git a/src/Fantomas.Tests/paket.references b/src/Fantomas.Tests/paket.references index 803cf52330..f05c2812fa 100644 --- a/src/Fantomas.Tests/paket.references +++ b/src/Fantomas.Tests/paket.references @@ -4,4 +4,5 @@ FsCheck Microsoft.NET.Test.Sdk NUnit3TestAdapter NunitXml.TestLogger -FSharp.Core \ No newline at end of file +FSharp.Core +System.IO.Abstractions.TestingHelpers From 22d13d83de33cfb5ef615911fc0f52efb7c6dc56 Mon Sep 17 00:00:00 2001 From: Patrick Stevens Date: Sun, 27 Mar 2022 12:36:46 +0100 Subject: [PATCH 10/10] Update Documentation.md after re-adding search --- docs/Documentation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Documentation.md b/docs/Documentation.md index 8b10e68e67..cd7261f4ab 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -1264,8 +1264,8 @@ Exclusion applies both to formatting and the format checking. *.fsx ``` -Note that Fantomas only reads the `.fantomasignore` file in its current working directory, if one exists; unlike Git, it does not traverse the filesystem to find an appropriate ignore file. -(Technically this is not true of the Fantomas daemon. The daemon can't rely on being invoked from the right place, and indeed there may not even be a well-defined notion of "right place" for the formatting tasks the daemon is required to perform, so it does search the filesystem.) +Note that Fantomas only searches for a `.fantomasignore` file in or above its current working directory, if one exists; unlike Git, it does not traverse the filesystem for each input file to find an appropriate ignore file. +(This is not true of the Fantomas daemon. The daemon can't rely on being invoked from the right place, and indeed there may not even be a well-defined notion of "right place" for the formatting tasks the daemon is required to perform, so it does search the filesystem for every file individually.) ## Using the API