Skip to content

Ordered evaluation in FileSystemGlobbing Matcher #115940

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

Merged
merged 11 commits into from
May 28, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public partial class Matcher
{
public Matcher() { }
public Matcher(System.StringComparison comparisonType) { }
public Matcher(System.StringComparison comparisonType = System.StringComparison.OrdinalIgnoreCase, bool preserveFilterOrder = false) { }
public virtual Microsoft.Extensions.FileSystemGlobbing.Matcher AddExclude(string pattern) { throw null; }
public virtual Microsoft.Extensions.FileSystemGlobbing.Matcher AddInclude(string pattern) { throw null; }
public virtual Microsoft.Extensions.FileSystemGlobbing.PatternMatchingResult Execute(Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase directoryInfo) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.FileSystemGlobbing.Internal
{
internal struct IncludeOrExcludeValue<TValue>
{
internal TValue Value;
internal bool IsInclude;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments;
using Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts;
using Microsoft.Extensions.FileSystemGlobbing.Util;

namespace Microsoft.Extensions.FileSystemGlobbing.Internal
Expand All @@ -17,8 +18,7 @@ namespace Microsoft.Extensions.FileSystemGlobbing.Internal
public class MatcherContext
{
private readonly DirectoryInfoBase _root;
private readonly IPatternContext[] _includePatternContexts;
private readonly IPatternContext[] _excludePatternContexts;
private readonly IPatternContext _patternContext;
private readonly List<FilePatternMatch> _files;

private readonly HashSet<string> _declaredLiteralFolderSegmentInString;
Expand All @@ -27,22 +27,33 @@ public class MatcherContext
private bool _declaredParentPathSegment;
private bool _declaredWildcardPathSegment;

private readonly StringComparison _comparisonType;

public MatcherContext(
IEnumerable<IPattern> includePatterns,
IEnumerable<IPattern> excludePatterns,
DirectoryInfoBase directoryInfo,
StringComparison comparison)
public MatcherContext(IEnumerable<IPattern> includePatterns, IEnumerable<IPattern> excludePatterns, DirectoryInfoBase directoryInfo, StringComparison comparison)
{
_root = directoryInfo;
_files = new List<FilePatternMatch>();
_comparisonType = comparison;
_files = [];
_declaredLiteralFolderSegmentInString = new HashSet<string>(StringComparisonHelper.GetStringComparer(comparison));

_includePatternContexts = includePatterns.Select(pattern => pattern.CreatePatternContextForInclude()).ToArray();
_excludePatternContexts = excludePatterns.Select(pattern => pattern.CreatePatternContextForExclude()).ToArray();
IPatternContext[] includePatternContexts = includePatterns.Select(pattern => pattern.CreatePatternContextForInclude()).ToArray();
IPatternContext[] excludePatternContexts = excludePatterns.Select(pattern => pattern.CreatePatternContextForExclude()).ToArray();

_patternContext = new IncludesFirstCompositePatternContext(includePatternContexts, excludePatternContexts);
}

internal MatcherContext(List<IncludeOrExcludeValue<IPattern>> orderedPatterns, DirectoryInfoBase directoryInfo, StringComparison comparison)
{
_root = directoryInfo;
_files = [];
_declaredLiteralFolderSegmentInString = new HashSet<string>(StringComparisonHelper.GetStringComparer(comparison));

IncludeOrExcludeValue<IPatternContext>[] includeOrExcludePatternContexts = orderedPatterns
.Select(item => new IncludeOrExcludeValue<IPatternContext>
{
Value = item.IsInclude ? item.Value.CreatePatternContextForInclude() : item.Value.CreatePatternContextForExclude(),
IsInclude = item.IsInclude
})
.ToArray();

_patternContext = new PreserveOrderCompositePatternContext(includeOrExcludePatternContexts);
}

public PatternMatchingResult Execute()
Expand All @@ -57,7 +68,7 @@ public PatternMatchingResult Execute()
private void Match(DirectoryInfoBase directory, string? parentRelativePath)
{
// Request all the including and excluding patterns to push current directory onto their status stack.
PushDirectory(directory);
_patternContext.PushDirectory(directory);
Declare();

var entities = new List<FileSystemInfoBase?>();
Expand Down Expand Up @@ -89,7 +100,7 @@ private void Match(DirectoryInfoBase directory, string? parentRelativePath)
{
if (entity is FileInfoBase fileInfo)
{
PatternTestResult result = MatchPatternContexts(fileInfo, (pattern, file) => pattern.Test(file));
PatternTestResult result = _patternContext.Test(fileInfo);
if (result.IsSuccessful)
{
_files.Add(new FilePatternMatch(
Expand All @@ -102,7 +113,7 @@ private void Match(DirectoryInfoBase directory, string? parentRelativePath)

if (entity is DirectoryInfoBase directoryInfo)
{
if (MatchPatternContexts(directoryInfo, (pattern, dir) => pattern.Test(dir)))
if (_patternContext.Test(directoryInfo))
{
subDirectories.Add(directoryInfo);
}
Expand All @@ -120,7 +131,7 @@ private void Match(DirectoryInfoBase directory, string? parentRelativePath)
}

// Request all the including and excluding patterns to pop their status stack.
PopDirectory();
_patternContext.PopDirectory();
}

private void Declare()
Expand All @@ -129,10 +140,7 @@ private void Declare()
_declaredParentPathSegment = false;
_declaredWildcardPathSegment = false;

foreach (IPatternContext include in _includePatternContexts)
{
include.Declare(DeclareInclude);
}
_patternContext.Declare(DeclareInclude);
}

private void DeclareInclude(IPathSegment patternSegment, bool isLastSegment)
Expand Down Expand Up @@ -169,82 +177,5 @@ internal static string CombinePath(string? left, string right)
return $"{left}/{right}";
}
}

// Used to adapt Test(DirectoryInfoBase) for the below overload
private bool MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileinfo, Func<IPatternContext, TFileInfoBase, bool> test)
{
return MatchPatternContexts(
fileinfo,
(ctx, file) =>
{
if (test(ctx, file))
{
return PatternTestResult.Success(stem: string.Empty);
}
else
{
return PatternTestResult.Failed;
}
}).IsSuccessful;
}

private PatternTestResult MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileinfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
{
PatternTestResult result = PatternTestResult.Failed;

// If the given file/directory matches any including pattern, continues to next step.
foreach (IPatternContext context in _includePatternContexts)
{
PatternTestResult localResult = test(context, fileinfo);
if (localResult.IsSuccessful)
{
result = localResult;
break;
}
}

// If the given file/directory doesn't match any of the including pattern, returns false.
if (!result.IsSuccessful)
{
return PatternTestResult.Failed;
}

// If the given file/directory matches any excluding pattern, returns false.
foreach (IPatternContext context in _excludePatternContexts)
{
if (test(context, fileinfo).IsSuccessful)
{
return PatternTestResult.Failed;
}
}

return result;
}

private void PopDirectory()
{
foreach (IPatternContext context in _excludePatternContexts)
{
context.PopDirectory();
}

foreach (IPatternContext context in _includePatternContexts)
{
context.PopDirectory();
}
}

private void PushDirectory(DirectoryInfoBase directory)
{
foreach (IPatternContext context in _includePatternContexts)
{
context.PushDirectory(directory);
}

foreach (IPatternContext context in _excludePatternContexts)
{
context.PushDirectory(directory);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;

namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts
{
internal abstract class CompositePatternContext : IPatternContext
{
public abstract void Declare(Action<IPathSegment, bool> onDeclare);
public abstract void PopDirectory();
public abstract void PushDirectory(DirectoryInfoBase directory);

protected internal abstract PatternTestResult MatchPatternContexts<TFileInfoBase>(
TFileInfoBase fileInfo,
Func<IPatternContext, TFileInfoBase, PatternTestResult> test);
Comment on lines +15 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not need to be generic.

Suggested change
protected internal abstract PatternTestResult MatchPatternContexts<TFileInfoBase>(
TFileInfoBase fileInfo,
Func<IPatternContext, TFileInfoBase, PatternTestResult> test);
protected internal abstract PatternTestResult MatchPatternContexts(
FileSystemInfoBase fileSystemInfo,
Func<IPatternContext, PatternTestResult> test);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type argument's naming is confusing since we don't actually care that it's a file info. This is instead using a TState pattern, so the implementation will just call the test with their IPatternContext and pass in this opaque state for the ride. It could also just be:

protected internal abstract PatternTestResult MatchPatternContexts(Func<IPatternContext, PatternTestResult> test);

which would force us to have a closure. I don't know if that matters so much here, but this method is from the existing code and I decided to just leave it as is.


public bool Test(DirectoryInfoBase directory) =>
MatchPatternContexts(directory,
static (context, dir) =>
context.Test(dir) ? PatternTestResult.Success(stem: string.Empty) : PatternTestResult.Failed).IsSuccessful;

public PatternTestResult Test(FileInfoBase file) =>
MatchPatternContexts(file, static (context, fileInfo) => context.Test(fileInfo));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;

namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts
{
internal sealed class IncludesFirstCompositePatternContext : CompositePatternContext
{
private readonly IPatternContext[] _includePatternContexts;
private readonly IPatternContext[] _excludePatternContexts;

internal IncludesFirstCompositePatternContext(IPatternContext[] includePatternContexts, IPatternContext[] excludePatternContexts)
{
_includePatternContexts = includePatternContexts;
_excludePatternContexts = excludePatternContexts;
}

public override void Declare(Action<IPathSegment, bool> onDeclare)
{
foreach (IPatternContext include in _includePatternContexts)
{
include.Declare(onDeclare);
}
}

protected internal override PatternTestResult MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileInfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
{
PatternTestResult result = PatternTestResult.Failed;

// If the given file/directory matches any including pattern, continues to next step.
foreach (IPatternContext context in _includePatternContexts)
{
PatternTestResult localResult = test(context, fileInfo);
if (localResult.IsSuccessful)
{
result = localResult;
break;
}
}

// If the given file/directory doesn't match any of the including pattern, returns false.
if (!result.IsSuccessful)
{
return PatternTestResult.Failed;
}

// If the given file/directory matches any excluding pattern, returns false.
foreach (IPatternContext context in _excludePatternContexts)
{
if (test(context, fileInfo).IsSuccessful)
{
return PatternTestResult.Failed;
}
}

return result;
}

public override void PopDirectory()
{
foreach (IPatternContext context in _excludePatternContexts)
{
context.PopDirectory();
}

foreach (IPatternContext context in _includePatternContexts)
{
context.PopDirectory();
}
}

public override void PushDirectory(DirectoryInfoBase directory)
{
foreach (IPatternContext context in _includePatternContexts)
{
context.PushDirectory(directory);
}

foreach (IPatternContext context in _excludePatternContexts)
{
context.PushDirectory(directory);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;

namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts
{
internal sealed class PreserveOrderCompositePatternContext : CompositePatternContext
{
private readonly IncludeOrExcludeValue<IPatternContext>[] _includeOrExcludePatternContexts;

internal PreserveOrderCompositePatternContext(IncludeOrExcludeValue<IPatternContext>[] includeOrExcludePatternContexts) =>
_includeOrExcludePatternContexts = includeOrExcludePatternContexts;

public override void Declare(Action<IPathSegment, bool> onDeclare)
{
foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
{
if (context.IsInclude)
{
context.Value.Declare(onDeclare);
}
}
}

protected internal override PatternTestResult MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileInfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
{
PatternTestResult result = PatternTestResult.Failed;

foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
{
// If the file is currently a match and the pattern is exclude, then test it to determine
// if we should unmatch. And if the file is currently not a match and the pattern is include,
// then test it to determine if we should match.
if (result.IsSuccessful != context.IsInclude)
{
PatternTestResult localResult = test(context.Value, fileInfo);
if (localResult.IsSuccessful)
{
result = context.IsInclude ? localResult : PatternTestResult.Failed;
}
}
}

return result;
}

public override void PopDirectory()
{
foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
{
context.Value.PopDirectory();
}
}

public override void PushDirectory(DirectoryInfoBase directory)
{
foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
{
context.Value.PushDirectory(directory);
}
}
}
}
Loading
Loading