From 89daf9655a6de5610582e3b81b319420e24bb6f5 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 13 Apr 2022 09:41:13 -0700 Subject: [PATCH] Adding Regex.EnumerateMatches (#67794) * Adding Regex.EnumerateMatches() * Addressing some feedback and implementing Count(span) on top of EnumerateMatches and cleaning up some code. * Revert Regex.Count implementation over Enumerate * PR Feedback --- .../ref/System.Text.RegularExpressions.cs | 18 ++ .../src/System.Text.RegularExpressions.csproj | 2 + .../Regex.EnumerateMatches.cs | 148 +++++++++++++ .../Text/RegularExpressions/Regex.Match.cs | 2 +- .../System/Text/RegularExpressions/Regex.cs | 38 +++- .../Text/RegularExpressions/ValueMatch.cs | 39 ++++ .../FunctionalTests/Regex.Count.Tests.cs | 2 +- .../Regex.EnumerateMatches.Tests.cs | 197 ++++++++++++++++++ .../FunctionalTests/Regex.Match.Tests.cs | 2 +- .../Regex.MultipleMatches.Tests.cs | 2 +- ...ystem.Text.RegularExpressions.Tests.csproj | 1 + 11 files changed, 444 insertions(+), 7 deletions(-) create mode 100644 src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.EnumerateMatches.cs create mode 100644 src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/ValueMatch.cs create mode 100644 src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.EnumerateMatches.Tests.cs diff --git a/src/libraries/System.Text.RegularExpressions/ref/System.Text.RegularExpressions.cs b/src/libraries/System.Text.RegularExpressions/ref/System.Text.RegularExpressions.cs index e2870a5932997..73c1a7a733910 100644 --- a/src/libraries/System.Text.RegularExpressions/ref/System.Text.RegularExpressions.cs +++ b/src/libraries/System.Text.RegularExpressions/ref/System.Text.RegularExpressions.cs @@ -171,6 +171,10 @@ public static void CompileToAssembly(System.Text.RegularExpressions.RegexCompila public static int Count(System.ReadOnlySpan input, [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex, "options")] string pattern, System.Text.RegularExpressions.RegexOptions options) { throw null; } public static int Count(System.ReadOnlySpan input, [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex, "options")] string pattern, System.Text.RegularExpressions.RegexOptions options, System.TimeSpan matchTimeout) { throw null; } public static string Escape(string str) { throw null; } + public System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan input) { throw null; } + public static System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex")] string pattern) { throw null; } + public static System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex", new object[]{ "options"})] string pattern, System.Text.RegularExpressions.RegexOptions options) { throw null; } + public static System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex", new object[]{ "options"})] string pattern, System.Text.RegularExpressions.RegexOptions options, System.TimeSpan matchTimeout) { throw null; } public string[] GetGroupNames() { throw null; } public int[] GetGroupNumbers() { throw null; } public string GroupNameFromNumber(int i) { throw null; } @@ -220,6 +224,14 @@ void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Ser protected bool UseOptionC() { throw null; } protected internal bool UseOptionR() { throw null; } protected internal static void ValidateMatchTimeout(System.TimeSpan matchTimeout) { } + public ref partial struct ValueMatchEnumerator + { + private object _dummy; + private int _dummyPrimitive; + public readonly System.Text.RegularExpressions.ValueMatch Current { get { throw null; } } + public readonly System.Text.RegularExpressions.Regex.ValueMatchEnumerator GetEnumerator() { throw null; } + public bool MoveNext() { throw null; } + } } [System.ObsoleteAttribute("Regex.CompileToAssembly is obsolete and not supported. Use the RegexGeneratorAttribute with the regular expression source generator instead.", DiagnosticId = "SYSLIB0036", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] public partial class RegexCompilationInfo @@ -359,4 +371,10 @@ public abstract partial class RegexRunnerFactory protected RegexRunnerFactory() { } protected internal abstract System.Text.RegularExpressions.RegexRunner CreateInstance(); } + public readonly ref partial struct ValueMatch + { + private readonly int _dummyPrimitive; + public int Index { get { throw null; } } + public int Length { get { throw null; } } + } } diff --git a/src/libraries/System.Text.RegularExpressions/src/System.Text.RegularExpressions.csproj b/src/libraries/System.Text.RegularExpressions/src/System.Text.RegularExpressions.csproj index 1a59caa426a67..ad33ecdd61174 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System.Text.RegularExpressions.csproj +++ b/src/libraries/System.Text.RegularExpressions/src/System.Text.RegularExpressions.csproj @@ -7,6 +7,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.EnumerateMatches.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.EnumerateMatches.cs new file mode 100644 index 0000000000000..44a0cca9e6d8d --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.EnumerateMatches.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.RegularExpressions +{ + public partial class Regex + { + /// + /// Searches an input span for all occurrences of a regular expression and returns a to iterate over the matches. + /// + /// + /// Each match won't actually happen until is invoked on the enumerator, with one match being performed per call. + /// Since the evaluation of the match happens lazily, any changes to the passed in input in between calls to will affect the match results. + /// The enumerator returned by this method, as well as the structs returned by the enumerator that wrap each match found in the input are ref structs which + /// make this method be amortized allocation free. + /// + /// The span to search for a match. + /// The regular expression pattern to match. + /// A to iterate over the matches. + /// is null. + /// A regular expression parsing error occurred. + public static ValueMatchEnumerator EnumerateMatches(ReadOnlySpan input, [StringSyntax(StringSyntaxAttribute.Regex)] string pattern) => + RegexCache.GetOrAdd(pattern).EnumerateMatches(input); + + /// + /// Searches an input span for all occurrences of a regular expression and returns a to iterate over the matches. + /// + /// + /// Each match won't actually happen until is invoked on the enumerator, with one match being performed per call. + /// Since the evaluation of the match happens lazily, any changes to the passed in input in between calls to will affect the match results. + /// The enumerator returned by this method, as well as the structs returned by the enumerator that wrap each match found in the input are ref structs which + /// make this method be amortized allocation free. + /// + /// The span to search for a match. + /// The regular expression pattern to match. + /// A bitwise combination of the enumeration values that specify options for matching. + /// A to iterate over the matches. + /// is null. + /// is not a valid bitwise combination of RegexOptions values. + /// A regular expression parsing error occurred. + public static ValueMatchEnumerator EnumerateMatches(ReadOnlySpan input, [StringSyntax(StringSyntaxAttribute.Regex, "options")] string pattern, RegexOptions options) => + RegexCache.GetOrAdd(pattern, options, s_defaultMatchTimeout).EnumerateMatches(input); + + /// + /// Searches an input span for all occurrences of a regular expression and returns a to iterate over the matches. + /// + /// + /// Each match won't actually happen until is invoked on the enumerator, with one match being performed per call. + /// Since the evaluation of the match happens lazily, any changes to the passed in input in between calls to will affect the match results. + /// The enumerator returned by this method, as well as the structs returned by the enumerator that wrap each match found in the input are ref structs which + /// make this method be amortized allocation free. + /// + /// The span to search for a match. + /// The regular expression pattern to match. + /// A bitwise combination of the enumeration values that specify options for matching. + /// A time-out interval, or to indicate that the method should not time out. + /// A to iterate over the matches. + /// is null. + /// is not a valid bitwise combination of RegexOptions values, or is negative, zero, or greater than approximately 24 days. + /// A regular expression parsing error occurred. + public static ValueMatchEnumerator EnumerateMatches(ReadOnlySpan input, [StringSyntax(StringSyntaxAttribute.Regex, "options")] string pattern, RegexOptions options, TimeSpan matchTimeout) => + RegexCache.GetOrAdd(pattern, options, matchTimeout).EnumerateMatches(input); + + /// + /// Searches an input span for all occurrences of a regular expression and returns a to iterate over the matches. + /// + /// + /// Each match won't actually happen until is invoked on the enumerator, with one match being performed per call. + /// Since the evaluation of the match happens lazily, any changes to the passed in input in between calls to will affect the match results. + /// The enumerator returned by this method, as well as the structs returned by the enumerator that wrap each match found in the input are ref structs which + /// make this method be amortized allocation free. + /// + /// The span to search for a match. + /// A to iterate over the matches. + public ValueMatchEnumerator EnumerateMatches(ReadOnlySpan input) => + new ValueMatchEnumerator(this, input, RightToLeft ? input.Length : 0); + + /// + /// Represents an enumerator containing the set of successful matches found by iteratively applying a regular expression pattern to the input span. + /// + /// + /// The enumerator has no public constructor. The method returns a + /// object.The enumerator will lazily iterate over zero or more objects. If there is at least one successful match in the span, then + /// returns and will contain the first . If there are no successful matches, + /// then returns and throws an . + /// + /// This type is a ref struct since it stores the input span as a field in order to be able to lazily iterate over it. + /// + public ref struct ValueMatchEnumerator + { + private readonly Regex _regex; + private readonly ReadOnlySpan _input; + private ValueMatch _current; + private int _startAt; + private int _prevLen; + + /// + /// Creates an instance of the for the passed in which iterates over . + /// + /// The to use for finding matches. + /// The input span to iterate over. + /// The position where the engine should start looking for matches from. + internal ValueMatchEnumerator(Regex regex, ReadOnlySpan input, int startAt) + { + _regex = regex; + _input = input; + _current = default; + _startAt = startAt; + _prevLen = -1; + } + + /// + /// Provides an enumerator that iterates through the matches in the input span. + /// + /// A copy of this enumerator. + public readonly ValueMatchEnumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next match in the span. + /// + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator cannot find additional matches. + /// + public bool MoveNext() + { + Match? match = _regex.RunSingleMatch(quick: false, _prevLen, _input, _startAt); + Debug.Assert(match != null, "Match shouldn't be null because we passed quick = false."); + if (match != RegularExpressions.Match.Empty) + { + _current = new ValueMatch(match.Index, match.Length); + _startAt = match._textpos; + _prevLen = match.Length; + return true; + } + return false; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + /// Enumeration has either not started or has already finished. + public readonly ValueMatch Current => _current; + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.Match.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.Match.cs index fe0a2cdc1ca4f..a821c0590a622 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.Match.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.Match.cs @@ -87,7 +87,7 @@ public bool IsMatch(string input) /// if the regular expression finds a match; otherwise, . /// A time-out ocurred. public bool IsMatch(ReadOnlySpan input) => - RunSingleMatch(input, RightToLeft ? input.Length : 0) is null; + RunSingleMatch(quick: true, -1, input, RightToLeft ? input.Length : 0) is null; /// /// Searches the input string for one or more matches using the previous pattern and options, diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs index bc803f5c827b9..320030f969c65 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs @@ -406,7 +406,7 @@ protected void InitializeReferences() } /// Internal worker which will scan the passed in span for a match. Used by public APIs. - internal Match? RunSingleMatch(ReadOnlySpan input, int startat) + internal Match? RunSingleMatch(bool quick, int prevlen, ReadOnlySpan input, int startat) { // startat parameter is always either 0 or input.Length since public API for IsMatch doesn't have an overload // that takes in startat. @@ -416,13 +416,45 @@ protected void InitializeReferences() try { runner.InitializeTimeout(internalMatchTimeout); - runner.InitializeForScan(this, input, startat, quick: true); + runner.InitializeForScan(this, input, startat, quick); + + // If previous match was empty or failed, advance by one before matching. + if (prevlen == 0) + { + if (RightToLeft) + { + if (runner.runtextstart == 0) + { + return RegularExpressions.Match.Empty; + } + runner.runtextpos--; + } + else + { + if (runner.runtextstart == input.Length) + { + return RegularExpressions.Match.Empty; + } + runner.runtextpos++; + } + } runner.Scan(input); // If runmatch is null it means that an override of Scan didn't implement it correctly, so we will // let this null ref since there are lots of ways where you can end up in a erroneous state. - return runner.runmatch!.FoundMatch ? null : RegularExpressions.Match.Empty; + Match match = runner.runmatch!; + if (match!.FoundMatch) + { + if (quick) + { + return null; + } + match.Tidy(runner.runtextpos, 0); + return match; + } + + return RegularExpressions.Match.Empty; } finally { diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/ValueMatch.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/ValueMatch.cs new file mode 100644 index 0000000000000..7380edfb422fd --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/ValueMatch.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.RegularExpressions +{ + /// + /// Represents the results from a single regular expression match. + /// + /// + /// The type is immutable and has no public constructor. An instance of the struct is returned by the + /// method when iterating over the results from calling . + /// + public readonly ref struct ValueMatch + { + private readonly int _index; + private readonly int _length; + + /// + /// Crates an instance of the type based on the passed in and . + /// + /// The position in the original span where the first character of the captured sliced span is found. + /// The length of the captured sliced span. + internal ValueMatch(int index, int length) + { + _index = index; + _length = length; + } + + /// + /// Gets the position in the original span where the first character of the captured sliced span is found. + /// + public int Index => _index; + + /// + /// Gets the length of the captured sliced span. + /// + public int Length => _length; + } +} diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Count.Tests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Count.Tests.cs index 966b54c219d22..c7aab09e9b151 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Count.Tests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Count.Tests.cs @@ -8,7 +8,7 @@ namespace System.Text.RegularExpressions.Tests { - public class RegexCountTests + public partial class RegexCountTests { [Theory] [MemberData(nameof(Count_ReturnsExpectedCount_TestData))] diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.EnumerateMatches.Tests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.EnumerateMatches.Tests.cs new file mode 100644 index 0000000000000..6defa59ff8932 --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.EnumerateMatches.Tests.cs @@ -0,0 +1,197 @@ +// 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 System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.RegularExpressions.Tests +{ + public class RegexEnumerateMatchesTests + { + public static IEnumerable NoneCompiledBacktracking() + { + yield return new object[] { RegexOptions.None }; + yield return new object[] { RegexOptions.Compiled }; + if (PlatformDetection.IsNetCore) + { + yield return new object[] { RegexHelpers.RegexOptionNonBacktracking }; + } + } + + [Fact] + public void EnumerateMatches_Ctor_Invalid() + { + // Pattern is null + AssertExtensions.Throws("pattern", () => Regex.EnumerateMatches("input", null)); + AssertExtensions.Throws("pattern", () => Regex.EnumerateMatches("input", null, RegexOptions.None)); + AssertExtensions.Throws("pattern", () => Regex.EnumerateMatches("input", null, RegexOptions.None, TimeSpan.FromSeconds(1))); + + // Options are invalid + AssertExtensions.Throws("options", () => Regex.EnumerateMatches("input", "pattern", (RegexOptions)(-1))); + AssertExtensions.Throws("options", () => Regex.EnumerateMatches("input", "pattern", (RegexOptions)(-1), TimeSpan.FromSeconds(1))); + + // 0x400 is new NonBacktracking mode that is now valid, 0x800 is still invalid + AssertExtensions.Throws("options", () => Regex.EnumerateMatches("input", "pattern", (RegexOptions)0x800)); + AssertExtensions.Throws("options", () => Regex.EnumerateMatches("input", "pattern", (RegexOptions)0x800, TimeSpan.FromSeconds(1))); + + // MatchTimeout is invalid + AssertExtensions.Throws("matchTimeout", () => Regex.EnumerateMatches("input", "pattern", RegexOptions.None, TimeSpan.Zero)); + AssertExtensions.Throws("matchTimeout", () => Regex.EnumerateMatches("input", "pattern", RegexOptions.None, TimeSpan.Zero)); + } + + [Theory] + [MemberData(nameof(RegexHelpers.AvailableEngines_MemberData), MemberType = typeof(RegexHelpers))] + public void EnumerateMatches_Lookahead(RegexEngine engine) + { + if (RegexHelpers.IsNonBacktracking(engine)) + { + // lookaheads not supported + return; + } + + const string Pattern = @"\b(?!un)\w+\b"; + const string Input = "unite one unethical ethics use untie ultimate"; + + Regex r = RegexHelpers.GetRegexAsync(engine, Pattern, RegexOptions.IgnoreCase).GetAwaiter().GetResult(); + int count = 0; + string[] expectedMatches = new[] { "one", "ethics", "use", "ultimate" }; + ReadOnlySpan span = Input.AsSpan(); + foreach (ValueMatch match in r.EnumerateMatches(span)) + { + Assert.Equal(expectedMatches[count++], span.Slice(match.Index, match.Length).ToString()); + } + Assert.Equal(4, count); + } + + [Theory] + [MemberData(nameof(RegexHelpers.AvailableEngines_MemberData), MemberType = typeof(RegexHelpers))] + public void EnumerateMatches_Lookbehind(RegexEngine engine) + { + if (RegexHelpers.IsNonBacktracking(engine)) + { + // lookbehinds not supported + return; + } + + const string Pattern = @"(?<=\b20)\d{2}\b"; + const string Input = "2010 1999 1861 2140 2009"; + + Regex r = RegexHelpers.GetRegexAsync(engine, Pattern, RegexOptions.IgnoreCase).GetAwaiter().GetResult(); + int count = 0; + string[] expectedMatches = new[] { "10", "09" }; + ReadOnlySpan span = Input.AsSpan(); + foreach (ValueMatch match in r.EnumerateMatches(span)) + { + Assert.Equal(expectedMatches[count++], span.Slice(match.Index, match.Length).ToString()); + } + Assert.Equal(2, count); + } + + [Theory] + [MemberData(nameof(RegexHelpers.AvailableEngines_MemberData), MemberType = typeof(RegexHelpers))] + public void EnumerateMatches_CheckIndex(RegexEngine engine) + { + const string Pattern = @"e{2}\w\b"; + const string Input = "needing a reed"; + + Regex r = RegexHelpers.GetRegexAsync(engine, Pattern).GetAwaiter().GetResult(); + int count = 0; + string[] expectedMatches = new[] { "eed" }; + int[] expectedIndex = new[] { 11 }; + ReadOnlySpan span = Input.AsSpan(); + foreach (ValueMatch match in r.EnumerateMatches(span)) + { + Assert.Equal(expectedMatches[count], span.Slice(match.Index, match.Length).ToString()); + Assert.Equal(expectedIndex[count++], match.Index); + } + } + } + + public partial class RegexMultipleMatchTests + { + [Theory] + [MemberData(nameof(Matches_TestData))] + public void EnumerateMatches(RegexEngine engine, string pattern, string input, RegexOptions options, CaptureData[] expected) + { + Regex regexAdvanced = RegexHelpers.GetRegexAsync(engine, pattern, options).GetAwaiter().GetResult(); + int count = 0; + ReadOnlySpan span = input.AsSpan(); + foreach (ValueMatch match in regexAdvanced.EnumerateMatches(span)) + { + Assert.Equal(expected[count].Index, match.Index); + Assert.Equal(expected[count].Length, match.Length); + Assert.Equal(expected[count].Value, span.Slice(match.Index, match.Length).ToString()); + count++; + } + Assert.Equal(expected.Length, count); + } + } + + public partial class RegexMatchTests + { + [Theory] + [MemberData(nameof(Match_Count_TestData))] + public void EnumerateMatches_Count(RegexEngine engine, string pattern, string input, int expectedCount) + { + Regex r = RegexHelpers.GetRegexAsync(engine, pattern).GetAwaiter().GetResult(); + int count = 0; + foreach (ValueMatch _ in r.EnumerateMatches(input)) + { + count++; + } + Assert.Equal(expectedCount, count); + } + } + + public partial class RegexCountTests + { + [Theory] + [MemberData(nameof(Count_ReturnsExpectedCount_TestData))] + public void EnumerateMatches_ReturnsExpectedCount(RegexEngine engine, string pattern, string input, RegexOptions options, int expectedCount) + { + Regex r = RegexHelpers.GetRegexAsync(engine, pattern, options).GetAwaiter().GetResult(); + int count = 0; + foreach (ValueMatch _ in r.EnumerateMatches(input)) + { + count++; + } + Assert.Equal(expectedCount, count); + + if (options == RegexOptions.None && engine == RegexEngine.Interpreter) + { + count = 0; + foreach (ValueMatch _ in Regex.EnumerateMatches(input, pattern)) + { + count++; + } + Assert.Equal(expectedCount, count); + } + + switch (engine) + { + case RegexEngine.Interpreter: + case RegexEngine.Compiled: + case RegexEngine.NonBacktracking: + RegexOptions engineOptions = RegexHelpers.OptionsFromEngine(engine); + count = 0; + foreach (ValueMatch _ in Regex.EnumerateMatches(input, pattern, options | engineOptions)) + { + count++; + } + Assert.Equal(expectedCount, count); + + count = 0; + foreach (ValueMatch _ in Regex.EnumerateMatches(input, pattern, options | engineOptions, Regex.InfiniteMatchTimeout)) + { + count++; + } + Assert.Equal(expectedCount, count); + break; + } + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs index 2c1b64fa70026..e60fb57ad8854 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs @@ -13,7 +13,7 @@ namespace System.Text.RegularExpressions.Tests { - public class RegexMatchTests + public partial class RegexMatchTests { public static IEnumerable Match_MemberData() { diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.MultipleMatches.Tests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.MultipleMatches.Tests.cs index e3884f15a2dcc..65a3b5cd294a1 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.MultipleMatches.Tests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.MultipleMatches.Tests.cs @@ -9,7 +9,7 @@ namespace System.Text.RegularExpressions.Tests { - public class RegexMultipleMatchTests + public partial class RegexMultipleMatchTests { [Theory] [MemberData(nameof(RegexHelpers.AvailableEngines_MemberData), MemberType = typeof(RegexHelpers))] diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/System.Text.RegularExpressions.Tests.csproj b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/System.Text.RegularExpressions.Tests.csproj index 4be0e31a2c509..ceed4984ce0df 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/System.Text.RegularExpressions.Tests.csproj +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/System.Text.RegularExpressions.Tests.csproj @@ -47,6 +47,7 @@ +