Skip to content

Commit

Permalink
Make FileMatcher consume IDirectoryCache instead of IFileSystem
Browse files Browse the repository at this point in the history
  • Loading branch information
ladipro committed Aug 10, 2021
1 parent d87e13f commit fde5ed6
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 56 deletions.
2 changes: 0 additions & 2 deletions src/Build/Evaluation/Context/EvaluationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public enum SharingPolicy

internal ISdkResolverService SdkResolverService { get; }
internal IFileSystem FileSystem { get; }
internal FileMatcher FileMatcher { get; }

private IDirectoryCacheFactory _directoryCacheFactory;
private ConditionalWeakTable<Project, IDirectoryCache> _directoryCachesPerProject;
Expand All @@ -68,7 +67,6 @@ private EvaluationContext(SharingPolicy policy, IFileSystem fileSystem, IDirecto
SdkResolverService = new CachingSdkResolverService();
FileEntryExpansionCache = new ConcurrentDictionary<string, IReadOnlyList<string>>();
FileSystem = fileSystem ?? new CachingFileSystemWrapper(FileSystems.Default);
FileMatcher = new FileMatcher(FileSystem, FileEntryExpansionCache);

if (directoryCacheFactory != null)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Build/Evaluation/Evaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ private Evaluator(

// Create a FileMatcher for the given combination of EvaluationContext and the project being evaluated.
IDirectoryCache directoryCache = _evaluationContext.GetDirectoryCacheForProject(project);
_fileMatcher = new FileMatcher(evaluationContext.FileSystem, evaluationContext.FileEntryExpansionCache);
_fileMatcher = new FileMatcher(directoryCache, evaluationContext.FileEntryExpansionCache);
}

/// <summary>
Expand Down
23 changes: 23 additions & 0 deletions src/Build/FileSystem/IDirectoryCacheFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Build.Evaluation;

namespace Microsoft.Build.FileSystem
{
/// <summary>
/// A provider of <see cref="IDirectoryCache"/> instances. To be implemented by MSBuild hosts that wish to intercept
/// file existence checks and file enumerations performed during project evaluation.
/// </summary>
/// <remarks>
/// Unlike <see cref="MSBuildFileSystemBase"/>, file enumeration returns file/directory names, not full paths.
/// </remarks>
public interface IDirectoryCacheFactory
{
/// <summary>
/// Returns an <see cref="IDirectoryCache"/> to be used when evaluating the given <see cref="Project"/>.
/// </summary>
/// <param name="project">The project being evaluated.</param>
IDirectoryCache GetDirectoryCacheForProject(Project project);
}
}
75 changes: 49 additions & 26 deletions src/Shared/FileMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Threading.Tasks;
#if BUILD_ENGINE
using Microsoft.Build.FileSystem;
#endif
using Microsoft.Build.Shared.FileSystem;
using Microsoft.Build.Utilities;

Expand All @@ -20,7 +23,7 @@ namespace Microsoft.Build.Shared
/// </summary>
internal class FileMatcher
{
private readonly IFileSystem _fileSystem;
private readonly IDirectoryCache _directoryCache;
private const string recursiveDirectoryMatch = "**";

private static readonly string s_directorySeparator = new string(Path.DirectorySeparatorChar, 1);
Expand Down Expand Up @@ -79,12 +82,12 @@ private static class FileSpecRegexParts
/// <summary>
/// The Default FileMatcher does not cache directory enumeration.
/// </summary>
public static FileMatcher Default = new FileMatcher(FileSystems.Default, null);
public static FileMatcher Default = new FileMatcher(FileSystems.DefaultDirectoryCache, null);

public FileMatcher(IFileSystem fileSystem, ConcurrentDictionary<string, IReadOnlyList<string>> fileEntryExpansionCache = null) : this(
fileSystem,
public FileMatcher(IDirectoryCache directoryCache, ConcurrentDictionary<string, IReadOnlyList<string>> fileEntryExpansionCache = null) : this(
directoryCache,
(entityType, path, pattern, projectDirectory, stripProjectDirectory) => GetAccessibleFileSystemEntries(
fileSystem,
directoryCache,
entityType,
path,
pattern,
Expand All @@ -94,7 +97,7 @@ public FileMatcher(IFileSystem fileSystem, ConcurrentDictionary<string, IReadOnl
{
}

internal FileMatcher(IFileSystem fileSystem, GetFileSystemEntries getFileSystemEntries, ConcurrentDictionary<string, IReadOnlyList<string>> fileEntryExpansionCache = null)
internal FileMatcher(IDirectoryCache directoryCache, GetFileSystemEntries getFileSystemEntries, ConcurrentDictionary<string, IReadOnlyList<string>> fileEntryExpansionCache = null)
{
if (Traits.Instance.MSBuildCacheFileEnumerations)
{
Expand All @@ -106,7 +109,7 @@ internal FileMatcher(IFileSystem fileSystem, GetFileSystemEntries getFileSystemE
_cachedGlobExpansions = fileEntryExpansionCache;
}

_fileSystem = fileSystem;
_directoryCache = directoryCache;

_getFileSystemEntries = fileEntryExpansionCache == null
? getFileSystemEntries
Expand Down Expand Up @@ -235,16 +238,16 @@ internal static bool HasPropertyOrItemReferences(string filespec)
/// <param name="pattern">The pattern to search.</param>
/// <param name="projectDirectory">The directory for the project within which the call is made</param>
/// <param name="stripProjectDirectory">If true the project directory should be stripped</param>
/// <param name="fileSystem">The file system abstraction to use that implements file system operations</param>
/// <param name="directoryCache">The file system abstraction to use that implements file system operations</param>
/// <returns></returns>
private static IReadOnlyList<string> GetAccessibleFileSystemEntries(IFileSystem fileSystem, FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory)
private static IReadOnlyList<string> GetAccessibleFileSystemEntries(IDirectoryCache directoryCache, FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory)
{
path = FileUtilities.FixFilePath(path);
switch (entityType)
{
case FileSystemEntity.Files: return GetAccessibleFiles(fileSystem, path, pattern, projectDirectory, stripProjectDirectory);
case FileSystemEntity.Directories: return GetAccessibleDirectories(fileSystem, path, pattern);
case FileSystemEntity.FilesAndDirectories: return GetAccessibleFilesAndDirectories(fileSystem, path, pattern);
case FileSystemEntity.Files: return GetAccessibleFiles(directoryCache, path, pattern, projectDirectory, stripProjectDirectory);
case FileSystemEntity.Directories: return GetAccessibleDirectories(directoryCache, path, pattern);
case FileSystemEntity.FilesAndDirectories: return GetAccessibleFilesAndDirectories(directoryCache, path, pattern);
default:
ErrorUtilities.VerifyThrow(false, "Unexpected filesystem entity type.");
break;
Expand All @@ -258,18 +261,18 @@ private static IReadOnlyList<string> GetAccessibleFileSystemEntries(IFileSystem
/// </summary>
/// <param name="path"></param>
/// <param name="pattern"></param>
/// <param name="fileSystem">The file system abstraction to use that implements file system operations</param>
/// <param name="directoryCache">The file system abstraction to use that implements file system operations</param>
/// <returns>An enumerable of matching file system entries (can be empty).</returns>
private static IReadOnlyList<string> GetAccessibleFilesAndDirectories(IFileSystem fileSystem, string path, string pattern)
private static IReadOnlyList<string> GetAccessibleFilesAndDirectories(IDirectoryCache directoryCache, string path, string pattern)
{
if (fileSystem.DirectoryExists(path))
if (directoryCache.DirectoryExists(path))
{
try
{
return (ShouldEnforceMatching(pattern)
? fileSystem.EnumerateFileSystemEntries(path, pattern)
? EnumerateFullFileSystemPaths(directoryCache, FileSystemEntity.FilesAndDirectories, path, pattern)
.Where(o => IsMatch(Path.GetFileName(o), pattern))
: fileSystem.EnumerateFileSystemEntries(path, pattern)
: EnumerateFullFileSystemPaths(directoryCache, FileSystemEntity.FilesAndDirectories, path, pattern)
).ToArray();
}
// for OS security
Expand Down Expand Up @@ -324,11 +327,11 @@ private static bool ShouldEnforceMatching(string searchPattern)
/// <param name="filespec">The pattern.</param>
/// <param name="projectDirectory">The project directory</param>
/// <param name="stripProjectDirectory"></param>
/// <param name="fileSystem">The file system abstraction to use that implements file system operations</param>
/// <param name="directoryCache">The file system abstraction to use that implements file system operations</param>
/// <returns>Files that can be accessed.</returns>
private static IReadOnlyList<string> GetAccessibleFiles
(
IFileSystem fileSystem,
IDirectoryCache directoryCache,
string path,
string filespec, // can be null
string projectDirectory,
Expand All @@ -344,11 +347,11 @@ bool stripProjectDirectory
IEnumerable<string> files;
if (filespec == null)
{
files = fileSystem.EnumerateFiles(dir);
files = EnumerateFullFileSystemPaths(directoryCache, FileSystemEntity.Files, dir, "*");
}
else
{
files = fileSystem.EnumerateFiles(dir, filespec);
files = EnumerateFullFileSystemPaths(directoryCache, FileSystemEntity.Files, dir, filespec);
if (ShouldEnforceMatching(filespec))
{
files = files.Where(o => IsMatch(Path.GetFileName(o), filespec));
Expand Down Expand Up @@ -392,11 +395,11 @@ bool stripProjectDirectory
/// </summary>
/// <param name="path">The path.</param>
/// <param name="pattern">Pattern to match</param>
/// <param name="fileSystem">The file system abstraction to use that implements file system operations</param>
/// <param name="directoryCache">The file system abstraction to use that implements file system operations</param>
/// <returns>Accessible directories.</returns>
private static IReadOnlyList<string> GetAccessibleDirectories
(
IFileSystem fileSystem,
IDirectoryCache directoryCache,
string path,
string pattern
)
Expand All @@ -407,11 +410,11 @@ string pattern

if (pattern == null)
{
directories = fileSystem.EnumerateDirectories((path.Length == 0) ? s_thisDirectory : path);
directories = EnumerateFullFileSystemPaths(directoryCache, FileSystemEntity.Directories, (path.Length == 0) ? s_thisDirectory : path, "*");
}
else
{
directories = fileSystem.EnumerateDirectories((path.Length == 0) ? s_thisDirectory : path, pattern);
directories = EnumerateFullFileSystemPaths(directoryCache, FileSystemEntity.Directories, (path.Length == 0) ? s_thisDirectory : path, pattern);
if (ShouldEnforceMatching(pattern))
{
directories = directories.Where(o => IsMatch(Path.GetFileName(o), pattern));
Expand Down Expand Up @@ -442,6 +445,26 @@ string pattern
}
}

// TODO: Temporary until #6075 is implemented.
private static IEnumerable<string> EnumerateFullFileSystemPaths(IDirectoryCache directoryCache, FileSystemEntity entityType, string path, string pattern)
{
FindPredicate predicate = (ref ReadOnlySpan<char> fileName) =>
{
string fileNameString = fileName.ToString();
return IsAllFilesWildcard(pattern) || IsMatch(fileNameString, pattern);
};
FindTransform<string> transform = (ref ReadOnlySpan<char> fileName) => Path.Combine(path, fileName.ToString());

IEnumerable<string> directories = (entityType == FileSystemEntity.Directories || entityType == FileSystemEntity.FilesAndDirectories)
? directoryCache.EnumerateDirectories(path, predicate, transform)
: Enumerable.Empty<string>();
IEnumerable<string> files = (entityType == FileSystemEntity.Files || entityType == FileSystemEntity.FilesAndDirectories)
? directoryCache.EnumerateFiles(path, predicate, transform)
: Enumerable.Empty<string>();

return Enumerable.Concat(directories, files);
}

/// <summary>
/// Given a path name, get its long version.
/// </summary>
Expand Down Expand Up @@ -2098,7 +2121,7 @@ out bool isLegalFileSpec
* If the fixed directory part doesn't exist, then this means no files should be
* returned.
*/
if (fixedDirectoryPart.Length > 0 && !_fileSystem.DirectoryExists(fixedDirectoryPart))
if (fixedDirectoryPart.Length > 0 && !_directoryCache.DirectoryExists(fixedDirectoryPart))
{
return SearchAction.ReturnEmptyList;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
#if BUILD_ENGINE
using Microsoft.Build.FileSystem;
#endif
using System;
using System.Collections.Generic;

namespace Microsoft.Build.FileSystem
namespace Microsoft.Build.Shared.FileSystem
{
/// <summary>
/// Implements <see cref="IDirectoryCache"/> on top of <see cref="IFileSystem"/>.
Expand Down
8 changes: 8 additions & 0 deletions src/Shared/FileSystem/FileSystems.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if BUILD_ENGINE
using Microsoft.Build.FileSystem;
#endif

namespace Microsoft.Build.Shared.FileSystem
{
/// <summary>
Expand All @@ -10,6 +14,10 @@ internal static class FileSystems
{
public static IFileSystem Default = GetFileSystem();

#if !CLR2COMPATIBILITY
public static IDirectoryCache DefaultDirectoryCache = new DirectoryCacheOverFileSystem(Default);
#endif

private static IFileSystem GetFileSystem()
{
#if CLR2COMPATIBILITY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,38 @@
using System;
using System.Collections.Generic;

using Microsoft.Build.Evaluation;

#if BUILD_ENGINE
namespace Microsoft.Build.FileSystem
#else
namespace Microsoft.Build.Shared.FileSystem
#endif
{
/// <summary>
/// A provider of <see cref="IDirectoryCache"/> instances. To be implemented by MSBuild hosts that wish to intercept
/// file existence checks and file enumerations performed during project evaluation.
/// </summary>
/// <remarks>
/// Unlike <see cref="MSBuildFileSystemBase"/>, file enumeration returns file/directory names, not full paths.
/// </remarks>
public interface IDirectoryCacheFactory
{
/// <summary>
/// Returns an <see cref="IDirectoryCache"/> to be used when evaluating the given <see cref="Project"/>.
/// </summary>
/// <param name="project">The project being evaluated.</param>
IDirectoryCache GetDirectoryCacheForProject(Project project);
}

/// <summary>
/// A predicate taking file name.
/// </summary>
/// <param name="fileName">The file name to check.</param>
public delegate bool FindPredicate(ref ReadOnlySpan<char> fileName);
#if BUILD_ENGINE
public
#endif
delegate bool FindPredicate(ref ReadOnlySpan<char> fileName);

/// <summary>
/// A function taking file name and returning an arbitrary result.
/// </summary>
/// <typeparam name="TResult">The type of the result to return</typeparam>
/// <param name="fileName">The file name to transform.</param>
public delegate TResult FindTransform<TResult>(ref ReadOnlySpan<char> fileName);
#if BUILD_ENGINE
public
#endif
delegate TResult FindTransform<TResult>(ref ReadOnlySpan<char> fileName);

/// <summary>
/// Allows the implementor to intercept file existence checks and file enumerations performed during project evaluation.
/// </summary>
public interface IDirectoryCache
#if BUILD_ENGINE
public
#endif
interface IDirectoryCache
{
/// <summary>
/// Returns <code>true</code> if the given path points to an existing file on disk.
Expand Down
7 changes: 4 additions & 3 deletions src/Shared/UnitTests/FileMatcher_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.Build.Shared.FileSystem;
using Xunit;
using Xunit.Abstractions;
using Microsoft.Build.FileSystem;

namespace Microsoft.Build.UnitTests
{
Expand Down Expand Up @@ -100,7 +101,7 @@ void VerifyImpl(FileMatcher fileMatcher, string include, string[] excludes, bool
}
}

var fileMatcherWithCache = new FileMatcher(FileSystems.Default, new ConcurrentDictionary<string, IReadOnlyList<string>>());
var fileMatcherWithCache = new FileMatcher(FileSystems.DefaultDirectoryCache, new ConcurrentDictionary<string, IReadOnlyList<string>>());

void Verify(string include, string[] excludes, bool shouldHaveNoMatches = false, string customMessage = null)
{
Expand Down Expand Up @@ -2368,7 +2369,7 @@ private static void MatchDriver(string filespec, string[] excludeFilespecs, stri
{
MockFileSystem mockFileSystem = new MockFileSystem(matchingFiles, nonmatchingFiles, untouchableFiles);

var fileMatcher = new FileMatcher(new FileSystemAdapter(mockFileSystem), mockFileSystem.GetAccessibleFileSystemEntries);
var fileMatcher = new FileMatcher(new DirectoryCacheOverFileSystem(new FileSystemAdapter(mockFileSystem)), mockFileSystem.GetAccessibleFileSystemEntries);

string[] files = fileMatcher.GetFiles
(
Expand Down Expand Up @@ -2439,7 +2440,7 @@ private static IReadOnlyList<string> GetFileSystemEntriesLoopBack(FileMatcher.Fi
* Validate that SplitFileSpec(...) is returning the expected constituent values.
*************************************************************************************/

private static FileMatcher loopBackFileMatcher = new FileMatcher(FileSystems.Default, GetFileSystemEntriesLoopBack);
private static FileMatcher loopBackFileMatcher = new FileMatcher(FileSystems.DefaultDirectoryCache, GetFileSystemEntriesLoopBack);

private static void ValidateSplitFileSpec
(
Expand Down

0 comments on commit fde5ed6

Please sign in to comment.