Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 21 additions & 38 deletions src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,39 @@
#nullable disable
namespace Microsoft.ComponentDetection.Common;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public static class PatternMatchingUtility
{
public delegate bool FilePatternMatcher(ReadOnlySpan<char> span);

public static FilePatternMatcher GetFilePatternMatcher(IEnumerable<string> patterns)
{
var ordinalComparison = Expression.Constant(StringComparison.Ordinal, typeof(StringComparison));
var asSpan = typeof(MemoryExtensions).GetMethod("AsSpan", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, [typeof(string)], []);
var equals = typeof(MemoryExtensions).GetMethod("Equals", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, [typeof(ReadOnlySpan<char>), typeof(ReadOnlySpan<char>), typeof(StringComparison)], []);
var startsWith = typeof(MemoryExtensions).GetMethod("StartsWith", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, [typeof(ReadOnlySpan<char>), typeof(ReadOnlySpan<char>), typeof(StringComparison)], []);
var endsWith = typeof(MemoryExtensions).GetMethod("EndsWith", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, [typeof(ReadOnlySpan<char>), typeof(ReadOnlySpan<char>), typeof(StringComparison)], []);

var predicates = new List<Expression>();
var left = Expression.Parameter(typeof(ReadOnlySpan<char>), "fileName");

foreach (var pattern in patterns)
var matchers = patterns.Select<string, FilePatternMatcher>(pattern => pattern switch
{
if (pattern.StartsWith('*'))
{
var match = Expression.Constant(pattern[1..], typeof(string));
var right = Expression.Call(null, asSpan, match);
var combine = Expression.Call(null, endsWith, left, right, ordinalComparison);
predicates.Add(combine);
}
else if (pattern.EndsWith('*'))
{
var match = Expression.Constant(pattern[..^1], typeof(string));
var right = Expression.Call(null, asSpan, match);
var combine = Expression.Call(null, startsWith, left, right, ordinalComparison);
predicates.Add(combine);
}
else
_ when pattern.StartsWith('*') && pattern.EndsWith('*') =>
pattern.Length <= 2
? _ => true
: span => span.Contains(pattern.AsSpan(1, pattern.Length - 2), StringComparison.Ordinal),
_ when pattern.StartsWith('*') =>
span => span.EndsWith(pattern.AsSpan(1), StringComparison.Ordinal),
_ when pattern.EndsWith('*') =>
span => span.StartsWith(pattern.AsSpan(0, pattern.Length - 1), StringComparison.Ordinal),
_ => span => span.Equals(pattern.AsSpan(), StringComparison.Ordinal),
Comment thread
JamieMagee marked this conversation as resolved.
}).ToList();
Comment thread
JamieMagee marked this conversation as resolved.

return span =>
{
foreach (var matcher in matchers)
{
var match = Expression.Constant(pattern, typeof(string));
var right = Expression.Call(null, asSpan, match);
var combine = Expression.Call(null, equals, left, right, ordinalComparison);
predicates.Add(combine);
if (matcher(span))
{
return true;
}
}
}

var aggregateExpression = predicates.Aggregate(Expression.OrElse);

var func = Expression.Lambda<FilePatternMatcher>(aggregateExpression, left).Compile();

return func;
return false;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,43 @@ namespace Microsoft.ComponentDetection.Common.Tests;
public class PatternMatchingUtilityTests
{
[TestMethod]
public void PatternMatcher_Matches_StartsWith()
[DataRow("test*", "test123", true)]
[DataRow("test*", "123test", false)]
[DataRow("*test", "123test", true)]
[DataRow("*test", "test123", false)]
[DataRow("test", "test", true)]
[DataRow("test", "123test", false)]
[DataRow("*test*", "123test456", true)]
[DataRow("*test*", "test456", true)]
[DataRow("*test*", "123test", true)]
[DataRow("*test*", "test", true)]
[DataRow("*test*", "tes", false)]
[DataRow("*", "anything", true)]
[DataRow("*", "", true)]
[DataRow("**", "anything", true)]
[DataRow("**", "", true)]
public void PatternMatcher_MatchesExpected(string pattern, string input, bool expected)
{
var pattern = "test*";
var input = "test123";

var matcher = PatternMatchingUtility.GetFilePatternMatcher([pattern]);

matcher(input).Should().BeTrue();
matcher("123test").Should().BeFalse();
matcher(input).Should().Be(expected);
}
Comment thread
JamieMagee marked this conversation as resolved.

[TestMethod]
public void PatternMatcher_Matches_EndsWith()
public void PatternMatcher_MultiplePatterns_MatchesAny()
{
var pattern = "*test";
var input = "123test";

var matcher = PatternMatchingUtility.GetFilePatternMatcher([pattern]);
var matcher = PatternMatchingUtility.GetFilePatternMatcher(["a*", "*b"]);
Comment thread
JamieMagee marked this conversation as resolved.

matcher(input).Should().BeTrue();
matcher("test123").Should().BeFalse();
matcher("apple").Should().BeTrue();
matcher("crab").Should().BeTrue();
matcher("middle").Should().BeFalse();
}

[TestMethod]
public void PatternMatcher_Matches_Exact()
public void PatternMatcher_EmptyPatterns_DoesNotThrow()
{
var pattern = "test";
var input = "test";

var matcher = PatternMatchingUtility.GetFilePatternMatcher([pattern]);
var matcher = PatternMatchingUtility.GetFilePatternMatcher([]);

matcher(input).Should().BeTrue();
matcher("123test").Should().BeFalse();
matcher("anything").Should().BeFalse();
}
}
Loading