Skip to content

Commit 16a5d90

Browse files
Ordered evaluation in FileSystemGlobbing Matcher (#115940)
Co-authored-by: kasperk81 <83082615+kasperk81@users.noreply.github.com>
1 parent ab3d27f commit 16a5d90

File tree

10 files changed

+507
-169
lines changed

10 files changed

+507
-169
lines changed

src/libraries/Microsoft.Extensions.FileSystemGlobbing/ref/Microsoft.Extensions.FileSystemGlobbing.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public partial class Matcher
3131
{
3232
public Matcher() { }
3333
public Matcher(System.StringComparison comparisonType) { }
34+
public Matcher(System.StringComparison comparisonType = System.StringComparison.OrdinalIgnoreCase, bool preserveFilterOrder = false) { }
3435
public virtual Microsoft.Extensions.FileSystemGlobbing.Matcher AddExclude(string pattern) { throw null; }
3536
public virtual Microsoft.Extensions.FileSystemGlobbing.Matcher AddInclude(string pattern) { throw null; }
3637
public virtual Microsoft.Extensions.FileSystemGlobbing.PatternMatchingResult Execute(Microsoft.Extensions.FileSystemGlobbing.Abstractions.DirectoryInfoBase directoryInfo) { throw null; }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.Extensions.FileSystemGlobbing.Internal
5+
{
6+
internal struct IncludeOrExcludeValue<TValue>
7+
{
8+
internal TValue Value;
9+
internal bool IsInclude;
10+
}
11+
}

src/libraries/Microsoft.Extensions.FileSystemGlobbing/src/Internal/MatcherContext.cs

Lines changed: 29 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
88
using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments;
9+
using Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts;
910
using Microsoft.Extensions.FileSystemGlobbing.Util;
1011

1112
namespace Microsoft.Extensions.FileSystemGlobbing.Internal
@@ -17,8 +18,7 @@ namespace Microsoft.Extensions.FileSystemGlobbing.Internal
1718
public class MatcherContext
1819
{
1920
private readonly DirectoryInfoBase _root;
20-
private readonly IPatternContext[] _includePatternContexts;
21-
private readonly IPatternContext[] _excludePatternContexts;
21+
private readonly IPatternContext _patternContext;
2222
private readonly List<FilePatternMatch> _files;
2323

2424
private readonly HashSet<string> _declaredLiteralFolderSegmentInString;
@@ -27,22 +27,33 @@ public class MatcherContext
2727
private bool _declaredParentPathSegment;
2828
private bool _declaredWildcardPathSegment;
2929

30-
private readonly StringComparison _comparisonType;
31-
32-
public MatcherContext(
33-
IEnumerable<IPattern> includePatterns,
34-
IEnumerable<IPattern> excludePatterns,
35-
DirectoryInfoBase directoryInfo,
36-
StringComparison comparison)
30+
public MatcherContext(IEnumerable<IPattern> includePatterns, IEnumerable<IPattern> excludePatterns, DirectoryInfoBase directoryInfo, StringComparison comparison)
3731
{
3832
_root = directoryInfo;
39-
_files = new List<FilePatternMatch>();
40-
_comparisonType = comparison;
33+
_files = [];
34+
_declaredLiteralFolderSegmentInString = new HashSet<string>(StringComparisonHelper.GetStringComparer(comparison));
4135

42-
_includePatternContexts = includePatterns.Select(pattern => pattern.CreatePatternContextForInclude()).ToArray();
43-
_excludePatternContexts = excludePatterns.Select(pattern => pattern.CreatePatternContextForExclude()).ToArray();
36+
IPatternContext[] includePatternContexts = includePatterns.Select(pattern => pattern.CreatePatternContextForInclude()).ToArray();
37+
IPatternContext[] excludePatternContexts = excludePatterns.Select(pattern => pattern.CreatePatternContextForExclude()).ToArray();
38+
39+
_patternContext = new IncludesFirstCompositePatternContext(includePatternContexts, excludePatternContexts);
40+
}
4441

42+
internal MatcherContext(List<IncludeOrExcludeValue<IPattern>> orderedPatterns, DirectoryInfoBase directoryInfo, StringComparison comparison)
43+
{
44+
_root = directoryInfo;
45+
_files = [];
4546
_declaredLiteralFolderSegmentInString = new HashSet<string>(StringComparisonHelper.GetStringComparer(comparison));
47+
48+
IncludeOrExcludeValue<IPatternContext>[] includeOrExcludePatternContexts = orderedPatterns
49+
.Select(item => new IncludeOrExcludeValue<IPatternContext>
50+
{
51+
Value = item.IsInclude ? item.Value.CreatePatternContextForInclude() : item.Value.CreatePatternContextForExclude(),
52+
IsInclude = item.IsInclude
53+
})
54+
.ToArray();
55+
56+
_patternContext = new PreserveOrderCompositePatternContext(includeOrExcludePatternContexts);
4657
}
4758

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

6374
var entities = new List<FileSystemInfoBase?>();
@@ -89,7 +100,7 @@ private void Match(DirectoryInfoBase directory, string? parentRelativePath)
89100
{
90101
if (entity is FileInfoBase fileInfo)
91102
{
92-
PatternTestResult result = MatchPatternContexts(fileInfo, (pattern, file) => pattern.Test(file));
103+
PatternTestResult result = _patternContext.Test(fileInfo);
93104
if (result.IsSuccessful)
94105
{
95106
_files.Add(new FilePatternMatch(
@@ -102,7 +113,7 @@ private void Match(DirectoryInfoBase directory, string? parentRelativePath)
102113

103114
if (entity is DirectoryInfoBase directoryInfo)
104115
{
105-
if (MatchPatternContexts(directoryInfo, (pattern, dir) => pattern.Test(dir)))
116+
if (_patternContext.Test(directoryInfo))
106117
{
107118
subDirectories.Add(directoryInfo);
108119
}
@@ -120,7 +131,7 @@ private void Match(DirectoryInfoBase directory, string? parentRelativePath)
120131
}
121132

122133
// Request all the including and excluding patterns to pop their status stack.
123-
PopDirectory();
134+
_patternContext.PopDirectory();
124135
}
125136

126137
private void Declare()
@@ -129,10 +140,7 @@ private void Declare()
129140
_declaredParentPathSegment = false;
130141
_declaredWildcardPathSegment = false;
131142

132-
foreach (IPatternContext include in _includePatternContexts)
133-
{
134-
include.Declare(DeclareInclude);
135-
}
143+
_patternContext.Declare(DeclareInclude);
136144
}
137145

138146
private void DeclareInclude(IPathSegment patternSegment, bool isLastSegment)
@@ -169,82 +177,5 @@ internal static string CombinePath(string? left, string right)
169177
return $"{left}/{right}";
170178
}
171179
}
172-
173-
// Used to adapt Test(DirectoryInfoBase) for the below overload
174-
private bool MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileinfo, Func<IPatternContext, TFileInfoBase, bool> test)
175-
{
176-
return MatchPatternContexts(
177-
fileinfo,
178-
(ctx, file) =>
179-
{
180-
if (test(ctx, file))
181-
{
182-
return PatternTestResult.Success(stem: string.Empty);
183-
}
184-
else
185-
{
186-
return PatternTestResult.Failed;
187-
}
188-
}).IsSuccessful;
189-
}
190-
191-
private PatternTestResult MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileinfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
192-
{
193-
PatternTestResult result = PatternTestResult.Failed;
194-
195-
// If the given file/directory matches any including pattern, continues to next step.
196-
foreach (IPatternContext context in _includePatternContexts)
197-
{
198-
PatternTestResult localResult = test(context, fileinfo);
199-
if (localResult.IsSuccessful)
200-
{
201-
result = localResult;
202-
break;
203-
}
204-
}
205-
206-
// If the given file/directory doesn't match any of the including pattern, returns false.
207-
if (!result.IsSuccessful)
208-
{
209-
return PatternTestResult.Failed;
210-
}
211-
212-
// If the given file/directory matches any excluding pattern, returns false.
213-
foreach (IPatternContext context in _excludePatternContexts)
214-
{
215-
if (test(context, fileinfo).IsSuccessful)
216-
{
217-
return PatternTestResult.Failed;
218-
}
219-
}
220-
221-
return result;
222-
}
223-
224-
private void PopDirectory()
225-
{
226-
foreach (IPatternContext context in _excludePatternContexts)
227-
{
228-
context.PopDirectory();
229-
}
230-
231-
foreach (IPatternContext context in _includePatternContexts)
232-
{
233-
context.PopDirectory();
234-
}
235-
}
236-
237-
private void PushDirectory(DirectoryInfoBase directory)
238-
{
239-
foreach (IPatternContext context in _includePatternContexts)
240-
{
241-
context.PushDirectory(directory);
242-
}
243-
244-
foreach (IPatternContext context in _excludePatternContexts)
245-
{
246-
context.PushDirectory(directory);
247-
}
248-
}
249180
}
250181
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
6+
7+
namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts
8+
{
9+
internal abstract class CompositePatternContext : IPatternContext
10+
{
11+
public abstract void Declare(Action<IPathSegment, bool> onDeclare);
12+
public abstract void PopDirectory();
13+
public abstract void PushDirectory(DirectoryInfoBase directory);
14+
15+
protected internal abstract PatternTestResult MatchPatternContexts<TFileInfoBase>(
16+
TFileInfoBase fileInfo,
17+
Func<IPatternContext, TFileInfoBase, PatternTestResult> test);
18+
19+
public bool Test(DirectoryInfoBase directory) =>
20+
MatchPatternContexts(directory,
21+
static (context, dir) =>
22+
context.Test(dir) ? PatternTestResult.Success(stem: string.Empty) : PatternTestResult.Failed).IsSuccessful;
23+
24+
public PatternTestResult Test(FileInfoBase file) =>
25+
MatchPatternContexts(file, static (context, fileInfo) => context.Test(fileInfo));
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
6+
7+
namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts
8+
{
9+
internal sealed class IncludesFirstCompositePatternContext : CompositePatternContext
10+
{
11+
private readonly IPatternContext[] _includePatternContexts;
12+
private readonly IPatternContext[] _excludePatternContexts;
13+
14+
internal IncludesFirstCompositePatternContext(IPatternContext[] includePatternContexts, IPatternContext[] excludePatternContexts)
15+
{
16+
_includePatternContexts = includePatternContexts;
17+
_excludePatternContexts = excludePatternContexts;
18+
}
19+
20+
public override void Declare(Action<IPathSegment, bool> onDeclare)
21+
{
22+
foreach (IPatternContext include in _includePatternContexts)
23+
{
24+
include.Declare(onDeclare);
25+
}
26+
}
27+
28+
protected internal override PatternTestResult MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileInfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
29+
{
30+
PatternTestResult result = PatternTestResult.Failed;
31+
32+
// If the given file/directory matches any including pattern, continues to next step.
33+
foreach (IPatternContext context in _includePatternContexts)
34+
{
35+
PatternTestResult localResult = test(context, fileInfo);
36+
if (localResult.IsSuccessful)
37+
{
38+
result = localResult;
39+
break;
40+
}
41+
}
42+
43+
// If the given file/directory doesn't match any of the including pattern, returns false.
44+
if (!result.IsSuccessful)
45+
{
46+
return PatternTestResult.Failed;
47+
}
48+
49+
// If the given file/directory matches any excluding pattern, returns false.
50+
foreach (IPatternContext context in _excludePatternContexts)
51+
{
52+
if (test(context, fileInfo).IsSuccessful)
53+
{
54+
return PatternTestResult.Failed;
55+
}
56+
}
57+
58+
return result;
59+
}
60+
61+
public override void PopDirectory()
62+
{
63+
foreach (IPatternContext context in _excludePatternContexts)
64+
{
65+
context.PopDirectory();
66+
}
67+
68+
foreach (IPatternContext context in _includePatternContexts)
69+
{
70+
context.PopDirectory();
71+
}
72+
}
73+
74+
public override void PushDirectory(DirectoryInfoBase directory)
75+
{
76+
foreach (IPatternContext context in _includePatternContexts)
77+
{
78+
context.PushDirectory(directory);
79+
}
80+
81+
foreach (IPatternContext context in _excludePatternContexts)
82+
{
83+
context.PushDirectory(directory);
84+
}
85+
}
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
6+
7+
namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts
8+
{
9+
internal sealed class PreserveOrderCompositePatternContext : CompositePatternContext
10+
{
11+
private readonly IncludeOrExcludeValue<IPatternContext>[] _includeOrExcludePatternContexts;
12+
13+
internal PreserveOrderCompositePatternContext(IncludeOrExcludeValue<IPatternContext>[] includeOrExcludePatternContexts) =>
14+
_includeOrExcludePatternContexts = includeOrExcludePatternContexts;
15+
16+
public override void Declare(Action<IPathSegment, bool> onDeclare)
17+
{
18+
foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
19+
{
20+
if (context.IsInclude)
21+
{
22+
context.Value.Declare(onDeclare);
23+
}
24+
}
25+
}
26+
27+
protected internal override PatternTestResult MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileInfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
28+
{
29+
PatternTestResult result = PatternTestResult.Failed;
30+
31+
foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
32+
{
33+
// If the file is currently a match and the pattern is exclude, then test it to determine
34+
// if we should unmatch. And if the file is currently not a match and the pattern is include,
35+
// then test it to determine if we should match.
36+
if (result.IsSuccessful != context.IsInclude)
37+
{
38+
PatternTestResult localResult = test(context.Value, fileInfo);
39+
if (localResult.IsSuccessful)
40+
{
41+
result = context.IsInclude ? localResult : PatternTestResult.Failed;
42+
}
43+
}
44+
}
45+
46+
return result;
47+
}
48+
49+
public override void PopDirectory()
50+
{
51+
foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
52+
{
53+
context.Value.PopDirectory();
54+
}
55+
}
56+
57+
public override void PushDirectory(DirectoryInfoBase directory)
58+
{
59+
foreach (IncludeOrExcludeValue<IPatternContext> context in _includeOrExcludePatternContexts)
60+
{
61+
context.Value.PushDirectory(directory);
62+
}
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)