-
-
Notifications
You must be signed in to change notification settings - Fork 194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make IgnoreFile
more efficient
#2086
Closed
Closed
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
a82a7b0
Initial implementation of a mutable-cache IgnoreFiles
Smaug123 56fbb68
Add tests
Smaug123 d901f92
Revert package change
Smaug123 e6e523e
Format
Smaug123 bbf1b84
Make comment less wrong
Smaug123 996083a
Make comment more scary
Smaug123 db189d2
Update paket lockfile
Smaug123 b60f2a8
Fix test on Windows
Smaug123 1f5486e
Switch to a mailbox processor, but without cache invalidation yet
Smaug123 8f21c8c
Format
Smaug123 f909577
Plumb the mailbox implementation through to the daemon
Smaug123 69d9657
Merge branch 'master' into ignore-file
Smaug123 bb01625
Add an example cache invalidation policy
Smaug123 a052513
Merge branch 'ignore-file' of github.com:fsprojects/fantomas into ign…
Smaug123 63b286a
Switch to asynchronous purging
Smaug123 2db6ad7
Delete the cancellationtoken which interacts very poorly with the Qui…
Smaug123 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace Fantomas.Extras.AssemblyInfo | ||
|
||
open System.Runtime.CompilerServices | ||
|
||
[<assembly: InternalsVisibleTo("Fantomas.Tests")>] | ||
|
||
do () |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,44 +3,83 @@ namespace Fantomas.Extras | |
[<RequireQualifiedAccess>] | ||
module IgnoreFile = | ||
|
||
open System.IO | ||
open System.Collections.Concurrent | ||
open System.IO.Abstractions | ||
open MAB.DotIgnore | ||
|
||
[<Literal>] | ||
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 ] | ||
let internal findIgnoreFile<'a> | ||
(fs: IFileSystem) | ||
(readFile: string -> 'a) | ||
(ignoreFiles: ConcurrentDictionary<string, 'a option>) | ||
(filePath: string) | ||
: 'a option = | ||
// Note that this function has side effects: it mutates the state `ignoreFiles`. | ||
let rec findIgnoreFileAbove (path: IDirectoryInfo) : 'a option = | ||
let potentialFile = fs.Path.Combine(path.FullName, IgnoreFileName) | ||
|
||
match ignoreFiles.TryGetValue path.FullName with | ||
| true, Some f -> Some f | ||
| true, None -> | ||
// A slight inefficiency here, in exchange for making the code a lot shorter: | ||
// if we've already computed that there is no .fantomasignore file immediately at this level, | ||
// we keep looking upwards in our knowledge of the file hierarchy. | ||
// (This is inefficient: we could in theory have stored the actual final result at all levels | ||
// once we computed it the first time.) | ||
if isNull path.Parent then | ||
None | ||
else | ||
addParent di.Parent (fun parents -> di.FullName :: parents |> finalContinuation) | ||
findIgnoreFileAbove path.Parent | ||
| _, _ -> | ||
let result = | ||
if fs.File.Exists potentialFile then | ||
readFile potentialFile |> Some | ||
else | ||
None | ||
|
||
addParent (Directory.GetParent filePath) id | ||
ignoreFiles.TryAdd(path.FullName, result) | ||
// We don't care if another thread beat us to this, they'll have | ||
// inserted exactly what we wanted to insert anyway | ||
|> ignore | ||
|
||
allParents | ||
|> List.tryFind (fun p -> Path.Combine(p, IgnoreFileName) |> File.Exists) | ||
|> Option.map (fun p -> Path.Combine(p, IgnoreFileName)) | ||
match result with | ||
| None -> | ||
if isNull path.Parent then | ||
None | ||
else | ||
findIgnoreFileAbove path.Parent | ||
| Some _ -> result | ||
|
||
let private relativePathPrefix = sprintf ".%c" Path.DirectorySeparatorChar | ||
findIgnoreFileAbove (fs.Directory.GetParent filePath) | ||
|
||
let private removeRelativePathPrefix (path: string) = | ||
if path.StartsWith(relativePathPrefix) then | ||
let private removeRelativePathPrefix (fs: IFileSystem) (path: string) = | ||
if | ||
path.StartsWith(sprintf ".%c" fs.Path.DirectorySeparatorChar) | ||
|| path.StartsWith(sprintf ".%c" fs.Path.AltDirectorySeparatorChar) | ||
then | ||
path.Substring(2) | ||
else | ||
path | ||
|
||
/// Global static state. | ||
/// Store of the IgnoreFiles present in each folder discovered so far. | ||
/// This is to save repeatedly hitting the disk for each file, and to save | ||
/// loading the IgnoreLists from the disk repeatedly (which is nontrivially expensive!). | ||
let private ignoreFiles: ConcurrentDictionary<string, IgnoreList option> = | ||
ConcurrentDictionary() | ||
|
||
let isIgnoredFile (file: string) = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The daemon is using this file function so I guess any changes to the content of the ignore will not be picked up. |
||
let fullPath = Path.GetFullPath(file) | ||
let fs = FileSystem() | ||
let fullPath = fs.Path.GetFullPath(file) | ||
|
||
match findIgnoreFile fullPath with | ||
match findIgnoreFile fs IgnoreList ignoreFiles fullPath with | ||
| None -> false | ||
| Some ignoreFile -> | ||
let ignores = IgnoreList(ignoreFile) | ||
| Some ignores -> | ||
|
||
try | ||
let path = removeRelativePathPrefix file | ||
let path = removeRelativePathPrefix fs file | ||
ignores.IsIgnored(path, false) | ||
with | ||
| ex -> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
FSharp.Core | ||
editorconfig | ||
MAB.DotIgnore | ||
Microsoft.SourceLink.GitHub | ||
Microsoft.SourceLink.GitHub | ||
System.IO.Abstractions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
module Fantomas.Tests.TestIgnoreFile | ||
|
||
open System.Collections.Concurrent | ||
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") | ||
|
||
let private snapshot (fs: ConcurrentDictionary<'k, 'v>) : ('k * 'v) list = | ||
fs | ||
|> Seq.map (function | ||
| KeyValue (k, v) -> (k, v)) | ||
|> Seq.toList | ||
|> List.sort | ||
|
||
/// A helper method to create a `parse` function for injection into IgnoreFile; | ||
/// this `parse` function will throw if it sees the same key twice. | ||
/// This allows us to test that we never attempt to read the same .fantomasignore file twice. | ||
let private oneShotParser () : string -> string = | ||
let seen = HashSet() | ||
|
||
fun s -> | ||
if seen.Add s then | ||
s | ||
else | ||
failwithf "Seen duplicate key: %s" s | ||
|
||
[<TestCase true>] | ||
[<TestCase false>] | ||
let ``findIgnoreFile does not crash at the root`` (fantomasIgnorePresent: bool) = | ||
let fs = MockFileSystem() | ||
let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot | ||
|
||
let fileAtRoot = fs.Path.Combine(root, "SomeFile.fs") | ||
|
||
let fantomasIgnore = | ||
if fantomasIgnorePresent then | ||
let target = fs.Path.Combine(root, ".fantomasignore") | ||
makeFileHierarchy fs [ target ] | ||
Some target | ||
else | ||
None | ||
|
||
let readFile = oneShotParser () | ||
|
||
let dict = ConcurrentDictionary() | ||
|
||
IgnoreFile.findIgnoreFile fs readFile dict fileAtRoot | ||
|> shouldEqual fantomasIgnore | ||
|
||
snapshot dict | ||
|> shouldEqual [ root, fantomasIgnore ] | ||
|
||
[<Test>] | ||
let ``findIgnoreFile 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 readFile = oneShotParser () | ||
|
||
let dict = ConcurrentDictionary() | ||
|
||
IgnoreFile.findIgnoreFile fs readFile dict source | ||
|> shouldEqual (Some target) | ||
|
||
snapshot dict | ||
|> shouldEqual [ fs.Directory.GetParent(target).FullName, Some target ] | ||
|
||
[<Test>] | ||
let ``findIgnoreFile 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 readFile = oneShotParser () | ||
|
||
let dict = ConcurrentDictionary() | ||
|
||
IgnoreFile.findIgnoreFile fs readFile dict source | ||
|> shouldEqual (Some target) | ||
|
||
snapshot dict | ||
|> shouldEqual [ fs.Directory.GetParent(target).FullName, Some target | ||
fs.Directory.GetParent(source).FullName, None ] | ||
|
||
[<Test>] | ||
let ``findIgnoreFile uses the store`` () = | ||
let fs = MockFileSystem() | ||
let root = fs.Path.GetTempPath() |> fs.Path.GetPathRoot | ||
|
||
let source1 = fs.Path.Combine(root, "folder1", "folder2", "Source1.fs") | ||
let source2 = fs.Path.Combine(root, "folder1", "folder2", "Source2.fs") | ||
let target = fs.Path.Combine(root, "folder1", ".fantomasignore") | ||
|
||
[ source1; source2; target ] | ||
|> makeFileHierarchy fs | ||
|
||
let readFile = oneShotParser () | ||
|
||
let dict = ConcurrentDictionary() | ||
|
||
IgnoreFile.findIgnoreFile fs readFile dict source1 | ||
|> shouldEqual (Some target) | ||
|
||
snapshot dict | ||
|> shouldEqual [ fs.Directory.GetParent(target).FullName, Some target | ||
fs.Directory.GetParent(source1).FullName, None ] | ||
|
||
IgnoreFile.findIgnoreFile fs readFile dict source2 | ||
|> shouldEqual (Some target) | ||
|
||
snapshot dict | ||
|> shouldEqual [ fs.Directory.GetParent(target).FullName, Some target | ||
fs.Directory.GetParent(source1).FullName, None ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we could not work with a mailbox processor instead?