From 8547c2c6d448f46518071e0bab8b3f3f1c875111 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Mon, 6 Apr 2026 12:35:56 -0700 Subject: [PATCH 1/3] Replace expression tree pattern matching with closures --- .../PatternMatchingUtility.cs | 59 +++++++------------ .../PatternMatchingUtilityTests.cs | 47 ++++++--------- 2 files changed, 38 insertions(+), 68 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs b/src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs index 884e79375..c39cf7dd7 100644 --- a/src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs +++ b/src/Microsoft.ComponentDetection.Common/PatternMatchingUtility.cs @@ -1,11 +1,8 @@ -#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 { @@ -13,44 +10,30 @@ public static class PatternMatchingUtility public static FilePatternMatcher GetFilePatternMatcher(IEnumerable 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), typeof(ReadOnlySpan), typeof(StringComparison)], []); - var startsWith = typeof(MemoryExtensions).GetMethod("StartsWith", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, [typeof(ReadOnlySpan), typeof(ReadOnlySpan), typeof(StringComparison)], []); - var endsWith = typeof(MemoryExtensions).GetMethod("EndsWith", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, [typeof(ReadOnlySpan), typeof(ReadOnlySpan), typeof(StringComparison)], []); - - var predicates = new List(); - var left = Expression.Parameter(typeof(ReadOnlySpan), "fileName"); - - foreach (var pattern in patterns) + var matchers = patterns.Select(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), + }).ToList(); + + 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(aggregateExpression, left).Compile(); - return func; + return false; + }; } } diff --git a/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs index a0a019b73..1ccb59ac8 100644 --- a/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs +++ b/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs @@ -10,38 +10,25 @@ 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(); - } - - [TestMethod] - public void PatternMatcher_Matches_EndsWith() - { - var pattern = "*test"; - var input = "123test"; - - var matcher = PatternMatchingUtility.GetFilePatternMatcher([pattern]); - - matcher(input).Should().BeTrue(); - matcher("test123").Should().BeFalse(); - } - - [TestMethod] - public void PatternMatcher_Matches_Exact() - { - var pattern = "test"; - var input = "test"; - var matcher = PatternMatchingUtility.GetFilePatternMatcher([pattern]); - matcher(input).Should().BeTrue(); - matcher("123test").Should().BeFalse(); + matcher(input).Should().Be(expected); } } From 3cb9e1ec96a80ae817dc0968996214b894b2f3c9 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Mon, 6 Apr 2026 13:08:36 -0700 Subject: [PATCH 2/3] Add multi-pattern and edge case tests --- .../PatternMatchingUtilityTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs index 1ccb59ac8..a4b30fff3 100644 --- a/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs +++ b/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs @@ -31,4 +31,14 @@ public void PatternMatcher_MatchesExpected(string pattern, string input, bool ex matcher(input).Should().Be(expected); } + + [TestMethod] + public void PatternMatcher_MultiplePatterns_MatchesAny() + { + var matcher = PatternMatchingUtility.GetFilePatternMatcher(["a*", "*b"]); + + matcher("apple").Should().BeTrue(); + matcher("crab").Should().BeTrue(); + matcher("middle").Should().BeFalse(); + } } From e7b0e8e563130f49a06eb8e4bf240439c24e0b80 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Mon, 6 Apr 2026 13:16:59 -0700 Subject: [PATCH 3/3] Add empty-patterns test --- .../PatternMatchingUtilityTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs index a4b30fff3..b3bb016f3 100644 --- a/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs +++ b/test/Microsoft.ComponentDetection.Common.Tests/PatternMatchingUtilityTests.cs @@ -41,4 +41,12 @@ public void PatternMatcher_MultiplePatterns_MatchesAny() matcher("crab").Should().BeTrue(); matcher("middle").Should().BeFalse(); } + + [TestMethod] + public void PatternMatcher_EmptyPatterns_DoesNotThrow() + { + var matcher = PatternMatchingUtility.GetFilePatternMatcher([]); + + matcher("anything").Should().BeFalse(); + } }