Skip to content

Commit e661de4

Browse files
Copilotdanmoseleystephentoub
authored
"Convert to GeneratedRegexAttribute" mangles Constants (#116149)
* update instructions * Fix RegexOptions constant handling in GeneratedRegexAttribute --------- Co-authored-by: Dan Moseley <[email protected]> Co-authored-by: Stephen Toub <[email protected]>
1 parent ede0118 commit e661de4

File tree

3 files changed

+287
-31
lines changed

3 files changed

+287
-31
lines changed

.github/copilot-instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ In addition to the rules enforced by `.editorconfig`, you SHOULD:
1616
- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
1717
- Prefer `?.` if applicable (e.g. `scope?.Dispose()`).
1818
- Use `ObjectDisposedException.ThrowIf` where applicable.
19+
- When adding new unit tests, strongly prefer to add them to existing test code files rather than creating new code files.
20+
- If you add new code files, ensure they are listed in the csproj file (if other files in that folder are listed there) so they build.
21+
- When running tests, if possible use filters and check test run counts, or look at test logs, to ensure they actually ran.
22+
- Do not finish work with any tests commented out or disabled that were not previously commented out or disabled.
1923
- When writing tests, do not emit "Act", "Arrange" or "Assert" comments.
2024

2125
---

src/libraries/System.Text.RegularExpressions/gen/UpgradeToGeneratedRegexCodeFixer.cs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -281,29 +281,35 @@ static RegexOptions GetRegexOptionsFromArgument(ImmutableArray<IArgumentOperatio
281281
return null;
282282
}
283283

284-
Debug.Assert(parameterName is UpgradeToGeneratedRegexAnalyzer.OptionsArgumentName or UpgradeToGeneratedRegexAnalyzer.PatternArgumentName);
285-
if (parameterName == UpgradeToGeneratedRegexAnalyzer.OptionsArgumentName)
284+
// Literals and class-level field references should be preserved as-is.
285+
if (argument.Value is ILiteralOperation ||
286+
argument.Value is IFieldReferenceOperation { Member: IFieldSymbol { IsConst: true } })
286287
{
287-
string optionsLiteral = Literal(((RegexOptions)(int)argument.Value.ConstantValue.Value!).ToString());
288-
return SyntaxFactory.ParseExpression(optionsLiteral);
288+
return argument.Value.Syntax;
289289
}
290-
else if (argument.Value is ILiteralOperation literalOperation)
291-
{
292-
return literalOperation.Syntax;
293-
}
294-
else if (argument.Value is IFieldReferenceOperation fieldReferenceOperation &&
295-
fieldReferenceOperation.Member is IFieldSymbol fieldSymbol && fieldSymbol.IsConst)
296-
{
297-
return generator.Argument(fieldReferenceOperation.Syntax);
298-
}
299-
else if (argument.Value.ConstantValue.Value is string str && str.Contains('\\'))
300-
{
301-
string escapedVerbatimText = str.Replace("\"", "\"\"");
302-
return SyntaxFactory.ParseExpression($"@\"{escapedVerbatimText}\"");
303-
}
304-
else
290+
291+
switch (parameterName)
305292
{
306-
return generator.LiteralExpression(argument.Value.ConstantValue.Value);
293+
case UpgradeToGeneratedRegexAnalyzer.OptionsArgumentName:
294+
string optionsLiteral = Literal(((RegexOptions)(int)argument.Value.ConstantValue.Value!).ToString());
295+
return SyntaxFactory.ParseExpression(optionsLiteral);
296+
297+
case UpgradeToGeneratedRegexAnalyzer.PatternArgumentName:
298+
if (argument.Value.ConstantValue.Value is string str && str.Contains('\\'))
299+
{
300+
// Special handling for string patterns with escaped characters
301+
string escapedVerbatimText = str.Replace("\"", "\"\"");
302+
return SyntaxFactory.ParseExpression($"@\"{escapedVerbatimText}\"");
303+
}
304+
else
305+
{
306+
// Default handling for all other patterns.
307+
return generator.LiteralExpression(argument.Value.ConstantValue.Value);
308+
}
309+
310+
default:
311+
Debug.Fail($"Unknown parameter: {parameterName}");
312+
return argument.Syntax;
307313
}
308314
}
309315

