From 7eb0fa89b465838d3a32ac26ba8516a241044eb9 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 20 Feb 2026 20:52:14 -0700 Subject: [PATCH 1/6] Fix: Allow inline regex options in conditional expression branches Fixes #111633. The regex parser incorrectly rejected inline options like (?i) inside the yes/no branches of expression conditionals (e.g. (?(test)(?i)yes|no)), throwing InvalidGroupingConstruct. The check in ScanGroupOpen blocked ScanOptions() whenever _group was an ExpressionConditional, but this was too broad - it blocked options in the branches, not just in the test expression. The fix narrows the check to only block options when no test expression has been added yet (ChildCount() == 0), allowing options in the branches (ChildCount > 0). Note: BackreferenceConditional was never blocked, confirming this was an unintentional inconsistency introduced in corefx PR #16609. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Text/RegularExpressions/RegexParser.cs | 5 +++-- .../tests/FunctionalTests/Regex.Match.Tests.cs | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs index 69511af80983bd..d30ec774159cb9 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs @@ -1000,8 +1000,9 @@ private RegexNode ScanReplacement() --_pos; nodeType = RegexNodeKind.Group; - // Disallow options in the children of a testgroup node - if (_group!.Kind != RegexNodeKind.ExpressionConditional) + // Disallow options as the test expression of a conditional (when no test has been added yet), + // but allow them in the yes/no branches. + if (_group!.Kind != RegexNodeKind.ExpressionConditional || _group.ChildCount() > 0) { ScanOptions(); } 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 c03f22a893ac1a..d015b22cb17239 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 @@ -898,6 +898,15 @@ public static IEnumerable Match_MemberData() yield return (@"(?(\w+)\w+)dog", "catdog", RegexOptions.None, 0, 6, true, "catdog"); yield return (@"(?(abc)\w+|\w{0,2})dog", "catdog", RegexOptions.None, 0, 6, true, "atdog"); yield return (@"(?(abc)cat|\w{0,2})dog", "catdog", RegexOptions.None, 0, 6, true, "atdog"); + + // Inline options inside conditional branches (https://github.com/dotnet/runtime/issues/111633) + yield return (@"(?(cat)(?i)CAT|dog)", "cat", RegexOptions.None, 0, 3, true, "cat"); + yield return (@"(?(cat)(?i)cat|dog)", "dog", RegexOptions.None, 0, 3, true, "dog"); + yield return (@"(?(cat)cat|(?i)dog)", "DOG", RegexOptions.None, 0, 3, true, "DOG"); + yield return (@"(?(cat)(?i:CAT)|dog)", "cat", RegexOptions.None, 0, 3, true, "cat"); + yield return (@"(?(?=cat)(?i)CAT|dog)", "cat", RegexOptions.None, 0, 3, true, "cat"); + yield return (@"(cat)?(?(1)(?i)dog|pig)", "catDOG", RegexOptions.None, 0, 6, true, "catDOG"); + yield return (@"(cat)?(?(1)(?i)dog|pig)", "pig", RegexOptions.None, 0, 3, true, "pig"); yield return ("(a|ab|abc|abcd)d", "abcd", RegexOptions.RightToLeft, 0, 4, true, "abcd"); yield return ("(?>(?:a|ab|abc|abcd))d", "abcd", RegexOptions.None, 0, 4, false, string.Empty); From 0b3f1a8836fb2a8453b059498077756e666b9498 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 20 Feb 2026 22:15:41 -0700 Subject: [PATCH 2/6] Add negative test for inline options as conditional test expression Verify that (?(?i)yes|no) is still rejected as InvalidGroupingConstruct, ensuring the fix only allows options in yes/no branches, not as the test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/RegexParserTests.netcoreapp.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs index a3e02e8439e2a9..b8daa83b589510 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs @@ -26,6 +26,7 @@ public partial class RegexParserTests [InlineData(@"(?(?X))", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] [InlineData(@"(?(?n))", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] [InlineData(@"(?(?m))", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] + [InlineData(@"(?(?i)yes|no)", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] // options as test expression with branches still blocked [InlineData("(?<-", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 3)] [InlineData("(?<-", RegexOptions.IgnorePatternWhitespace, RegexParseError.InvalidGroupingConstruct, 3)] [InlineData(@"^[^<>]*(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))$", RegexOptions.None, null)] From 3e9780e6b3be070ef101212bf32a82abaf891c1d Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Sun, 22 Feb 2026 22:24:07 -0700 Subject: [PATCH 3/6] Clarify that (?(?i)yes|no) not throwing on netfx is a parser bug Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/RegexParserTests.netcoreapp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs index b8daa83b589510..06e157a7587659 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexParserTests.netcoreapp.cs @@ -26,7 +26,7 @@ public partial class RegexParserTests [InlineData(@"(?(?X))", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] [InlineData(@"(?(?n))", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] [InlineData(@"(?(?m))", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] - [InlineData(@"(?(?i)yes|no)", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] // options as test expression with branches still blocked + [InlineData(@"(?(?i)yes|no)", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 5)] // does not throw on .NET Framework, but that is a parser bug there [InlineData("(?<-", RegexOptions.None, RegexParseError.InvalidGroupingConstruct, 3)] [InlineData("(?<-", RegexOptions.IgnorePatternWhitespace, RegexParseError.InvalidGroupingConstruct, 3)] [InlineData(@"^[^<>]*(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))$", RegexOptions.None, null)] From 7690fdd1d5c391a820ceb20b4e0547ba6e0517ed Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Sun, 22 Feb 2026 22:34:21 -0700 Subject: [PATCH 4/6] Improve comment describing inline options restriction in conditional test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Text/RegularExpressions/RegexParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs index d30ec774159cb9..eaf5f23d3c639f 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs @@ -1000,8 +1000,8 @@ private RegexNode ScanReplacement() --_pos; nodeType = RegexNodeKind.Group; - // Disallow options as the test expression of a conditional (when no test has been added yet), - // but allow them in the yes/no branches. + // While parsing the test of a conditional (ExpressionConditional with no children yet), + // disallow inline options; allow them once the test has been parsed and in the yes/no branches. if (_group!.Kind != RegexNodeKind.ExpressionConditional || _group.ChildCount() > 0) { ScanOptions(); From 5424aa0418815b625779ca1dde9f5a62f602b5f3 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Mon, 23 Feb 2026 10:36:58 -0700 Subject: [PATCH 5/6] Add test for inline options inside conditional test expression Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/Regex.Match.Tests.cs | 1 + 1 file changed, 1 insertion(+) 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 d015b22cb17239..6d3c649b22ec42 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 @@ -907,6 +907,7 @@ public static IEnumerable Match_MemberData() yield return (@"(?(?=cat)(?i)CAT|dog)", "cat", RegexOptions.None, 0, 3, true, "cat"); yield return (@"(cat)?(?(1)(?i)dog|pig)", "catDOG", RegexOptions.None, 0, 6, true, "catDOG"); yield return (@"(cat)?(?(1)(?i)dog|pig)", "pig", RegexOptions.None, 0, 3, true, "pig"); + yield return (@"(?((?i)cat)CAT|dog)", "CAT", RegexOptions.None, 0, 3, true, "CAT"); yield return ("(a|ab|abc|abcd)d", "abcd", RegexOptions.RightToLeft, 0, 4, true, "abcd"); yield return ("(?>(?:a|ab|abc|abcd))d", "abcd", RegexOptions.None, 0, 4, false, string.Empty); From 367253f4f6be8f5ce268cc5365fe7be8da117f8c Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Mon, 23 Feb 2026 10:39:06 -0700 Subject: [PATCH 6/6] Remove issue link from test comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/Regex.Match.Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6d3c649b22ec42..0a5e4e4d7e6fe5 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 @@ -899,7 +899,7 @@ public static IEnumerable Match_MemberData() yield return (@"(?(abc)\w+|\w{0,2})dog", "catdog", RegexOptions.None, 0, 6, true, "atdog"); yield return (@"(?(abc)cat|\w{0,2})dog", "catdog", RegexOptions.None, 0, 6, true, "atdog"); - // Inline options inside conditional branches (https://github.com/dotnet/runtime/issues/111633) + // Inline options inside conditional branches yield return (@"(?(cat)(?i)CAT|dog)", "cat", RegexOptions.None, 0, 3, true, "cat"); yield return (@"(?(cat)(?i)cat|dog)", "dog", RegexOptions.None, 0, 3, true, "dog"); yield return (@"(?(cat)cat|(?i)dog)", "DOG", RegexOptions.None, 0, 3, true, "DOG");