src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/UpgradeToGeneratedRegexAnalyzerTests.cs

Lines changed: 257 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,16 +256,16 @@ public static void Main(string[] args)
256256
private static partial Regex MyRegex();
257257
}" };
258258

259-
// Test constructor with a local constant pattern.
259+
// Test constructor with a local constant pattern (local constants are expanded).
260260
yield return new object[] { @"using System.Text;
261261
using System.Text.RegularExpressions;
262262
263263
public class Program
264264
{
265265
public static void Main(string[] args)
266266
{
267-
const string pattern = @"""";
268-
var isMatch = [|" + ConstructRegexInvocation(invocationType, "\"\"") + @"|]" + isMatchInvocation + @";
267+
const string pattern = @""a|b"";
268+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "pattern") + @"|]" + isMatchInvocation + @";
269269
}
270270
}", @"using System.Text;
271271
using System.Text.RegularExpressions;
@@ -274,39 +274,73 @@ public partial class Program
274274
{
275275
public static void Main(string[] args)
276276
{
277-
const string pattern = @"""";
277+
const string pattern = @""a|b"";
278278
var isMatch = MyRegex().IsMatch("""");
279279
}
280280
281-
[GeneratedRegex("""")]
281+
[GeneratedRegex(""a|b"")]
282282
private static partial Regex MyRegex();
283283
}" };
284284

285-
// Test constructor with a constant field pattern.
285+
// Test constructor with a constant field pattern (field constants are preserved).
286286
yield return new object[] { @"using System.Text;
287287
using System.Text.RegularExpressions;
288288
289289
public class Program
290290
{
291-
private const string pattern = @"""";
291+
private const string Pattern = @""a|b"";
292292
293293
public static void Main(string[] args)
294294
{
295-
var isMatch = [|" + ConstructRegexInvocation(invocationType, "\"\"") + @"|]" + isMatchInvocation + @";
295+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "Pattern") + @"|]" + isMatchInvocation + @";
296296
}
297297
}", @"using System.Text;
298298
using System.Text.RegularExpressions;
299299
300300
public partial class Program
301301
{
302-
private const string pattern = @"""";
302+
private const string Pattern = @""a|b"";
303303
304304
public static void Main(string[] args)
305305
{
306306
var isMatch = MyRegex().IsMatch("""");
307307
}
308308
309-
[GeneratedRegex("""")]
309+
[GeneratedRegex(Pattern)]
310+
private static partial Regex MyRegex();
311+
}" };
312+
313+
// Test constructor with external constant field pattern (external field constants are preserved).
314+
yield return new object[] { @"using System.Text;
315+
using System.Text.RegularExpressions;
316+
317+
public class PatternConstants
318+
{
319+
public const string EmailPattern = @""^[^@]+@[^@]+\.[^@]+$"";
320+
}
321+
322+
public class Program
323+
{
324+
public static void Main(string[] args)
325+
{
326+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "PatternConstants.EmailPattern") + @"|]" + isMatchInvocation + @";
327+
}
328+
}", @"using System.Text;
329+
using System.Text.RegularExpressions;
330+
331+
public class PatternConstants
332+
{
333+
public const string EmailPattern = @""^[^@]+@[^@]+\.[^@]+$"";
334+
}
335+
336+
public partial class Program
337+
{
338+
public static void Main(string[] args)
339+
{
340+
var isMatch = MyRegex().IsMatch("""");
341+
}
342+
343+
[GeneratedRegex(PatternConstants.EmailPattern)]
310344
private static partial Regex MyRegex();
311345
}" };
312346
}
@@ -468,7 +502,39 @@ public static void Main(string[] args)
468502
var isMatch = MyRegex().IsMatch("""");
469503
}
470504
471-
[GeneratedRegex("""", RegexOptions.None)]
505+
[GeneratedRegex("""", Options)]
506+
private static partial Regex MyRegex();
507+
}" };
508+
509+
// Test options as external constant field
510+
yield return new object[] { @"using System.Text.RegularExpressions;
511+
512+
public class RegexConstants
513+
{
514+
public const RegexOptions DefaultOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
515+
}
516+
517+
public class Program
518+
{
519+
public static void Main(string[] args)
520+
{
521+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "\"\"", "RegexConstants.DefaultOptions") + @"|]" + isMatchInvocation + @";
522+
}
523+
}", @"using System.Text.RegularExpressions;
524+
525+
public class RegexConstants
526+
{
527+
public const RegexOptions DefaultOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
528+
}
529+
530+
public partial class Program
531+
{
532+
public static void Main(string[] args)
533+
{
534+
var isMatch = MyRegex().IsMatch("""");
535+
}
536+
537+
[GeneratedRegex("""", RegexConstants.DefaultOptions)]
472538
private static partial Regex MyRegex();
473539
}" };
474540
}
@@ -481,6 +547,186 @@ public async Task DiagnosticEmittedForConstantOptions(string test, string fixedS
481547
await VerifyCS.VerifyCodeFixAsync(test, fixedSource);
482548
}
483549

550+
public static IEnumerable<object[]> MixedConstantTestData()
551+
{
552+
foreach (InvocationType invocationType in new[] { InvocationType.Constructor, InvocationType.StaticMethods })
553+
{
554+
string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty;
555+
556+
// Test both pattern and options as field constants (both should be preserved)
557+
yield return new object[] { @"using System.Text.RegularExpressions;
558+
559+
public class Program
560+
{
561+
const string MyPattern = @""[a-z]+"";
562+
const RegexOptions MyOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
563+
564+
public static void Main(string[] args)
565+
{
566+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "MyPattern", "MyOptions") + @"|]" + isMatchInvocation + @";
567+
}
568+
}", @"using System.Text.RegularExpressions;
569+
570+
public partial class Program
571+
{
572+
const string MyPattern = @""[a-z]+"";
573+
const RegexOptions MyOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
574+
575+
public static void Main(string[] args)
576+
{
577+
var isMatch = MyRegex().IsMatch("""");
578+
}
579+
580+
[GeneratedRegex(MyPattern, MyOptions)]
581+
private static partial Regex MyRegex();
582+
}" };
583+
584+
// Test pattern as field constant and options as local constant (field preserved, local expanded)
585+
yield return new object[] { @"using System.Text.RegularExpressions;
586+
587+
public class Program
588+
{
589+
const string GlobalPattern = @""\d+"";
590+
591+
public static void Main(string[] args)
592+
{
593+
const RegexOptions localOptions = RegexOptions.Multiline;
594+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "GlobalPattern", "localOptions") + @"|]" + isMatchInvocation + @";
595+
}
596+
}", @"using System.Text.RegularExpressions;
597+
598+
public partial class Program
599+
{
600+
const string GlobalPattern = @""\d+"";
601+
602+
public static void Main(string[] args)
603+
{
604+
const RegexOptions localOptions = RegexOptions.Multiline;
605+
var isMatch = MyRegex().IsMatch("""");
606+
}
607+
608+
[GeneratedRegex(GlobalPattern, RegexOptions.Multiline)]
609+
private static partial Regex MyRegex();
610+
}" };
611+
612+
// Test pattern as local constant and options as field constant (local expanded, field preserved)
613+
yield return new object[] { @"using System.Text.RegularExpressions;
614+
615+
public class Program
616+
{
617+
const RegexOptions DefaultOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
618+
619+
public static void Main(string[] args)
620+
{
621+
const string localPattern = @""test.*pattern"";
622+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "localPattern", "DefaultOptions") + @"|]" + isMatchInvocation + @";
623+
}
624+
}", @"using System.Text.RegularExpressions;
625+
626+
public partial class Program
627+
{
628+
const RegexOptions DefaultOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
629+
630+
public static void Main(string[] args)
631+
{
632+
const string localPattern = @""test.*pattern"";
633+
var isMatch = MyRegex().IsMatch("""");
634+
}
635+
636+
[GeneratedRegex(""test.*pattern"", DefaultOptions)]
637+
private static partial Regex MyRegex();
638+
}" };
639+
640+
// Test external constants for both pattern and options
641+
yield return new object[] { @"using System.Text.RegularExpressions;
642+
643+
public static class RegexConfig
644+
{
645+
public const string EmailPattern = @""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"";
646+
public const RegexOptions EmailOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
647+
}
648+
649+
public class Program
650+
{
651+
public static void Main(string[] args)
652+
{
653+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "RegexConfig.EmailPattern", "RegexConfig.EmailOptions") + @"|]" + isMatchInvocation + @";
654+
}
655+
}", @"using System.Text.RegularExpressions;
656+
657+
public static class RegexConfig
658+
{
659+
public const string EmailPattern = @""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"";
660+
public const RegexOptions EmailOptions = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
661+
}
662+
663+
public partial class Program
664+
{
665+
public static void Main(string[] args)
666+
{
667+
var isMatch = MyRegex().IsMatch("""");
668+
}
669+
670+
[GeneratedRegex(RegexConfig.EmailPattern, RegexConfig.EmailOptions)]
671+
private static partial Regex MyRegex();
672+
}" };
673+
}
674+
}
675+
676+
[Theory]
677+
[MemberData(nameof(MixedConstantTestData))]
678+
public async Task DiagnosticEmittedForMixedConstants(string test, string fixedSource)
679+
{
680+
await VerifyCS.VerifyCodeFixAsync(test, fixedSource);
681+
}
682+
683+
public static IEnumerable<object[]> StaticFieldConstantTestData()
684+
{
685+
foreach (InvocationType invocationType in new[] { InvocationType.Constructor, InvocationType.StaticMethods })
686+
{
687+
string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty;
688+
689+
// Test static field constants (should be preserved)
690+
yield return new object[] { @"using System.Text.RegularExpressions;
691+
692+
public class Program
693+
{
694+
public static readonly string ReadOnlyPattern = @""readonly""; // This is not const, so won't be preserved
695+
public const string ConstPattern = @""const"";
696+
private static readonly RegexOptions ReadOnlyOptions = RegexOptions.IgnoreCase; // This is not const, so won't be preserved
697+
private const RegexOptions ConstOptions = RegexOptions.Multiline;
698+
699+
public static void Main(string[] args)
700+
{
701+
var isMatch = [|" + ConstructRegexInvocation(invocationType, "ConstPattern", "ConstOptions") + @"|]" + isMatchInvocation + @";
702+
}
703+
}", @"using System.Text.RegularExpressions;
704+
705+
public partial class Program
706+
{
707+
public static readonly string ReadOnlyPattern = @""readonly""; // This is not const, so won't be preserved
708+
public const string ConstPattern = @""const"";
709+
private static readonly RegexOptions ReadOnlyOptions = RegexOptions.IgnoreCase; // This is not const, so won't be preserved
710+
private const RegexOptions ConstOptions = RegexOptions.Multiline;
711+
712+
public static void Main(string[] args)
713+
{
714+
var isMatch = MyRegex().IsMatch("""");
715+
}
716+
717+
[GeneratedRegex(ConstPattern, ConstOptions)]
718+
private static partial Regex MyRegex();
719+
}" };
720+
}
721+
}
722+
723+
[Theory]
724+
[MemberData(nameof(StaticFieldConstantTestData))]
725+
public async Task DiagnosticEmittedForStaticFieldConstants(string test, string fixedSource)
726+
{
727+
await VerifyCS.VerifyCodeFixAsync(test, fixedSource);
728+
}
729+
484730
public static IEnumerable<object[]> VariableOptionsTestData()
485731
{
486732
foreach (InvocationType invocationType in new[] { InvocationType.Constructor, InvocationType.StaticMethods })

0 commit comments

Comments
 (0)