From b5df11296d75a30f910d721042243e14b3ae34e0 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 6 May 2022 11:46:28 -0700 Subject: [PATCH 1/6] Adding analyzer/fixer for the Regex Source Generator --- .../gen/DiagnosticDescriptors.cs | 8 + .../gen/RegexAnalyzer.cs | 416 ++++++++++++++++++ .../gen/RegexCodeFixer.cs | 290 ++++++++++++ .../gen/Resources/Strings.resx | 8 +- .../gen/Resources/xlf/Strings.cs.xlf | 10 + .../gen/Resources/xlf/Strings.de.xlf | 10 + .../gen/Resources/xlf/Strings.es.xlf | 10 + .../gen/Resources/xlf/Strings.fr.xlf | 10 + .../gen/Resources/xlf/Strings.it.xlf | 10 + .../gen/Resources/xlf/Strings.ja.xlf | 10 + .../gen/Resources/xlf/Strings.ko.xlf | 10 + .../gen/Resources/xlf/Strings.pl.xlf | 10 + .../gen/Resources/xlf/Strings.pt-BR.xlf | 10 + .../gen/Resources/xlf/Strings.ru.xlf | 10 + .../gen/Resources/xlf/Strings.tr.xlf | 10 + .../gen/Resources/xlf/Strings.zh-Hans.xlf | 10 + .../gen/Resources/xlf/Strings.zh-Hant.xlf | 10 + 17 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs create mode 100644 src/libraries/System.Text.RegularExpressions/gen/RegexCodeFixer.cs diff --git a/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs b/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs index 2c1d2a0d4a881f..40b0d988084508 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs @@ -62,5 +62,13 @@ internal static class DiagnosticDescriptors category: Category, DiagnosticSeverity.Info, isEnabledByDefault: true); + + public static DiagnosticDescriptor UseRegexSourceGeneration { get; } = new DiagnosticDescriptor( + id: RegexAnalyzer.DiagnosticId, + title: new LocalizableResourceString(nameof(SR.UseRegexSourceGeneratorTitle), SR.ResourceManager, typeof(FxResources.System.Text.RegularExpressions.Generator.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.UseRegexSourceGeneratorMessage), SR.ResourceManager, typeof(FxResources.System.Text.RegularExpressions.Generator.SR)), + category: Category, + DiagnosticSeverity.Info, + isEnabledByDefault: true); } } diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs new file mode 100644 index 00000000000000..64c70e3a8249eb --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs @@ -0,0 +1,416 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace System.Text.RegularExpressions.Generator +{ + /// + /// Roslyn analyzer that searches for invocations of the Regex constructors, or the + /// Regex static methods and analyzes if the callsite could be using the Regex Generator instead. + /// If so, it will emit an informational diagnostic to suggest use the Regex Generator. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class RegexAnalyzer : DiagnosticAnalyzer + { + // private members + private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; + private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; + private const string TimeSpanTypeName = "System.TimeSpan"; + private const string TimeoutTypeName = "System.Threading.Timeout"; + + // internal members + internal const string PatternIndexName = "PatternIndex"; + internal const string RegexOptionsIndexName = "RegexOptionsIndex"; + internal const string RegexTimeoutIndexName = "RegexTimeoutIndex"; + internal const string RegexTimeoutName = "RegexTimeout"; + internal const string DiagnosticId = "SYSLIB1046"; + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseRegexSourceGeneration); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + // Register analysis of calls to the Regex constructors + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + + // Register analysis of calls to Regex static methods + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + } + + /// + /// Analyzes an invocation expression to see if the invocation is a call to one of the Regex static methods, + /// and checks if they could be using the source generator instead. + /// + /// The operation context representing the invocation. + private void AnalyzeInvocation(OperationAnalysisContext context) + { + // Ensure source generator is supported. + if (!ProjectSupportsRegexSourceGenerator(context.Operation)) + { + return; + } + + // Ensure the invocation is a Regex static method. + IInvocationOperation invocationOperation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocationOperation.TargetMethod; + if (!method.IsStatic || !SymbolEqualityComparer.Default.Equals(method.ContainingType, context.Compilation.GetTypeByMetadataName(RegexTypeName))) + { + return; + } + + // Depending on the static method being called, we need to save the parameters as properties so that we can save them onto the diagnostic so that the + // code fixer can later use that property bag to generate the code fix and emit the RegexGenerator attribute. + // Most static methods have the same parameter overloads which are covered by the first if block. Replace static method takes extra parameters so that one + // is treated specially. + if (method.Name is "IsMatch" or "Match" or "Matches" or "Split" or "Count" or "EnumerateMatches") + { + for (int i = 1; i < invocationOperation.Arguments.Length; i++) + { + // Ensure that all inputs to the static method are constant. + if (!IsConstant(invocationOperation.Arguments[i])) + { + return; + } + } + + // Create the property bag. + ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair(PatternIndexName, "1"), + new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 2 ? "2" : null), + new KeyValuePair(RegexTimeoutIndexName, invocationOperation.Arguments.Length > 3 ? "3" : null), + new KeyValuePair(RegexTimeoutName, invocationOperation.Arguments.Length > 3 ? CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[3].Value)?.ToString(CultureInfo.InvariantCulture) : null), + }); + + // Report the diagnostic. + SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax.ChildNodes().FirstOrDefault(); + Debug.Assert(syntaxNodeForDiagnostic != null); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); + } + else if (method.Name is "Replace") + { + for (int i = 1; i < invocationOperation.Arguments.Length; i++) + { + // Skip the third parameter as that is the parameter to be used as replacement and doesn't affect the source generator. + if (i == 2) + { + continue; + } + + // Ensure that all inputs to the static method are constant. + if (!IsConstant(invocationOperation.Arguments[i])) + { + return; + } + } + + // Create the property bag. + ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair(PatternIndexName, "1"), + new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 3 ? "3" : null), + new KeyValuePair(RegexTimeoutIndexName, invocationOperation.Arguments.Length > 4 ? "4" : null), + new KeyValuePair(RegexTimeoutName, invocationOperation.Arguments.Length > 4 ? CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[4].Value)?.ToString(CultureInfo.InvariantCulture) : null), + }); + + // Report the diagnostic. + SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax.ChildNodes().FirstOrDefault(); + Debug.Assert(syntaxNodeForDiagnostic is not null); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); + } + } + + /// + /// Analyzes an object creation expression to see if the invocation is a call to one of the Regex constructors, + /// and checks if they could be using the source generator instead. + /// + /// The object creation context. + private void AnalyzeObjectCreation(OperationAnalysisContext context) + { + // Ensure source generator is supported. + if (!ProjectSupportsRegexSourceGenerator(context.Operation)) + { + return; + } + + // Ensure the object creation is a call to the Regex constructor. + IObjectCreationOperation operation = (IObjectCreationOperation)context.Operation; + if (!SymbolEqualityComparer.Default.Equals(operation.Type, context.Compilation.GetTypeByMetadataName(RegexTypeName))) + { + return; + } + + // Ensure that all inputs to the constructor are constant. + foreach (IArgumentOperation argument in operation.Arguments) + { + if (!IsConstant(argument)) + { + return; + } + } + + // Create the property bag. + ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair(PatternIndexName, "0"), + new KeyValuePair(RegexOptionsIndexName, operation.Arguments.Length > 1 ? "1" : null), + new KeyValuePair(RegexTimeoutIndexName, operation.Arguments.Length > 2 ? "2" : null), + new KeyValuePair(RegexTimeoutName, operation.Arguments.Length > 2 ? CalculateMillisecondsFromTimeSpan(operation.Arguments[2].Value)?.ToString(CultureInfo.InvariantCulture) : null), + }); + + // Report the diagnostic. + SyntaxNode? syntaxNodeForDiagnostic = operation.Syntax.ChildNodes().FirstOrDefault()!.Parent; + Debug.Assert(syntaxNodeForDiagnostic is not null); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); + } + + /// + /// Calculates the constant total milliseconds represented by a timespan operation. + /// + /// + /// RegEx constructors and static methods optionally take a parameter to represent when it should timeout to avoid + /// catastrophic backtracking, but the RegexGeneratorAttribute can't take a non-constant parameter so it instead takes an optional int milliseconds. + /// This method is in charge of verifying if that timespan parameter has a constant value, and if it does, it makes the transformation from Timespan + /// to milliseconds. + /// + /// The operation representing the parameter. + /// The total milliseconds in case the TimeSpan value is constant, or if there is no constant value. + private static int? CalculateMillisecondsFromTimeSpan(IOperation operation) + { + return CalculateMillisecondsFromTimeSpan(operation, 1d); + + static int? CalculateMillisecondsFromTimeSpan(IOperation operation, double factor) + { + Compilation compilation = operation.SemanticModel!.Compilation; + + const double TicksToMilliseconds = 1d / TimeSpan.TicksPerMillisecond; + const double SecondsToMilliseconds = 1000; + const double MinutesToMilliseconds = 60 * 1000; + const double HoursToMilliseconds = 60 * 60 * 1000; + const double DaysToMilliseconds = 24 * 60 * 60 * 1000; + + // If there are implicit conversion operations, we unwrap them all. + operation = UnwrapImplicitConversionOperations(operation); + + // If we've reached a ConstantValue, then we just multiply it by the passed in factor and return. + if (operation.ConstantValue.HasValue) + { + if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is long int64Value) + { + return (int)(int64Value * factor); + } + + if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is int int32Value) + { + return (int)(int32Value * factor); + } + + if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is double doubleValue) + { + return (int)(doubleValue * factor); + } + } + + // Case: using the default keyword. + if (operation is IDefaultValueOperation) + { + return 0; + } + + // Cases: TimeSpan.FromTicks, TimeSpan.FromMilliseconds, TimeSpan.FromSeconds, TimeSpan.FromMinutes, TimeSpan.FromHours, TimeSpan.FromDays + if (operation is IInvocationOperation invocationOperation) + { + IMethodSymbol method = invocationOperation.TargetMethod; + if (method.IsStatic && SymbolEqualityComparer.Default.Equals(method.ContainingType, compilation.GetTypeByMetadataName(TimeSpanTypeName))) + { + return method.Name switch + { + "FromTicks" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, TicksToMilliseconds), + "FromMilliseconds" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, 1), + "FromSeconds" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, SecondsToMilliseconds), + "FromMinutes" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, MinutesToMilliseconds), + "FromHours" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, HoursToMilliseconds), + "FromDays" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, DaysToMilliseconds), + _ => null, + }; + } + + return null; + } + + if (operation is IFieldReferenceOperation fieldReferenceOperation) + { + // Cases: TimeSpan.Zero, TimeSpan.MinValue, TimeSpan.MaxValue + ISymbol member = fieldReferenceOperation.Member; + if (member.IsStatic && SymbolEqualityComparer.Default.Equals(member.ContainingType, compilation.GetTypeByMetadataName(TimeSpanTypeName))) + { + return member.Name switch + { + "Zero" => 0, + "MinValue" => (int)TimeSpan.MinValue.TotalMilliseconds, + "MaxValue" => (int)TimeSpan.MaxValue.TotalMilliseconds, + _ => null, + }; + } + + // Cases: Regex.InfiniteMatchTimeout + if (member.IsStatic && SymbolEqualityComparer.Default.Equals(member.ContainingType, compilation.GetTypeByMetadataName(RegexTypeName))) + { + return member.Name switch + { + "InfiniteMatchTimeout" => -1, + _ => null, + }; + } + + // Cases: Timeout.InfiniteTimeSpan, Timeout.Infinite + if (member.IsStatic && SymbolEqualityComparer.Default.Equals(member.ContainingType, compilation.GetTypeByMetadataName(TimeoutTypeName))) + { + return member.Name switch + { + "InfiniteTimeSpan" => -1, + "Infinite" => -1, + _ => null, + }; + } + + return null; + } + + // Cases: Instantiating a new TimeSpan instance via a call to one of the TimeSpan constructors. + if (operation is IObjectCreationOperation objectCreationOperation) + { + if (SymbolEqualityComparer.Default.Equals(objectCreationOperation.Type, compilation.GetTypeByMetadataName(TimeSpanTypeName))) + { + switch (objectCreationOperation.Arguments.Length) + { + case 1: // new TimeSpan(long ticks) + return CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, 1d / TimeSpan.TicksPerMillisecond); + + case 3: // new TimeSpan(int hours, int minutes, int seconds) + return AddValues( + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, HoursToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[1].Value, MinutesToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[2].Value, SecondsToMilliseconds) + ); + + case 4: // new TimeSpan(int days, int hours, int minutes, int seconds) + return AddValues( + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds) + ); + + case 5: // new TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds) + return AddValues( + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds), + CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[4].Value, 1) + ); + } + + return null; + } + } + + return null; + + // Helper method that adds all the passed in Nullable + static int? AddValues(params int?[] values) + { + int result = 0; + foreach (int? value in values) + { + if (!value.HasValue) + { + return null; + } + + result += value.GetValueOrDefault(); + } + + return result; + } + + // Helper method that unwraps all of the implicit operations + static IOperation UnwrapImplicitConversionOperations(IOperation operation) + { + if (operation is IConversionOperation conversionOperation && conversionOperation.IsImplicit) + { + return UnwrapImplicitConversionOperations(conversionOperation.Operand); + } + + return operation; + } + } + } + + /// + /// Ensures that the input to the constructor or invocation is constant at compile time + /// which is a requirement in order to be able to use the source generator. + /// + /// The argument to be analyzed. + /// if the argument is constant; otherwise, . + private static bool IsConstant(IArgumentOperation argument) + { + IOperation valueOperation = argument.Value; + if (valueOperation.ConstantValue.HasValue) + { + return true; + } + + Compilation compilation = argument.SemanticModel!.Compilation; + if (SymbolEqualityComparer.Default.Equals(valueOperation.Type, compilation.GetTypeByMetadataName(TimeSpanTypeName))) + { + return CalculateMillisecondsFromTimeSpan(valueOperation).HasValue; + } + + return false; + } + + /// + /// Ensures that the compilation can find the Regex and RegexAttribute types, and also validates that the + /// LangVersion of the project is >= 10.0 (which is the current requirement for the Regex source generator. + /// + /// The operation to be analyzed. + /// if source generator is supported in the project; otherwise, . + private static bool ProjectSupportsRegexSourceGenerator(IOperation operation) + { + Compilation compilation = operation.SemanticModel!.Compilation; + INamedTypeSymbol? regexSymbol = compilation.GetTypeByMetadataName(RegexTypeName); + if (regexSymbol == null) + { + return false; + } + + INamedTypeSymbol? regexGeneratorAttributeSymbol = compilation.GetTypeByMetadataName(RegexGeneratorTypeName); + if (regexGeneratorAttributeSymbol == null) + { + return false; + } + + if (operation.Syntax.SyntaxTree.Options is CSharpParseOptions options && options.LanguageVersion <= (LanguageVersion)1000) + { + return false; + } + + return true; + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexCodeFixer.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexCodeFixer.cs new file mode 100644 index 00000000000000..74c292ad339ff2 --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexCodeFixer.cs @@ -0,0 +1,290 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; + +namespace System.Text.RegularExpressions.Generator +{ + /// + /// Roslyn code fixer that will listen to SysLIB1046 diagnostics and will provide a code fix which onboards a particular RegEx into + /// source generation. + /// + [ExportCodeFixProvider(LanguageNames.CSharp)] + public class RegexCodeFixer : CodeFixProvider + { + // private members + private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; + private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; + private const string DefaultRegexMethodName = "MyRegex"; + + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RegexAnalyzer.DiagnosticId); + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + // Fetch the node to fix, and register the codefix by invoking the ConvertToSourceGenerator method. + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + SyntaxNode nodeToFix = root.FindNode(context.Span, getInnermostNodeForTie: false); + + if (nodeToFix is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + SR.UseRegexSourceGeneratorTitle, + cancellationToken => ConvertToSourceGenerator(context.Document, context.Diagnostics[0], cancellationToken), + equivalenceKey: SR.UseRegexSourceGeneratorTitle), + context.Diagnostics); + } + + /// + /// Takes a and a and returns a new with the replaced + /// nodes in order to apply the code fix to the diagnostic. + /// + /// The original document. + /// The diagnostic to fix. + /// The cancellation token for the async operation. + /// The new document with the replaced nodes after applying the code fix. + private static async Task ConvertToSourceGenerator(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + // We first get the compilation object from the document + SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel is null) + { + return document; + } + Compilation compilation = semanticModel.Compilation; + + // We then get the symbols for the Regex and RegexGeneratorAttribute types. + INamedTypeSymbol? regexSymbol = compilation.GetTypeByMetadataName(RegexTypeName); + INamedTypeSymbol? regexGeneratorAttributeSymbol = compilation.GetTypeByMetadataName(RegexGeneratorTypeName); + if (regexSymbol is null || regexGeneratorAttributeSymbol is null) + { + return document; + } + + // Find the node that corresponding to the diagnostic which we will then fix. + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + { + return document; + } + + SyntaxNode nodeToFix = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: false); + // Save the operation object from the nodeToFix before it gets replaced by the new method invocation. + // We will later use this operation to get the parameters out and pass them into the RegexGenerator attribute. + IOperation? operation = semanticModel.GetOperation(nodeToFix, cancellationToken); + if (operation is null) + { + return document; + } + + // Get the parent type declaration so that we can inspect its methods as well as check if we need to add the partial keyword. + TypeDeclarationSyntax? typeDeclaration = nodeToFix.Ancestors().OfType().FirstOrDefault(); + + if (typeDeclaration is null) + { + return document; + } + + // Calculate what name should be used for the generated static partial method + string methodName = DefaultRegexMethodName; + ITypeSymbol? typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) as ITypeSymbol; + if (typeSymbol is not null) + { + IEnumerable members = GetAllMembers(typeSymbol); + int memberCount = 1; + while (members.Any(m => m.Name == methodName)) + { + methodName = $"{DefaultRegexMethodName}{memberCount++}"; + } + } + + // Walk the type hirerarchy of the node to fix, and add the partial modifier to each ancestor (if it doesn't have it already) + // We also keep a count of how many partial keywords we added so that we can later find the nodeToFix again on the new root using the text offset. + int typesModified = 0; + root = root.ReplaceNodes( + nodeToFix.Ancestors().OfType(), + (_, typeDeclaration) => + { + if (!typeDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + typesModified++; + return typeDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)).WithAdditionalAnnotations(Simplifier.Annotation); + } + + return typeDeclaration; + }); + + // We find nodeToFix again by calculating the offset of how many partial keywords we had to add. + nodeToFix = root.FindNode(new TextSpan(nodeToFix.Span.Start + (typesModified * "partial".Length), nodeToFix.Span.Length)); + if (nodeToFix is null) + { + return document; + } + + // We need to find the typeDeclaration again, but now using the new root. + typeDeclaration = nodeToFix.Ancestors().OfType().FirstOrDefault(); + Debug.Assert(typeDeclaration is not null); + TypeDeclarationSyntax newTypeDeclaration = typeDeclaration; + + // We generate a new invocation node to call our new partial method, and use it to replace the nodeToFix. + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + SyntaxGenerator generator = editor.Generator; + ImmutableDictionary properties = diagnostic.Properties; + + // Generate the modified type declaration depending on whether the callsite was a Regex constructor call + // or a Regex static method invocation. + if (operation is IInvocationOperation invocationOperation) // When using a Regex static method + { + ImmutableArray arguments = invocationOperation.Arguments; + + // Parse the idices for where to get the arguments from. + int?[] indices = new[] + { + TryParseInt32(properties, RegexAnalyzer.PatternIndexName), + TryParseInt32(properties, RegexAnalyzer.RegexOptionsIndexName), + TryParseInt32(properties, RegexAnalyzer.RegexTimeoutIndexName), + }; + + foreach (int? index in indices.Where(value => value != null).OrderByDescending(value => value)) + { + arguments = arguments.RemoveAt(index.GetValueOrDefault()); + } + + SyntaxNode createRegexMethod = generator.InvocationExpression(generator.IdentifierName(methodName)); + SyntaxNode method = generator.InvocationExpression(generator.MemberAccessExpression(createRegexMethod, invocationOperation.TargetMethod.Name), arguments.Select(arg => arg.Syntax).ToArray()); + + newTypeDeclaration = newTypeDeclaration.ReplaceNode(nodeToFix, method); + } + else // When using a Regex constructor + { + SyntaxNode invokeMethod = generator.InvocationExpression(generator.IdentifierName(methodName)); + newTypeDeclaration = newTypeDeclaration.ReplaceNode(nodeToFix, invokeMethod); + } + + // Initialize the inputs for the RegexGenerator attribute. + SyntaxNode? patternValue = null; + SyntaxNode? regexOptionsValue = null; + SyntaxNode? timeoutValue = null; + + // Try to get the timeout from the diagnostic's property bag. It will only be present if the original callsite used a timeout. + int? timeout = TryParseInt32(properties, RegexAnalyzer.RegexTimeoutName); + if (timeout is not null) + { + timeoutValue = generator.LiteralExpression(timeout.Value); + } + + // Try to get the pattern and RegexOptions values out from the diagnostic's property bag. + if (operation is IObjectCreationOperation objectCreationOperation) // When using the Regex constructors + { + patternValue = GetNode((objectCreationOperation).Arguments, properties, RegexAnalyzer.PatternIndexName); + regexOptionsValue = GetNode((objectCreationOperation).Arguments, properties, RegexAnalyzer.RegexOptionsIndexName); + } + else if (operation is IInvocationOperation invocation) // When using the Regex static methods. + { + patternValue = GetNode(invocation.Arguments, properties, RegexAnalyzer.PatternIndexName); + regexOptionsValue = GetNode(invocation.Arguments, properties, RegexAnalyzer.RegexOptionsIndexName); + } + + // If the property bag had a timeout but not RegexOptions value, then we default to RegexOptions.None + if (timeoutValue is not null && regexOptionsValue is null) + { + regexOptionsValue = generator.MemberAccessExpression(generator.TypeExpression(compilation.GetTypeByMetadataName("System.Text.RegularExpressions.RegexOptions")!), "None"); + } + + // Generate the new static partial method + MethodDeclarationSyntax newMethod = (MethodDeclarationSyntax)generator.MethodDeclaration( + name: methodName, + returnType: generator.TypeExpression(regexSymbol), + modifiers: DeclarationModifiers.Static | DeclarationModifiers.Partial, + accessibility: Accessibility.Private); + + // Allow user to pick a different name for the method. + newMethod = newMethod.ReplaceToken(newMethod.Identifier, SyntaxFactory.Identifier(methodName).WithAdditionalAnnotations(RenameAnnotation.Create())); + + // Generate the RegexGenerator attribute syntax node with the specified parameters. + SyntaxNode attributes = generator.Attribute(generator.TypeExpression(regexGeneratorAttributeSymbol), attributeArguments: (patternValue, regexOptionsValue, timeoutValue) switch + { + ({ }, null, null) => new[] { patternValue }, + ({ }, { }, null) => new[] { patternValue, regexOptionsValue }, + ({ }, { }, { }) => new[] { patternValue, regexOptionsValue, SyntaxFactory.AttributeArgument((ExpressionSyntax)timeoutValue).WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName("matchTimeoutMilliseconds"))) }, + _ => Array.Empty(), + }); + + // Add the attribute to the generated method. + newMethod = (MethodDeclarationSyntax)generator.AddAttributes(newMethod, attributes); + + // Add the method to the type. + newTypeDeclaration = newTypeDeclaration.AddMembers(newMethod); + + // Replace the old type declaration with the new modified one, and return the document. + return document.WithSyntaxRoot(root.ReplaceNode(typeDeclaration, newTypeDeclaration)); + + static IEnumerable GetAllMembers(ITypeSymbol? symbol) + { + while (symbol != null) + { + foreach (ISymbol member in symbol.GetMembers()) + { + yield return member; + } + + symbol = symbol.BaseType; + } + } + + // Helper method that searches the passed in property bag for the property with the passed in name, and if found, it converts the + // value to an int. + static int? TryParseInt32(ImmutableDictionary properties, string name) + { + if (!properties.TryGetValue(name, out string? value)) + { + return null; + } + + if (!int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out int result)) + { + return null; + } + + return result; + } + + // Helper method that looks int the properties bag for the index of the passed in propertyname, and then returns that index from the args parameter. + static SyntaxNode? GetNode(ImmutableArray args, ImmutableDictionary properties, string propertyName) + { + int? index = TryParseInt32(properties, propertyName); + if (index == null) + { + return null; + } + + return args[index.Value].Value.Syntax; + } + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx b/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx index 4fd64a89335f92..3873e2a46058dc 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx @@ -284,4 +284,10 @@ Regex replacements with substitutions of groups are not supported with RegexOptions.NonBacktracking. {Locked="RegexOptions.NonBacktracking"} - + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf index b65a3d3233ecf8..de57e7efdba56d 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf @@ -272,6 +272,16 @@ Neukončený komentář (?#...). + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf index 058937f0c2c96c..77099eb220f1b1 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf @@ -272,6 +272,16 @@ Nicht abgeschlossener (?#...)-Kommentar. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf index f2a89061af3e05..5c4c0076c96b25 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf @@ -272,6 +272,16 @@ Comentario (?#...) sin terminar. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf index ece8855c96ddc9..9b88d20c42a2f1 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf @@ -272,6 +272,16 @@ Commentaire (?#...) inachevé. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf index f0f23759feafe8..5a8d1eb8e2f8e4 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf @@ -272,6 +272,16 @@ Commento (?#...) non terminato. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf index 39f4ee14831420..33b3699a852b87 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf @@ -272,6 +272,16 @@ 未終了の (?#...) コメントです。 + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf index b096ce1d4762d7..a5e1b4b1b5b459 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf @@ -272,6 +272,16 @@ 종결되지 않은 (?#...) 주석입니다. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf index c5e3f71bf92b9b..629b21363bc31f 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf @@ -272,6 +272,16 @@ Niezakończony komentarz (?#...). + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf index 3527b61b791e09..123e2eb656ee79 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf @@ -272,6 +272,16 @@ Comentário (?#...) não finalizado. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf index 0e31de23787c54..e7555c4f0650f7 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf @@ -272,6 +272,16 @@ Комментарий (?#...) без признака завершения. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf index 180d2bfa015a59..47cabe064112c0 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf @@ -272,6 +272,16 @@ Sonlandırılmayan (?#...) yorumu. + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf index 9b035a7e6c2216..96e92ee4ba5233 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -272,6 +272,16 @@ 未终止的(?#...)注释。 + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf index de77fca34ad29c..e9ed03d494c208 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -272,6 +272,16 @@ 未結束的 (?#...) 註解。 + + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + + + + Use the RegEx source generator. + Use the RegEx source generator. + + \ No newline at end of file From 4458f6b0a4a8d8d674604770128ec6891e9566a7 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 19 May 2022 17:09:11 -0700 Subject: [PATCH 2/6] Adding some tests to the analyzer and fixer --- .../gen/DiagnosticDescriptors.cs | 2 +- .../gen/RegexAnalyzer.cs | 416 ----------- .../gen/Resources/Strings.resx | 6 +- .../gen/Resources/xlf/Strings.cs.xlf | 12 +- .../gen/Resources/xlf/Strings.de.xlf | 12 +- .../gen/Resources/xlf/Strings.es.xlf | 12 +- .../gen/Resources/xlf/Strings.fr.xlf | 12 +- .../gen/Resources/xlf/Strings.it.xlf | 12 +- .../gen/Resources/xlf/Strings.ja.xlf | 12 +- .../gen/Resources/xlf/Strings.ko.xlf | 12 +- .../gen/Resources/xlf/Strings.pl.xlf | 12 +- .../gen/Resources/xlf/Strings.pt-BR.xlf | 12 +- .../gen/Resources/xlf/Strings.ru.xlf | 12 +- .../gen/Resources/xlf/Strings.tr.xlf | 12 +- .../gen/Resources/xlf/Strings.zh-Hans.xlf | 12 +- .../gen/Resources/xlf/Strings.zh-Hant.xlf | 12 +- .../gen/UpgradeToRegexGeneratorAnalyzer.cs | 230 ++++++ ...cs => UpgradeToRegexGeneratorCodeFixer.cs} | 77 +- .../src/Resources/Strings.resx | 5 +- .../UnitTests/CSharpCodeFixVerifier`2.cs | 128 ++++ .../tests/UnitTests/Stubs.cs | 15 + ....Text.RegularExpressions.Unit.Tests.csproj | 10 + .../UpgradeToRegexGeneratorAnalyzerTests.cs | 699 ++++++++++++++++++ 23 files changed, 1214 insertions(+), 530 deletions(-) delete mode 100644 src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs create mode 100644 src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs rename src/libraries/System.Text.RegularExpressions/gen/{RegexCodeFixer.cs => UpgradeToRegexGeneratorCodeFixer.cs} (81%) create mode 100644 src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs create mode 100644 src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs diff --git a/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs b/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs index 40b0d988084508..5c59055e11ecb4 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/DiagnosticDescriptors.cs @@ -64,7 +64,7 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true); public static DiagnosticDescriptor UseRegexSourceGeneration { get; } = new DiagnosticDescriptor( - id: RegexAnalyzer.DiagnosticId, + id: "SYSLIB1046", title: new LocalizableResourceString(nameof(SR.UseRegexSourceGeneratorTitle), SR.ResourceManager, typeof(FxResources.System.Text.RegularExpressions.Generator.SR)), messageFormat: new LocalizableResourceString(nameof(SR.UseRegexSourceGeneratorMessage), SR.ResourceManager, typeof(FxResources.System.Text.RegularExpressions.Generator.SR)), category: Category, diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs deleted file mode 100644 index 64c70e3a8249eb..00000000000000 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexAnalyzer.cs +++ /dev/null @@ -1,416 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace System.Text.RegularExpressions.Generator -{ - /// - /// Roslyn analyzer that searches for invocations of the Regex constructors, or the - /// Regex static methods and analyzes if the callsite could be using the Regex Generator instead. - /// If so, it will emit an informational diagnostic to suggest use the Regex Generator. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class RegexAnalyzer : DiagnosticAnalyzer - { - // private members - private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; - private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; - private const string TimeSpanTypeName = "System.TimeSpan"; - private const string TimeoutTypeName = "System.Threading.Timeout"; - - // internal members - internal const string PatternIndexName = "PatternIndex"; - internal const string RegexOptionsIndexName = "RegexOptionsIndex"; - internal const string RegexTimeoutIndexName = "RegexTimeoutIndex"; - internal const string RegexTimeoutName = "RegexTimeout"; - internal const string DiagnosticId = "SYSLIB1046"; - - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseRegexSourceGeneration); - - /// - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - // Register analysis of calls to the Regex constructors - context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); - - // Register analysis of calls to Regex static methods - context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); - } - - /// - /// Analyzes an invocation expression to see if the invocation is a call to one of the Regex static methods, - /// and checks if they could be using the source generator instead. - /// - /// The operation context representing the invocation. - private void AnalyzeInvocation(OperationAnalysisContext context) - { - // Ensure source generator is supported. - if (!ProjectSupportsRegexSourceGenerator(context.Operation)) - { - return; - } - - // Ensure the invocation is a Regex static method. - IInvocationOperation invocationOperation = (IInvocationOperation)context.Operation; - IMethodSymbol method = invocationOperation.TargetMethod; - if (!method.IsStatic || !SymbolEqualityComparer.Default.Equals(method.ContainingType, context.Compilation.GetTypeByMetadataName(RegexTypeName))) - { - return; - } - - // Depending on the static method being called, we need to save the parameters as properties so that we can save them onto the diagnostic so that the - // code fixer can later use that property bag to generate the code fix and emit the RegexGenerator attribute. - // Most static methods have the same parameter overloads which are covered by the first if block. Replace static method takes extra parameters so that one - // is treated specially. - if (method.Name is "IsMatch" or "Match" or "Matches" or "Split" or "Count" or "EnumerateMatches") - { - for (int i = 1; i < invocationOperation.Arguments.Length; i++) - { - // Ensure that all inputs to the static method are constant. - if (!IsConstant(invocationOperation.Arguments[i])) - { - return; - } - } - - // Create the property bag. - ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] - { - new KeyValuePair(PatternIndexName, "1"), - new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 2 ? "2" : null), - new KeyValuePair(RegexTimeoutIndexName, invocationOperation.Arguments.Length > 3 ? "3" : null), - new KeyValuePair(RegexTimeoutName, invocationOperation.Arguments.Length > 3 ? CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[3].Value)?.ToString(CultureInfo.InvariantCulture) : null), - }); - - // Report the diagnostic. - SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax.ChildNodes().FirstOrDefault(); - Debug.Assert(syntaxNodeForDiagnostic != null); - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); - } - else if (method.Name is "Replace") - { - for (int i = 1; i < invocationOperation.Arguments.Length; i++) - { - // Skip the third parameter as that is the parameter to be used as replacement and doesn't affect the source generator. - if (i == 2) - { - continue; - } - - // Ensure that all inputs to the static method are constant. - if (!IsConstant(invocationOperation.Arguments[i])) - { - return; - } - } - - // Create the property bag. - ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] - { - new KeyValuePair(PatternIndexName, "1"), - new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 3 ? "3" : null), - new KeyValuePair(RegexTimeoutIndexName, invocationOperation.Arguments.Length > 4 ? "4" : null), - new KeyValuePair(RegexTimeoutName, invocationOperation.Arguments.Length > 4 ? CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[4].Value)?.ToString(CultureInfo.InvariantCulture) : null), - }); - - // Report the diagnostic. - SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax.ChildNodes().FirstOrDefault(); - Debug.Assert(syntaxNodeForDiagnostic is not null); - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); - } - } - - /// - /// Analyzes an object creation expression to see if the invocation is a call to one of the Regex constructors, - /// and checks if they could be using the source generator instead. - /// - /// The object creation context. - private void AnalyzeObjectCreation(OperationAnalysisContext context) - { - // Ensure source generator is supported. - if (!ProjectSupportsRegexSourceGenerator(context.Operation)) - { - return; - } - - // Ensure the object creation is a call to the Regex constructor. - IObjectCreationOperation operation = (IObjectCreationOperation)context.Operation; - if (!SymbolEqualityComparer.Default.Equals(operation.Type, context.Compilation.GetTypeByMetadataName(RegexTypeName))) - { - return; - } - - // Ensure that all inputs to the constructor are constant. - foreach (IArgumentOperation argument in operation.Arguments) - { - if (!IsConstant(argument)) - { - return; - } - } - - // Create the property bag. - ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] - { - new KeyValuePair(PatternIndexName, "0"), - new KeyValuePair(RegexOptionsIndexName, operation.Arguments.Length > 1 ? "1" : null), - new KeyValuePair(RegexTimeoutIndexName, operation.Arguments.Length > 2 ? "2" : null), - new KeyValuePair(RegexTimeoutName, operation.Arguments.Length > 2 ? CalculateMillisecondsFromTimeSpan(operation.Arguments[2].Value)?.ToString(CultureInfo.InvariantCulture) : null), - }); - - // Report the diagnostic. - SyntaxNode? syntaxNodeForDiagnostic = operation.Syntax.ChildNodes().FirstOrDefault()!.Parent; - Debug.Assert(syntaxNodeForDiagnostic is not null); - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); - } - - /// - /// Calculates the constant total milliseconds represented by a timespan operation. - /// - /// - /// RegEx constructors and static methods optionally take a parameter to represent when it should timeout to avoid - /// catastrophic backtracking, but the RegexGeneratorAttribute can't take a non-constant parameter so it instead takes an optional int milliseconds. - /// This method is in charge of verifying if that timespan parameter has a constant value, and if it does, it makes the transformation from Timespan - /// to milliseconds. - /// - /// The operation representing the parameter. - /// The total milliseconds in case the TimeSpan value is constant, or if there is no constant value. - private static int? CalculateMillisecondsFromTimeSpan(IOperation operation) - { - return CalculateMillisecondsFromTimeSpan(operation, 1d); - - static int? CalculateMillisecondsFromTimeSpan(IOperation operation, double factor) - { - Compilation compilation = operation.SemanticModel!.Compilation; - - const double TicksToMilliseconds = 1d / TimeSpan.TicksPerMillisecond; - const double SecondsToMilliseconds = 1000; - const double MinutesToMilliseconds = 60 * 1000; - const double HoursToMilliseconds = 60 * 60 * 1000; - const double DaysToMilliseconds = 24 * 60 * 60 * 1000; - - // If there are implicit conversion operations, we unwrap them all. - operation = UnwrapImplicitConversionOperations(operation); - - // If we've reached a ConstantValue, then we just multiply it by the passed in factor and return. - if (operation.ConstantValue.HasValue) - { - if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is long int64Value) - { - return (int)(int64Value * factor); - } - - if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is int int32Value) - { - return (int)(int32Value * factor); - } - - if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is double doubleValue) - { - return (int)(doubleValue * factor); - } - } - - // Case: using the default keyword. - if (operation is IDefaultValueOperation) - { - return 0; - } - - // Cases: TimeSpan.FromTicks, TimeSpan.FromMilliseconds, TimeSpan.FromSeconds, TimeSpan.FromMinutes, TimeSpan.FromHours, TimeSpan.FromDays - if (operation is IInvocationOperation invocationOperation) - { - IMethodSymbol method = invocationOperation.TargetMethod; - if (method.IsStatic && SymbolEqualityComparer.Default.Equals(method.ContainingType, compilation.GetTypeByMetadataName(TimeSpanTypeName))) - { - return method.Name switch - { - "FromTicks" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, TicksToMilliseconds), - "FromMilliseconds" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, 1), - "FromSeconds" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, SecondsToMilliseconds), - "FromMinutes" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, MinutesToMilliseconds), - "FromHours" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, HoursToMilliseconds), - "FromDays" => CalculateMillisecondsFromTimeSpan(invocationOperation.Arguments[0].Value, DaysToMilliseconds), - _ => null, - }; - } - - return null; - } - - if (operation is IFieldReferenceOperation fieldReferenceOperation) - { - // Cases: TimeSpan.Zero, TimeSpan.MinValue, TimeSpan.MaxValue - ISymbol member = fieldReferenceOperation.Member; - if (member.IsStatic && SymbolEqualityComparer.Default.Equals(member.ContainingType, compilation.GetTypeByMetadataName(TimeSpanTypeName))) - { - return member.Name switch - { - "Zero" => 0, - "MinValue" => (int)TimeSpan.MinValue.TotalMilliseconds, - "MaxValue" => (int)TimeSpan.MaxValue.TotalMilliseconds, - _ => null, - }; - } - - // Cases: Regex.InfiniteMatchTimeout - if (member.IsStatic && SymbolEqualityComparer.Default.Equals(member.ContainingType, compilation.GetTypeByMetadataName(RegexTypeName))) - { - return member.Name switch - { - "InfiniteMatchTimeout" => -1, - _ => null, - }; - } - - // Cases: Timeout.InfiniteTimeSpan, Timeout.Infinite - if (member.IsStatic && SymbolEqualityComparer.Default.Equals(member.ContainingType, compilation.GetTypeByMetadataName(TimeoutTypeName))) - { - return member.Name switch - { - "InfiniteTimeSpan" => -1, - "Infinite" => -1, - _ => null, - }; - } - - return null; - } - - // Cases: Instantiating a new TimeSpan instance via a call to one of the TimeSpan constructors. - if (operation is IObjectCreationOperation objectCreationOperation) - { - if (SymbolEqualityComparer.Default.Equals(objectCreationOperation.Type, compilation.GetTypeByMetadataName(TimeSpanTypeName))) - { - switch (objectCreationOperation.Arguments.Length) - { - case 1: // new TimeSpan(long ticks) - return CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, 1d / TimeSpan.TicksPerMillisecond); - - case 3: // new TimeSpan(int hours, int minutes, int seconds) - return AddValues( - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, HoursToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[1].Value, MinutesToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[2].Value, SecondsToMilliseconds) - ); - - case 4: // new TimeSpan(int days, int hours, int minutes, int seconds) - return AddValues( - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds) - ); - - case 5: // new TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds) - return AddValues( - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[0].Value, DaysToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[1].Value, HoursToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[2].Value, MinutesToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[3].Value, SecondsToMilliseconds), - CalculateMillisecondsFromTimeSpan(objectCreationOperation.Arguments[4].Value, 1) - ); - } - - return null; - } - } - - return null; - - // Helper method that adds all the passed in Nullable - static int? AddValues(params int?[] values) - { - int result = 0; - foreach (int? value in values) - { - if (!value.HasValue) - { - return null; - } - - result += value.GetValueOrDefault(); - } - - return result; - } - - // Helper method that unwraps all of the implicit operations - static IOperation UnwrapImplicitConversionOperations(IOperation operation) - { - if (operation is IConversionOperation conversionOperation && conversionOperation.IsImplicit) - { - return UnwrapImplicitConversionOperations(conversionOperation.Operand); - } - - return operation; - } - } - } - - /// - /// Ensures that the input to the constructor or invocation is constant at compile time - /// which is a requirement in order to be able to use the source generator. - /// - /// The argument to be analyzed. - /// if the argument is constant; otherwise, . - private static bool IsConstant(IArgumentOperation argument) - { - IOperation valueOperation = argument.Value; - if (valueOperation.ConstantValue.HasValue) - { - return true; - } - - Compilation compilation = argument.SemanticModel!.Compilation; - if (SymbolEqualityComparer.Default.Equals(valueOperation.Type, compilation.GetTypeByMetadataName(TimeSpanTypeName))) - { - return CalculateMillisecondsFromTimeSpan(valueOperation).HasValue; - } - - return false; - } - - /// - /// Ensures that the compilation can find the Regex and RegexAttribute types, and also validates that the - /// LangVersion of the project is >= 10.0 (which is the current requirement for the Regex source generator. - /// - /// The operation to be analyzed. - /// if source generator is supported in the project; otherwise, . - private static bool ProjectSupportsRegexSourceGenerator(IOperation operation) - { - Compilation compilation = operation.SemanticModel!.Compilation; - INamedTypeSymbol? regexSymbol = compilation.GetTypeByMetadataName(RegexTypeName); - if (regexSymbol == null) - { - return false; - } - - INamedTypeSymbol? regexGeneratorAttributeSymbol = compilation.GetTypeByMetadataName(RegexGeneratorTypeName); - if (regexGeneratorAttributeSymbol == null) - { - return false; - } - - if (operation.Syntax.SyntaxTree.Options is CSharpParseOptions options && options.LanguageVersion <= (LanguageVersion)1000) - { - return false; - } - - return true; - } - } -} diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx b/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx index 3873e2a46058dc..316c58e8850e50 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx @@ -233,7 +233,7 @@ Quantifier '{0}' following nothing. - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. Replacement pattern error. @@ -285,9 +285,9 @@ {Locked="RegexOptions.NonBacktracking"} - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. + Convert to 'RegexGenerator'. \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf index de57e7efdba56d..624b540b957c4d 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Vypršel časový limit modulu RegEx při pokusu o porovnání vzoru se vstupním řetězcem. K tomu může dojít z celé řady důvodů, mezi které patří velká velikost vstupních dat nebo nadměrné zpětné navracení způsobené vloženými kvantifikátory, zpětnými odkazy a dalšími faktory. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Vypršel časový limit modulu RegEx při pokusu o porovnání vzoru se vstupním řetězcem. K tomu může dojít z celé řady důvodů, mezi které patří velká velikost vstupních dat nebo nadměrné zpětné navracení způsobené vloženými kvantifikátory, zpětnými odkazy a dalšími faktory. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf index 77099eb220f1b1..50b76c0280c4eb 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Zeitüberschreitung des RegEx-Moduls beim Versuch, ein Muster mit einer Eingabezeichenfolge in Übereinstimmung zu bringen. Dies kann viele Ursachen haben, darunter sehr große Eingaben oder übermäßige Rückverfolgung aufgrund von geschachtelten Quantifizierern, Rückverweisen und anderen Faktoren. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Zeitüberschreitung des RegEx-Moduls beim Versuch, ein Muster mit einer Eingabezeichenfolge in Übereinstimmung zu bringen. Dies kann viele Ursachen haben, darunter sehr große Eingaben oder übermäßige Rückverfolgung aufgrund von geschachtelten Quantifizierern, Rückverweisen und anderen Faktoren. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf index 5c4c0076c96b25..7cc5c76dd03409 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Se agotó el tiempo de espera mientras el motor de RegEx intentaba comparar una cadena de entrada con un patrón. Esto puede deberse a muchos motivos, como la especificación de cadenas de entrada muy grandes o búsquedas hacia atrás excesivas causadas por cuantificadores anidados, referencias inversas y otros factores. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Se agotó el tiempo de espera mientras el motor de RegEx intentaba comparar una cadena de entrada con un patrón. Esto puede deberse a muchos motivos, como la especificación de cadenas de entrada muy grandes o búsquedas hacia atrás excesivas causadas por cuantificadores anidados, referencias inversas y otros factores. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf index 9b88d20c42a2f1..8d859dda8388c8 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Le délai d'attente du moteur RegEx a expiré pendant la tentative d'appariement d'un modèle avec une chaîne d'entrée. Ce problème peut se produire pour de nombreuses raisons, notamment en cas d'entrées volumineuses ou de retour sur trace excessif causé par les quantificateurs imbriqués, les références arrière et d'autres facteurs. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Le délai d'attente du moteur RegEx a expiré pendant la tentative d'appariement d'un modèle avec une chaîne d'entrée. Ce problème peut se produire pour de nombreuses raisons, notamment en cas d'entrées volumineuses ou de retour sur trace excessif causé par les quantificateurs imbriqués, les références arrière et d'autres facteurs. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf index 5a8d1eb8e2f8e4..0a7f3009f48fdb 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Timeout del motore RegEx durante il tentativo di corrispondenza di un criterio a una stringa di input. Il timeout si può verificare per molti motivi, compresi gli input molto grandi o un eccessivo backtracking causato dai quantificatori annidati, backreference e altri fattori. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Timeout del motore RegEx durante il tentativo di corrispondenza di un criterio a una stringa di input. Il timeout si può verificare per molti motivi, compresi gli input molto grandi o un eccessivo backtracking causato dai quantificatori annidati, backreference e altri fattori. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf index 33b3699a852b87..e4fd6d9e69a8eb 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - パターンと入力文字列との照合中に、RegEx エンジンがタイムアウトしました。これは、非常に大きな入力、入れ子になった量指定子によって生じた過剰なバックトラッキング、前方参照などの要因を含む、さまざまな原因によって発生する可能性があります。 + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + パターンと入力文字列との照合中に、RegEx エンジンがタイムアウトしました。これは、非常に大きな入力、入れ子になった量指定子によって生じた過剰なバックトラッキング、前方参照などの要因を含む、さまざまな原因によって発生する可能性があります。 @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf index a5e1b4b1b5b459..aa04d6190b0e18 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - RegEx 엔진이 패턴을 입력 문자열에 일치시키는 동안 시간 초과되었습니다. 이 오류는 많은 입력, 중첩 수량자로 인한 과도한 역추적, 역참조, 기타 요인 등의 다양한 이유로 인해 발생할 수 있습니다. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + RegEx 엔진이 패턴을 입력 문자열에 일치시키는 동안 시간 초과되었습니다. 이 오류는 많은 입력, 중첩 수량자로 인한 과도한 역추적, 역참조, 기타 요인 등의 다양한 이유로 인해 발생할 수 있습니다. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf index 629b21363bc31f..237d66466b87b9 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Upłynął limit czasu podczas próby dopasowania przez aparat wyrażeń regularnych wzorca do ciągu wejściowego. Mogło to być spowodowane wieloma przyczynami, w tym bardzo dużą ilością danych wejściowych, nadmiernym wykorzystaniem algorytmu wycofywania związanym z kwantyfikatorami zagnieżdżonymi, odwołaniami wstecznymi i innymi czynnikami. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Upłynął limit czasu podczas próby dopasowania przez aparat wyrażeń regularnych wzorca do ciągu wejściowego. Mogło to być spowodowane wieloma przyczynami, w tym bardzo dużą ilością danych wejściowych, nadmiernym wykorzystaniem algorytmu wycofywania związanym z kwantyfikatorami zagnieżdżonymi, odwołaniami wstecznymi i innymi czynnikami. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf index 123e2eb656ee79..d0d2e50388315e 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - O mecanismo de RegEx esgotou o tempo limite ao tentar combinar um padrão com uma cadeia de caracteres de entrada. Isso pode ocorrer por vários motivos, incluindo entradas muito grandes ou acompanhamento inverso excessivo causado por quantificadores aninhados, referências inversas e outros fatores. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + O mecanismo de RegEx esgotou o tempo limite ao tentar combinar um padrão com uma cadeia de caracteres de entrada. Isso pode ocorrer por vários motivos, incluindo entradas muito grandes ou acompanhamento inverso excessivo causado por quantificadores aninhados, referências inversas e outros fatores. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf index e7555c4f0650f7..5495d38b6eb012 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Истекло время ожидания модуля RegEx при попытке сравнить шаблон со входной строкой. Это могло произойти по многим причинам, в том числе из-за очень большого объема входных данных или излишнего обратного отслеживания, вызванного вложенными квантификаторами, обратными ссылками и прочими факторами. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Истекло время ожидания модуля RegEx при попытке сравнить шаблон со входной строкой. Это могло произойти по многим причинам, в том числе из-за очень большого объема входных данных или излишнего обратного отслеживания, вызванного вложенными квантификаторами, обратными ссылками и прочими факторами. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf index 47cabe064112c0..3853a8375a07d0 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - RegEx altyapısı bir deseni bir giriş dizesiyle eşleştirmeye çalışırken zaman aşımına uğradı. Bu birçok nedenle oluşabilir; buna çok büyük girişler ya da iç içe geçmiş miktar belirleyiciler, geri başvurular ve diğer faktörler nedeniyle oluşan aşırı geri dönüşler dahildir. + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + RegEx altyapısı bir deseni bir giriş dizesiyle eşleştirmeye çalışırken zaman aşımına uğradı. Bu birçok nedenle oluşabilir; buna çok büyük girişler ya da iç içe geçmiş miktar belirleyiciler, geri başvurular ve diğer faktörler nedeniyle oluşan aşırı geri dönüşler dahildir. @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf index 96e92ee4ba5233..0ae9d3f93eeaf3 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - 尝试将模式与输入字符串匹配时,RegEx 引擎超时。许多原因均可能导致出现这种情况,包括由嵌套限定符、反向引用和其他因素引起的大量输入或过度回溯。 + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + 尝试将模式与输入字符串匹配时,RegEx 引擎超时。许多原因均可能导致出现这种情况,包括由嵌套限定符、反向引用和其他因素引起的大量输入或过度回溯。 @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf index e9ed03d494c208..df103dd22f2886 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - 嘗試將模式對應至輸入字串時,RegEx 引擎發生逾時。有很多原因會導致這個情形,其中包括巢狀數量詞、反向參考及其他因素所造成的極大量輸入或過度使用回溯法。 + The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + 嘗試將模式對應至輸入字串時,RegEx 引擎發生逾時。有很多原因會導致這個情形,其中包括巢狀數量詞、反向參考及其他因素所造成的極大量輸入或過度使用回溯法。 @@ -273,13 +273,13 @@ - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. - The inputs to your RegEx are known at compile-time so you could be using the source generator to boost performance. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. + Use 'RegexGeneratorAttribute' to generate the regular expression implementation at compile-time. - Use the RegEx source generator. - Use the RegEx source generator. + Convert to 'RegexGenerator'. + Convert to 'RegexGenerator'. diff --git a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs new file mode 100644 index 00000000000000..8fd757cc84e637 --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs @@ -0,0 +1,230 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace System.Text.RegularExpressions.Generator +{ + /// + /// Roslyn analyzer that searches for invocations of the Regex constructors, or the + /// Regex static methods and analyzes if the callsite could be using the Regex Generator instead. + /// If so, it will emit an informational diagnostic to suggest use the Regex Generator. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class UpgradeToRegexGeneratorAnalyzer : DiagnosticAnalyzer + { + private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; + private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; + + internal const string PatternIndexName = "PatternIndex"; + internal const string RegexOptionsIndexName = "RegexOptionsIndex"; + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseRegexSourceGeneration); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationContext => + { + Compilation compilation = compilationContext.Compilation; + + if (!ProjectSupportsRegexSourceGenerator(compilation, out INamedTypeSymbol? regexTypeSymbol)) + { + return; + } + + // Register analysis of calls to the Regex constructors + compilationContext.RegisterOperationAction(context => AnalyzeObjectCreation(context, regexTypeSymbol), OperationKind.ObjectCreation); + + // Register analysis of calls to Regex static methods + compilationContext.RegisterOperationAction(context => AnalyzeInvocation(context, regexTypeSymbol), OperationKind.Invocation); + }); + } + + /// + /// Analyzes an invocation expression to see if the invocation is a call to one of the Regex static methods, + /// and checks if they could be using the source generator instead. + /// + /// The compilation context representing the invocation. + private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol) + { + // Ensure the invocation is a Regex static method. + IInvocationOperation invocationOperation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocationOperation.TargetMethod; + if (!method.IsStatic || !SymbolEqualityComparer.Default.Equals(method.ContainingType, regexTypeSymbol)) + { + return; + } + + // Depending on the static method being called, we need to save the parameters as properties so that we can save them onto the diagnostic so that the + // code fixer can later use that property bag to generate the code fix and emit the RegexGenerator attribute. + // Most static methods have the same parameter overloads which are covered by the first if block. Replace static method takes extra parameters so that one + // is treated specially. + if (method.Name is "IsMatch" or "Match" or "Matches" or "Split" or "Count" or "EnumerateMatches") + { + // if the static method invocation has a timeout, then don't emit a diagnostic. + if (invocationOperation.Arguments.Length > 3) + { + return; + } + + for (int i = 1; i < invocationOperation.Arguments.Length; i++) + { + // Ensure that all inputs to the static method are constant. + if (!IsConstant(invocationOperation.Arguments[i])) + { + return; + } + } + + // Create the property bag. + ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair(PatternIndexName, "1"), + new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 2 ? "2" : null) + }); + + // Report the diagnostic. + SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax; + Debug.Assert(syntaxNodeForDiagnostic != null); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); + } + else if (method.Name is "Replace") + { + // if the static method invocation has a timeout, then don't emit a diagnostic. + if (invocationOperation.Arguments.Length > 4) + { + return; + } + + for (int i = 1; i < invocationOperation.Arguments.Length; i++) + { + // Skip the third parameter as that is the parameter to be used as replacement and doesn't affect the source generator. + if (i == 2) + { + continue; + } + + // Ensure that all inputs to the static method are constant. + if (!IsConstant(invocationOperation.Arguments[i])) + { + return; + } + } + + // Create the property bag. + ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair(PatternIndexName, "1"), + new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 3 ? "3" : null) + }); + + // Report the diagnostic. + SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax; + Debug.Assert(syntaxNodeForDiagnostic is not null); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); + } + } + + /// + /// Analyzes an object creation expression to see if the invocation is a call to one of the Regex constructors, + /// and checks if they could be using the source generator instead. + /// + /// The object creation context. + private static void AnalyzeObjectCreation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol) + { + // Ensure the object creation is a call to the Regex constructor. + IObjectCreationOperation operation = (IObjectCreationOperation)context.Operation; + if (!SymbolEqualityComparer.Default.Equals(operation.Type, regexTypeSymbol)) + { + return; + } + + // If the constructor also has a timeout argument, then don't emit a diagnostic. + if (operation.Arguments.Length > 2) + { + return; + } + + // Ensure that all inputs to the constructor are constant. + foreach (IArgumentOperation argument in operation.Arguments) + { + if (!IsConstant(argument)) + { + return; + } + } + + // Create the property bag. + ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair(PatternIndexName, "0"), + new KeyValuePair(RegexOptionsIndexName, operation.Arguments.Length > 1 ? "1" : null) + }); + + // Report the diagnostic. + SyntaxNode? syntaxNodeForDiagnostic = operation.Syntax; + Debug.Assert(syntaxNodeForDiagnostic is not null); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); + } + + /// + /// Ensures that the input to the constructor or invocation is constant at compile time + /// which is a requirement in order to be able to use the source generator. + /// + /// The argument to be analyzed. + /// if the argument is constant; otherwise, . + private static bool IsConstant(IArgumentOperation argument) + { + IOperation valueOperation = argument.Value; + if (valueOperation.ConstantValue.HasValue) + { + return true; + } + + return false; + } + + /// + /// Ensures that the compilation can find the Regex and RegexAttribute types, and also validates that the + /// LangVersion of the project is >= 10.0 (which is the current requirement for the Regex source generator. + /// + /// The compilation to be analyzed. + /// The resolved Regex type symbol + /// if source generator is supported in the project; otherwise, . + private static bool ProjectSupportsRegexSourceGenerator(Compilation compilation, [NotNullWhen(true)] out INamedTypeSymbol? regexTypeSymbol) + { + regexTypeSymbol = compilation.GetTypeByMetadataName(RegexTypeName); + if (regexTypeSymbol == null) + { + return false; + } + + INamedTypeSymbol regexGeneratorAttributeTypeSymbol = compilation.GetTypeByMetadataName(RegexGeneratorTypeName); + if (regexGeneratorAttributeTypeSymbol == null) + { + return false; + } + + if (compilation.SyntaxTrees.FirstOrDefault().Options is CSharpParseOptions options && options.LanguageVersion <= (LanguageVersion)1000) + { + return false; + } + + return true; + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexCodeFixer.cs b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs similarity index 81% rename from src/libraries/System.Text.RegularExpressions/gen/RegexCodeFixer.cs rename to src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs index 74c292ad339ff2..cdeefeae3d4410 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexCodeFixer.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs @@ -21,19 +21,20 @@ namespace System.Text.RegularExpressions.Generator { /// - /// Roslyn code fixer that will listen to SysLIB1046 diagnostics and will provide a code fix which onboards a particular RegEx into + /// Roslyn code fixer that will listen to SysLIB1046 diagnostics and will provide a code fix which onboards a particular Regex into /// source generation. /// [ExportCodeFixProvider(LanguageNames.CSharp)] - public class RegexCodeFixer : CodeFixProvider + public class UpgradeToRegexGeneratorCodeFixer : CodeFixProvider { - // private members private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; private const string DefaultRegexMethodName = "MyRegex"; /// - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RegexAnalyzer.DiagnosticId); + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticDescriptors.UseRegexSourceGeneration.Id); + + public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; /// public override async Task RegisterCodeFixesAsync(CodeFixContext context) @@ -165,9 +166,8 @@ private static async Task ConvertToSourceGenerator(Document document, // Parse the idices for where to get the arguments from. int?[] indices = new[] { - TryParseInt32(properties, RegexAnalyzer.PatternIndexName), - TryParseInt32(properties, RegexAnalyzer.RegexOptionsIndexName), - TryParseInt32(properties, RegexAnalyzer.RegexTimeoutIndexName), + TryParseInt32(properties, UpgradeToRegexGeneratorAnalyzer.PatternIndexName), + TryParseInt32(properties, UpgradeToRegexGeneratorAnalyzer.RegexOptionsIndexName) }; foreach (int? index in indices.Where(value => value != null).OrderByDescending(value => value)) @@ -189,31 +189,17 @@ private static async Task ConvertToSourceGenerator(Document document, // Initialize the inputs for the RegexGenerator attribute. SyntaxNode? patternValue = null; SyntaxNode? regexOptionsValue = null; - SyntaxNode? timeoutValue = null; - - // Try to get the timeout from the diagnostic's property bag. It will only be present if the original callsite used a timeout. - int? timeout = TryParseInt32(properties, RegexAnalyzer.RegexTimeoutName); - if (timeout is not null) - { - timeoutValue = generator.LiteralExpression(timeout.Value); - } // Try to get the pattern and RegexOptions values out from the diagnostic's property bag. if (operation is IObjectCreationOperation objectCreationOperation) // When using the Regex constructors { - patternValue = GetNode((objectCreationOperation).Arguments, properties, RegexAnalyzer.PatternIndexName); - regexOptionsValue = GetNode((objectCreationOperation).Arguments, properties, RegexAnalyzer.RegexOptionsIndexName); + patternValue = GetNode((objectCreationOperation).Arguments, properties, UpgradeToRegexGeneratorAnalyzer.PatternIndexName, generator, useOptionsMemberExpression: false, compilation); + regexOptionsValue = GetNode((objectCreationOperation).Arguments, properties, UpgradeToRegexGeneratorAnalyzer.RegexOptionsIndexName, generator, useOptionsMemberExpression: true, compilation); } else if (operation is IInvocationOperation invocation) // When using the Regex static methods. { - patternValue = GetNode(invocation.Arguments, properties, RegexAnalyzer.PatternIndexName); - regexOptionsValue = GetNode(invocation.Arguments, properties, RegexAnalyzer.RegexOptionsIndexName); - } - - // If the property bag had a timeout but not RegexOptions value, then we default to RegexOptions.None - if (timeoutValue is not null && regexOptionsValue is null) - { - regexOptionsValue = generator.MemberAccessExpression(generator.TypeExpression(compilation.GetTypeByMetadataName("System.Text.RegularExpressions.RegexOptions")!), "None"); + patternValue = GetNode(invocation.Arguments, properties, UpgradeToRegexGeneratorAnalyzer.PatternIndexName, generator, useOptionsMemberExpression: false, compilation); + regexOptionsValue = GetNode(invocation.Arguments, properties, UpgradeToRegexGeneratorAnalyzer.RegexOptionsIndexName, generator, useOptionsMemberExpression: true, compilation); } // Generate the new static partial method @@ -227,11 +213,10 @@ private static async Task ConvertToSourceGenerator(Document document, newMethod = newMethod.ReplaceToken(newMethod.Identifier, SyntaxFactory.Identifier(methodName).WithAdditionalAnnotations(RenameAnnotation.Create())); // Generate the RegexGenerator attribute syntax node with the specified parameters. - SyntaxNode attributes = generator.Attribute(generator.TypeExpression(regexGeneratorAttributeSymbol), attributeArguments: (patternValue, regexOptionsValue, timeoutValue) switch + SyntaxNode attributes = generator.Attribute(generator.TypeExpression(regexGeneratorAttributeSymbol), attributeArguments: (patternValue, regexOptionsValue) switch { - ({ }, null, null) => new[] { patternValue }, - ({ }, { }, null) => new[] { patternValue, regexOptionsValue }, - ({ }, { }, { }) => new[] { patternValue, regexOptionsValue, SyntaxFactory.AttributeArgument((ExpressionSyntax)timeoutValue).WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName("matchTimeoutMilliseconds"))) }, + ({ }, null) => new[] { patternValue }, + ({ }, { }) => new[] { patternValue, regexOptionsValue }, _ => Array.Empty(), }); @@ -275,7 +260,7 @@ static IEnumerable GetAllMembers(ITypeSymbol? symbol) } // Helper method that looks int the properties bag for the index of the passed in propertyname, and then returns that index from the args parameter. - static SyntaxNode? GetNode(ImmutableArray args, ImmutableDictionary properties, string propertyName) + static SyntaxNode? GetNode(ImmutableArray args, ImmutableDictionary properties, string propertyName, SyntaxGenerator generator, bool useOptionsMemberExpression, Compilation compilation) { int? index = TryParseInt32(properties, propertyName); if (index == null) @@ -283,7 +268,37 @@ static IEnumerable GetAllMembers(ITypeSymbol? symbol) return null; } - return args[index.Value].Value.Syntax; + if (!useOptionsMemberExpression) + { + return generator.LiteralExpression(args[index.Value].Value.ConstantValue.Value); + } + else + { + RegexOptions options = (RegexOptions)(int)args[index.Value].Value.ConstantValue.Value; + string optionsLiteral = Literal(options); + return SyntaxFactory.ParseExpression(optionsLiteral).SyntaxTree.GetRoot(); + } + } + + static string Literal(RegexOptions options) + { + string s = options.ToString(); + if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) + { + // The options were formatted as an int, which means the runtime couldn't + // produce a textual representation. So just output casting the value as an int. + Debug.Fail("This shouldn't happen, as we should only get to the point of emitting code if RegexOptions was valid."); + return $"(RegexOptions)({(int)options})"; + } + + // Parse the runtime-generated "Option1, Option2" into each piece and then concat + // them back together. + string[] parts = s.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < parts.Length; i++) + { + parts[i] = "RegexOptions." + parts[i].Trim(); + } + return string.Join(" | ", parts); } } } diff --git a/src/libraries/System.Text.RegularExpressions/src/Resources/Strings.resx b/src/libraries/System.Text.RegularExpressions/src/Resources/Strings.resx index 452b4ccf8f2cf9..e2d170c5ab9bcb 100644 --- a/src/libraries/System.Text.RegularExpressions/src/Resources/Strings.resx +++ b/src/libraries/System.Text.RegularExpressions/src/Resources/Strings.resx @@ -288,4 +288,7 @@ Searching an input span using a pre-compiled Regex assembly is not supported. Please use the string overloads or use a newer Regex implementation. - + + Use the RegEx source generator. + + \ No newline at end of file diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs new file mode 100644 index 00000000000000..0cbda942d8cc06 --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace System.Text.RegularExpressions.Unit.Tests +{ + public static class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + { + /// + public static DiagnosticResult Diagnostic() + => CSharpCodeFixVerifier.Diagnostic(); + + /// + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpCodeFixVerifier.Diagnostic(diagnosticId); + + /// + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => CSharpCodeFixVerifier.Diagnostic(descriptor); + + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + => await VerifyAnalyzerAsync(source, ReferenceAssemblies.Net.Net70, usePreviewLanguageVersion: true, expected); + + /// + public static async Task VerifyAnalyzerAsync(string source, ReferenceAssemblies references, bool usePreviewLanguageVersion, params DiagnosticResult[] expected) + { + Test test = new Test(references, usePreviewLanguageVersion, numberOfIterations: 1) + { + TestCode = source, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + /// + public static async Task VerifyCodeFixAsync(string source, string fixedSource) + => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource); + + /// + public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource, int numberOfIterations = 1) + { + Test test = new Test(ReferenceAssemblies.Net.Net70, usePreviewLanguageVersion: true, numberOfIterations) + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + public class Test : CSharpCodeFixTest + { + public Test(ReferenceAssemblies references, bool usePreviewLanguageVersion, int numberOfIterations) + { + // Code Fixer generates partial methods that will need to use the source generator to be filled. + this.CompilerDiagnostics = CompilerDiagnostics.None; + + ReferenceAssemblies = references; + + NumberOfFixAllIterations = numberOfIterations; + + SolutionTransforms.Add((solution, projectId) => + { + Microsoft.CodeAnalysis.CompilationOptions compilationOptions = solution.GetProject(projectId).CompilationOptions; + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + if (usePreviewLanguageVersion) + { + CSharpParseOptions parseOptions = solution.GetProject(projectId).ParseOptions as CSharpParseOptions; + parseOptions = parseOptions.WithLanguageVersion(LanguageVersion.Preview); + solution = solution.WithProjectParseOptions(projectId, parseOptions); + } + + return solution; + }); + } + } + } + + internal static class CSharpVerifierHelper + { + /// + /// By default, the compiler reports diagnostics for nullable reference types at + /// , and the analyzer test framework defaults to only validating + /// diagnostics at . This map contains all compiler diagnostic IDs + /// related to nullability mapped to , which is then used to enable all + /// of these warnings for default validation during analyzer and code fix tests. + /// + internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + CSharpCommandLineArguments commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + ImmutableDictionary nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + // Workaround for https://github.com/dotnet/roslyn/issues/41610 + nullableWarnings = nullableWarnings + .SetItem("CS8632", ReportDiagnostic.Error) + .SetItem("CS8669", ReportDiagnostic.Error); + + return nullableWarnings; + } + } +} diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/Stubs.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/Stubs.cs index 70594d4e08bb46..9776dc3e38cd53 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/Stubs.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/Stubs.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using Microsoft.CodeAnalysis; namespace System.Text.RegularExpressions { @@ -22,3 +23,17 @@ public RegexReplacement(string rep, RegexNode concat, Hashtable caps) { } public const int WholeString = -4; } } + +namespace System.Text.RegularExpressions.Generator +{ + internal static class DiagnosticDescriptors + { + public static DiagnosticDescriptor UseRegexSourceGeneration { get; } = new DiagnosticDescriptor( + id: "SYSLIB1046", + title: "Use the Regex source generator.", + messageFormat: "The inputs to your Regex are known at compile-time so you could be using the source generator to boost performance.", + category: "RegexGenerator", + DiagnosticSeverity.Info, + isEnabledByDefault: true); + } +} diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj index acf507b90c8a64..6129eab9b8e1ae 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj @@ -12,6 +12,9 @@ + + + @@ -43,8 +46,15 @@ + + + + + + + diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs new file mode 100644 index 00000000000000..757b70d229bad7 --- /dev/null +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs @@ -0,0 +1,699 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions.Generator; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = System.Text.RegularExpressions.Unit.Tests.CSharpCodeFixVerifier< + System.Text.RegularExpressions.Generator.UpgradeToRegexGeneratorAnalyzer, + System.Text.RegularExpressions.Generator.UpgradeToRegexGeneratorCodeFixer>; + +namespace System.Text.RegularExpressions.Unit.Tests +{ + public class UpgradeToRegexGeneratorAnalyzerTests + { + [Fact] + public async Task NoDiagnosticsForEmpty() + => await VerifyCS.VerifyAnalyzerAsync(source: string.Empty); + + [Fact] + public async Task NoDiagnosticForConstructorWithTimeout() + { + string test = @"using System; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var regex = new Regex("""", RegexOptions.None, TimeSpan.FromSeconds(10)); + } +} +"; + + await VerifyCS.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task NoDiagnosticForTopLevelStatements() + { + string test = @"using System.Text.RegularExpressions; + +Regex r = new Regex("""");"; + + await VerifyCS.VerifyAnalyzerAsync(test); + } + + public static IEnumerable StaticInvocationWithTimeoutTestData() + { + foreach(string method in new[] { "Count", "EnumerateMatches", "IsMatch", "Match", "Matches", "Split"}) + { + yield return new object[] { @"using System; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + Regex." + method + @"(""input"", ""a|b"", RegexOptions.None, TimeSpan.FromSeconds(10)); + } +}" }; + } + + // Replace is special since it takes an extra argument + yield return new object[] { @"using System; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + Regex.Replace(""input"", ""a|b"", ""replacement"" ,RegexOptions.None, TimeSpan.FromSeconds(10)); + } +}" }; + } + + [Theory] + [MemberData(nameof(StaticInvocationWithTimeoutTestData))] + public async Task NoDiagnosticForStaticInvocationWithTimeout(string test) + => await VerifyCS.VerifyAnalyzerAsync(test); + + [Theory] + [MemberData(nameof(InvocationTypes))] + public async Task NoDiagnosticsForNet60(InvocationType invocationType) + { + string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty; + string test = @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var isMatch = " + ConstructRegexInvocation(invocationType, pattern: "\"\"") + isMatchInvocation + @"; + } +}"; + + await VerifyCS.VerifyAnalyzerAsync(test, ReferenceAssemblies.Net.Net60, usePreviewLanguageVersion: true); + } + + [Theory] + [MemberData(nameof(InvocationTypes))] + public async Task NoDiagnosticsForLowerLanguageVersion(InvocationType invocationType) + { + string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty; + string test = @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var isMatch = " + ConstructRegexInvocation(invocationType, "\"\"") + isMatchInvocation + @"; + } +}"; + + await VerifyCS.VerifyAnalyzerAsync(test, ReferenceAssemblies.Net.Net70, usePreviewLanguageVersion: false); + } + + public static IEnumerable ConstantPatternTestData() + { + foreach (InvocationType invocationType in new[] { InvocationType.Constructor, InvocationType.StaticMethods }) + { + string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty; + // Test constructor with a passed in literal pattern. + yield return new object[] { @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var isMatch = {|#0:" + ConstructRegexInvocation(invocationType, "\"\"") + @"|}" + isMatchInvocation + @"; + } +}", @"using System.Text; +using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + var isMatch = MyRegex().IsMatch(""""); + } + + [RegexGenerator("""")] + private static partial Regex MyRegex(); +}" }; + + // Test constructor with a local constant pattern. + yield return new object[] { @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + const string pattern = @""""; + var isMatch = {|#0:" + ConstructRegexInvocation(invocationType, "\"\"") + @"|}" + isMatchInvocation + @"; + } +}", @"using System.Text; +using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + const string pattern = @""""; + var isMatch = MyRegex().IsMatch(""""); + } + + [RegexGenerator("""")] + private static partial Regex MyRegex(); +}" }; + + // Test constructor with a constant field pattern. + yield return new object[] { @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + private const string pattern = @""""; + + public static void Main(string[] args) + { + var isMatch = {|#0:" + ConstructRegexInvocation(invocationType, "\"\"") + @"|}" + isMatchInvocation + @"; + } +}", @"using System.Text; +using System.Text.RegularExpressions; + +public partial class Program +{ + private const string pattern = @""""; + + public static void Main(string[] args) + { + var isMatch = MyRegex().IsMatch(""""); + } + + [RegexGenerator("""")] + private static partial Regex MyRegex(); +}" }; + } + } + + [Theory] + [MemberData(nameof(ConstantPatternTestData))] + public async Task DiagnosticEmittedForConstantPattern(string test, string fixedSource) + { + DiagnosticResult expectedDiagnostic = VerifyCS.Diagnostic(DiagnosticDescriptors.UseRegexSourceGeneration.Id).WithLocation(0); + await VerifyCS.VerifyCodeFixAsync(test, expectedDiagnostic, fixedSource); + } + + public static IEnumerable VariablePatternTestData() + { + foreach (InvocationType invocationType in new[] { InvocationType.Constructor, InvocationType.StaticMethods }) + { + string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty; + // Test constructor with passed in parameter + yield return new object[] { @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var isMatch = " + ConstructRegexInvocation(invocationType, "args[0]") + isMatchInvocation + @"; + } +}" }; + + // Test constructor with passed in variable + yield return new object[] { @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + string somePattern = """"; + var isMatch = " + ConstructRegexInvocation(invocationType, "somePattern") + isMatchInvocation + @"; + } +}" }; + + // Test constructor with readonly property + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public string Pattern { get; } + + public void M() + { + var isMatch = " + ConstructRegexInvocation(invocationType, "Pattern") + isMatchInvocation + @"; + } +}" }; + + // Test constructor with field + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public readonly string Pattern; + + public void M() + { + var isMatch = " + ConstructRegexInvocation(invocationType, "Pattern") + isMatchInvocation + @"; + } +}" }; + + // Test constructor with return method + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public string GetMyPattern() => """"; + + public void M() + { + var isMatch = " + ConstructRegexInvocation(invocationType, "GetMyPattern()") + isMatchInvocation + @"; + } +}" }; + } + } + + [Theory] + [MemberData(nameof(VariablePatternTestData))] + public async Task DiagnosticNotEmittedForVariablePattern(string test) + => await VerifyCS.VerifyAnalyzerAsync(test); + + public static IEnumerable ConstantOptionsTestData() + { + foreach (InvocationType invocationType in new[] { InvocationType.Constructor, InvocationType.StaticMethods }) + { + string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty; + // Test options as passed in literal + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var isMatch = {|#0:" + ConstructRegexInvocation(invocationType, "\"\"", "RegexOptions.None") + @"|}" + isMatchInvocation + @"; + } +}", @"using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + var isMatch = MyRegex().IsMatch(""""); + } + + [RegexGenerator("""", RegexOptions.None)] + private static partial Regex MyRegex(); +}" }; + + // Test options as local constant + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + const RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + var isMatch = {|#0:" + ConstructRegexInvocation(invocationType, "\"\"", "options") + @"|}" + isMatchInvocation + @"; + } +}", @"using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + const RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + var isMatch = MyRegex().IsMatch(""""); + } + + [RegexGenerator("""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex MyRegex(); +}" }; + + // Test options as constant field + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + const RegexOptions Options = RegexOptions.None; + + public static void Main(string[] args) + { + var isMatch = {|#0:" + ConstructRegexInvocation(invocationType, "\"\"", "Options") + @"|}" + isMatchInvocation + @"; + } +}", @"using System.Text.RegularExpressions; + +public partial class Program +{ + const RegexOptions Options = RegexOptions.None; + + public static void Main(string[] args) + { + var isMatch = MyRegex().IsMatch(""""); + } + + [RegexGenerator("""", RegexOptions.None)] + private static partial Regex MyRegex(); +}" }; + } + } + + [Theory] + [MemberData(nameof(ConstantOptionsTestData))] + public async Task DiagnosticEmittedForConstantOptions(string test, string fixedSource) + { + DiagnosticResult expected = VerifyCS.Diagnostic(DiagnosticDescriptors.UseRegexSourceGeneration.Id).WithLocation(0); + await VerifyCS.VerifyCodeFixAsync(test, expected, fixedSource); + } + + public static IEnumerable VariableOptionsTestData() + { + foreach (InvocationType invocationType in new[] { InvocationType.Constructor, InvocationType.StaticMethods }) + { + string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty; + // Test options as passed in parameter + yield return new object[] { @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(RegexOptions options) + { + var isMatch = " + ConstructRegexInvocation(invocationType, "\"\"", "options") + isMatchInvocation + @"; + } +}" }; + + // Test options as passed in variable + yield return new object[] { @"using System.Text; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + RegexOptions options = RegexOptions.None; + var isMatch = " + ConstructRegexInvocation(invocationType, "\"\"", "options") + isMatchInvocation + @"; + } +}" }; + + // Test options as readonly property + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public RegexOptions Options { get; } + + public void M() + { + var isMatch = " + ConstructRegexInvocation(invocationType, "\"\"", "Options") + isMatchInvocation + @"; + } +}" }; + + // Test options as readonly field + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public readonly RegexOptions Options; + + public void M() + { + var isMatch = " + ConstructRegexInvocation(invocationType, "\"\"", "Options") + isMatchInvocation + @"; + } +}" }; + + // Test options as return method. + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public RegexOptions GetMyOptions() => RegexOptions.None; + + public void M() + { + var isMatch = " + ConstructRegexInvocation(invocationType, "\"\"", "GetMyOptions()") + isMatchInvocation + @"; + } +}" }; + } + } + + [Theory] + [MemberData(nameof(VariableOptionsTestData))] + public async Task DiagnosticNotEmittedForVariableOptions(string test) + => await VerifyCS.VerifyAnalyzerAsync(test); + + public static IEnumerable StaticInvocationsAndFixedSourceTestData() + { + const string testTemplateWithOptions = @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + {|#0:Regex.@@Method@@(""input"", ""a|b"", RegexOptions.None)|}; + } +}"; + const string fixedSourceWithOptions = @"using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + MyRegex().@@Method@@(""input""); + } + + [RegexGenerator(""a|b"", RegexOptions.None)] + private static partial Regex MyRegex(); +}"; + DiagnosticResult expectedDiagnostic = VerifyCS.Diagnostic(DiagnosticDescriptors.UseRegexSourceGeneration.Id).WithLocation(0); + + const string testTemplateWithoutOptions = @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + {|#0:Regex.@@Method@@(""input"", ""a|b"")|}; + } +}"; + const string fixedSourceWithoutOptions = @"using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + MyRegex().@@Method@@(""input""); + } + + [RegexGenerator(""a|b"")] + private static partial Regex MyRegex(); +}"; + + foreach (bool includeRegexOptions in new[] { true, false }) + { + // Can't test EnumerateMatches yet since the reference assemblies used by the test infrastructure doesn't have that API yet. + foreach (string methodName in new[] { "Count", /* "EnumerateMatches" ,*/ "IsMatch", "Match", "Matches", "Split" }) + { + if (includeRegexOptions) + { + yield return new object[] { testTemplateWithOptions.Replace("@@Method@@", methodName), expectedDiagnostic, fixedSourceWithOptions.Replace("@@Method@@", methodName) }; + } + else + { + yield return new object[] { testTemplateWithoutOptions.Replace("@@Method@@", methodName), expectedDiagnostic, fixedSourceWithoutOptions.Replace("@@Method@@", methodName) }; + + } + } + } + + // Replace has one additional parameter so we treat that case separately. + + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + {|#0:Regex.Replace(""input"", ""a[b|c]*"", ""replacement"", RegexOptions.CultureInvariant)|}; + } +} +", expectedDiagnostic, @"using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + MyRegex().Replace(""input"", ""replacement""); + } + + [RegexGenerator(""a[b|c]*"", RegexOptions.CultureInvariant)] + private static partial Regex MyRegex(); +} +" }; + + yield return new object[] { @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + {|#0:Regex.Replace(""input"", ""a[b|c]*"", ""replacement"")|}; + } +} +", expectedDiagnostic, @"using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main(string[] args) + { + MyRegex().Replace(""input"", ""replacement""); + } + + [RegexGenerator(""a[b|c]*"")] + private static partial Regex MyRegex(); +} +" }; + } + + [Theory] + [MemberData(nameof(StaticInvocationsAndFixedSourceTestData))] + public async Task DiagnosticAndCodeFixForAllStaticMethods(string test, DiagnosticResult expectedDiagnostic, string fixedSource) + => await VerifyCS.VerifyCodeFixAsync(test, expectedDiagnostic, fixedSource); + + [Fact] + public async Task CodeFixSupportsNesting() + { + DiagnosticResult expectedDiagnostic = VerifyCS.Diagnostic(DiagnosticDescriptors.UseRegexSourceGeneration.Id).WithLocation(0); + string test = @"using System.Text.RegularExpressions; + +public class A +{ + public partial class B + { + public class C + { + public partial class D + { + public void Foo() + { + Regex regex = {|#0:new Regex(""pattern"", RegexOptions.IgnorePatternWhitespace)|}; + } + } + } + } +} +"; + string fixedSource = @"using System.Text.RegularExpressions; + +public partial class A +{ + public partial class B + { + public partial class C + { + public partial class D + { + public void Foo() + { + Regex regex = MyRegex(); + } + + [RegexGenerator(""pattern"", RegexOptions.IgnorePatternWhitespace)] + private static partial Regex MyRegex(); + } + } + } +} +"; + + await VerifyCS.VerifyCodeFixAsync(test, expectedDiagnostic, fixedSource); + } + + [Fact] + public async Task AnayzerSupportsMultipleDiagnostics() + { + string test = @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main() + { + Regex regex1 = {|#0:new Regex(""a|b"")|}; + Regex regex2 = {|#1:new Regex(""c|d"", RegexOptions.CultureInvariant)|}; + } +} +"; + DiagnosticResult[] expectedDiagnostics = new[] + { + VerifyCS.Diagnostic(DiagnosticDescriptors.UseRegexSourceGeneration.Id).WithLocation(0), + VerifyCS.Diagnostic(DiagnosticDescriptors.UseRegexSourceGeneration.Id).WithLocation(1) + }; + + string fixedSource = @"using System.Text.RegularExpressions; + +public partial class Program +{ + public static void Main() + { + Regex regex1 = MyRegex(); + Regex regex2 = MyRegex1(); + } + + [RegexGenerator(""a|b"")] + private static partial Regex MyRegex(); + [RegexGenerator(""c|d"", RegexOptions.CultureInvariant)] + private static partial Regex MyRegex1(); +} +"; + + await VerifyCS.VerifyCodeFixAsync(test, expectedDiagnostics, fixedSource, 2); + } + + #region Test helpers + + private static string ConstructRegexInvocation(InvocationType invocationType, string pattern, string? options = null) + => invocationType switch + { + InvocationType.StaticMethods => (pattern is null, options is null) switch + { + (false, true) => $"Regex.IsMatch(\"\", {pattern})", + (false, false) => $"Regex.IsMatch(\"\", {pattern}, {options})", + _ => throw new InvalidOperationException() + }, + InvocationType.Constructor => (pattern is null, options is null) switch + { + (false, true) => $"new Regex({pattern})", + (false, false) => $"new Regex({pattern}, {options})", + _ => throw new InvalidOperationException() + }, + _ => throw new ArgumentOutOfRangeException(nameof(invocationType)) + }; + + public static IEnumerable InvocationTypes + => new object[][] + { + new object[] { InvocationType.StaticMethods }, + new object[] { InvocationType.Constructor } + }; + + public enum InvocationType + { + StaticMethods, + Constructor + } + + #endregion Test helpers + } +} From 42631642b626df2b0078b4b5ba713dc8c112e098 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Fri, 20 May 2022 15:32:07 -0700 Subject: [PATCH 3/6] Fix build and reference live ref pack --- .../gen/UpgradeToRegexGeneratorCodeFixer.cs | 12 +++++------ .../UnitTests/CSharpCodeFixVerifier`2.cs | 20 ++++++++++++++----- ....Text.RegularExpressions.Unit.Tests.csproj | 6 ++++-- .../UpgradeToRegexGeneratorAnalyzerTests.cs | 5 ++--- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs index cdeefeae3d4410..a5c4df1d4b3891 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs @@ -193,13 +193,13 @@ private static async Task ConvertToSourceGenerator(Document document, // Try to get the pattern and RegexOptions values out from the diagnostic's property bag. if (operation is IObjectCreationOperation objectCreationOperation) // When using the Regex constructors { - patternValue = GetNode((objectCreationOperation).Arguments, properties, UpgradeToRegexGeneratorAnalyzer.PatternIndexName, generator, useOptionsMemberExpression: false, compilation); - regexOptionsValue = GetNode((objectCreationOperation).Arguments, properties, UpgradeToRegexGeneratorAnalyzer.RegexOptionsIndexName, generator, useOptionsMemberExpression: true, compilation); + patternValue = GetNode((objectCreationOperation).Arguments, properties, UpgradeToRegexGeneratorAnalyzer.PatternIndexName, generator, useOptionsMemberExpression: false, compilation, cancellationToken); + regexOptionsValue = GetNode((objectCreationOperation).Arguments, properties, UpgradeToRegexGeneratorAnalyzer.RegexOptionsIndexName, generator, useOptionsMemberExpression: true, compilation, cancellationToken); } else if (operation is IInvocationOperation invocation) // When using the Regex static methods. { - patternValue = GetNode(invocation.Arguments, properties, UpgradeToRegexGeneratorAnalyzer.PatternIndexName, generator, useOptionsMemberExpression: false, compilation); - regexOptionsValue = GetNode(invocation.Arguments, properties, UpgradeToRegexGeneratorAnalyzer.RegexOptionsIndexName, generator, useOptionsMemberExpression: true, compilation); + patternValue = GetNode(invocation.Arguments, properties, UpgradeToRegexGeneratorAnalyzer.PatternIndexName, generator, useOptionsMemberExpression: false, compilation, cancellationToken); + regexOptionsValue = GetNode(invocation.Arguments, properties, UpgradeToRegexGeneratorAnalyzer.RegexOptionsIndexName, generator, useOptionsMemberExpression: true, compilation, cancellationToken); } // Generate the new static partial method @@ -260,7 +260,7 @@ static IEnumerable GetAllMembers(ITypeSymbol? symbol) } // Helper method that looks int the properties bag for the index of the passed in propertyname, and then returns that index from the args parameter. - static SyntaxNode? GetNode(ImmutableArray args, ImmutableDictionary properties, string propertyName, SyntaxGenerator generator, bool useOptionsMemberExpression, Compilation compilation) + static SyntaxNode? GetNode(ImmutableArray args, ImmutableDictionary properties, string propertyName, SyntaxGenerator generator, bool useOptionsMemberExpression, Compilation compilation, CancellationToken cancellationToken) { int? index = TryParseInt32(properties, propertyName); if (index == null) @@ -276,7 +276,7 @@ static IEnumerable GetAllMembers(ITypeSymbol? symbol) { RegexOptions options = (RegexOptions)(int)args[index.Value].Value.ConstantValue.Value; string optionsLiteral = Literal(options); - return SyntaxFactory.ParseExpression(optionsLiteral).SyntaxTree.GetRoot(); + return SyntaxFactory.ParseExpression(optionsLiteral).SyntaxTree.GetRoot(cancellationToken); } } diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs index 0cbda942d8cc06..83ce1218adb7ce 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs @@ -34,10 +34,10 @@ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) /// public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) - => await VerifyAnalyzerAsync(source, ReferenceAssemblies.Net.Net70, usePreviewLanguageVersion: true, expected); + => await VerifyAnalyzerAsync(source, null, usePreviewLanguageVersion: true, expected); /// - public static async Task VerifyAnalyzerAsync(string source, ReferenceAssemblies references, bool usePreviewLanguageVersion, params DiagnosticResult[] expected) + public static async Task VerifyAnalyzerAsync(string source, ReferenceAssemblies? references, bool usePreviewLanguageVersion, params DiagnosticResult[] expected) { Test test = new Test(references, usePreviewLanguageVersion, numberOfIterations: 1) { @@ -59,7 +59,7 @@ public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expe /// public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource, int numberOfIterations = 1) { - Test test = new Test(ReferenceAssemblies.Net.Net70, usePreviewLanguageVersion: true, numberOfIterations) + Test test = new Test(null, usePreviewLanguageVersion: true, numberOfIterations) { TestCode = source, FixedCode = fixedSource, @@ -71,12 +71,22 @@ public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] ex public class Test : CSharpCodeFixTest { - public Test(ReferenceAssemblies references, bool usePreviewLanguageVersion, int numberOfIterations) + public Test(ReferenceAssemblies? references, bool usePreviewLanguageVersion, int numberOfIterations) { // Code Fixer generates partial methods that will need to use the source generator to be filled. this.CompilerDiagnostics = CompilerDiagnostics.None; - ReferenceAssemblies = references; + if (references != null) + { + ReferenceAssemblies = references; + } + else + { + // Clear out the default reference assemblies. We explicitly add references from the live ref pack, + // so we don't want the Roslyn test infrastructure to resolve/add any default reference assemblies + ReferenceAssemblies = new ReferenceAssemblies(string.Empty); + TestState.AdditionalReferences.AddRange(SourceGenerators.Tests.LiveReferencePack.GetMetadataReferences()); + } NumberOfFixAllIterations = numberOfIterations; diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj index 6129eab9b8e1ae..77f1ad9eb8adec 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -9,6 +9,7 @@ true true $(DefineConstants);DEBUG + true @@ -48,6 +49,7 @@ + @@ -55,6 +57,6 @@ - + diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs index 757b70d229bad7..dc123275c84f5b 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs @@ -119,7 +119,7 @@ public static void Main(string[] args) } }"; - await VerifyCS.VerifyAnalyzerAsync(test, ReferenceAssemblies.Net.Net70, usePreviewLanguageVersion: false); + await VerifyCS.VerifyAnalyzerAsync(test, null, usePreviewLanguageVersion: false); } public static IEnumerable ConstantPatternTestData() @@ -503,8 +503,7 @@ public static void Main(string[] args) foreach (bool includeRegexOptions in new[] { true, false }) { - // Can't test EnumerateMatches yet since the reference assemblies used by the test infrastructure doesn't have that API yet. - foreach (string methodName in new[] { "Count", /* "EnumerateMatches" ,*/ "IsMatch", "Match", "Matches", "Split" }) + foreach (string methodName in new[] { "Count", "EnumerateMatches" , "IsMatch", "Match", "Matches", "Split" }) { if (includeRegexOptions) { From c9135c1fc4471cbc08c243a4cc0ac0ba1fe14784 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Tue, 24 May 2022 14:41:54 -0700 Subject: [PATCH 4/6] Address remaining feedback and fix top-level statement programs --- .../gen/Resources/Strings.resx | 2 +- .../gen/Resources/xlf/Strings.cs.xlf | 4 +- .../gen/Resources/xlf/Strings.de.xlf | 4 +- .../gen/Resources/xlf/Strings.es.xlf | 4 +- .../gen/Resources/xlf/Strings.fr.xlf | 4 +- .../gen/Resources/xlf/Strings.it.xlf | 4 +- .../gen/Resources/xlf/Strings.ja.xlf | 4 +- .../gen/Resources/xlf/Strings.ko.xlf | 4 +- .../gen/Resources/xlf/Strings.pl.xlf | 4 +- .../gen/Resources/xlf/Strings.pt-BR.xlf | 4 +- .../gen/Resources/xlf/Strings.ru.xlf | 4 +- .../gen/Resources/xlf/Strings.tr.xlf | 4 +- .../gen/Resources/xlf/Strings.zh-Hans.xlf | 4 +- .../gen/Resources/xlf/Strings.zh-Hant.xlf | 4 +- .../gen/UpgradeToRegexGeneratorAnalyzer.cs | 82 ++++++++++++++++--- .../UnitTests/CSharpCodeFixVerifier`2.cs | 31 ------- .../UpgradeToRegexGeneratorAnalyzerTests.cs | 18 ++++ 17 files changed, 116 insertions(+), 69 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx b/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx index 316c58e8850e50..dfee4c675b49c7 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/Strings.resx @@ -233,7 +233,7 @@ Quantifier '{0}' following nothing. - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. Replacement pattern error. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf index 624b540b957c4d..9ca26e78bdd487 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.cs.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Vypršel časový limit modulu RegEx při pokusu o porovnání vzoru se vstupním řetězcem. K tomu může dojít z celé řady důvodů, mezi které patří velká velikost vstupních dat nebo nadměrné zpětné navracení způsobené vloženými kvantifikátory, zpětnými odkazy a dalšími faktory. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Vypršel časový limit modulu RegEx při pokusu o porovnání vzoru se vstupním řetězcem. K tomu může dojít z celé řady důvodů, mezi které patří velká velikost vstupních dat nebo nadměrné zpětné navracení způsobené vloženými kvantifikátory, zpětnými odkazy a dalšími faktory. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf index 50b76c0280c4eb..f94bcea6d402c2 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.de.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Zeitüberschreitung des RegEx-Moduls beim Versuch, ein Muster mit einer Eingabezeichenfolge in Übereinstimmung zu bringen. Dies kann viele Ursachen haben, darunter sehr große Eingaben oder übermäßige Rückverfolgung aufgrund von geschachtelten Quantifizierern, Rückverweisen und anderen Faktoren. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Zeitüberschreitung des RegEx-Moduls beim Versuch, ein Muster mit einer Eingabezeichenfolge in Übereinstimmung zu bringen. Dies kann viele Ursachen haben, darunter sehr große Eingaben oder übermäßige Rückverfolgung aufgrund von geschachtelten Quantifizierern, Rückverweisen und anderen Faktoren. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf index 7cc5c76dd03409..40fde404fdfc5b 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.es.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Se agotó el tiempo de espera mientras el motor de RegEx intentaba comparar una cadena de entrada con un patrón. Esto puede deberse a muchos motivos, como la especificación de cadenas de entrada muy grandes o búsquedas hacia atrás excesivas causadas por cuantificadores anidados, referencias inversas y otros factores. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Se agotó el tiempo de espera mientras el motor de RegEx intentaba comparar una cadena de entrada con un patrón. Esto puede deberse a muchos motivos, como la especificación de cadenas de entrada muy grandes o búsquedas hacia atrás excesivas causadas por cuantificadores anidados, referencias inversas y otros factores. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf index 8d859dda8388c8..1db9425597ffb0 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.fr.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Le délai d'attente du moteur RegEx a expiré pendant la tentative d'appariement d'un modèle avec une chaîne d'entrée. Ce problème peut se produire pour de nombreuses raisons, notamment en cas d'entrées volumineuses ou de retour sur trace excessif causé par les quantificateurs imbriqués, les références arrière et d'autres facteurs. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Le délai d'attente du moteur RegEx a expiré pendant la tentative d'appariement d'un modèle avec une chaîne d'entrée. Ce problème peut se produire pour de nombreuses raisons, notamment en cas d'entrées volumineuses ou de retour sur trace excessif causé par les quantificateurs imbriqués, les références arrière et d'autres facteurs. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf index 0a7f3009f48fdb..458bc18b84b5c2 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.it.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Timeout del motore RegEx durante il tentativo di corrispondenza di un criterio a una stringa di input. Il timeout si può verificare per molti motivi, compresi gli input molto grandi o un eccessivo backtracking causato dai quantificatori annidati, backreference e altri fattori. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Timeout del motore RegEx durante il tentativo di corrispondenza di un criterio a una stringa di input. Il timeout si può verificare per molti motivi, compresi gli input molto grandi o un eccessivo backtracking causato dai quantificatori annidati, backreference e altri fattori. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf index e4fd6d9e69a8eb..02f2a8c91a2bf2 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ja.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - パターンと入力文字列との照合中に、RegEx エンジンがタイムアウトしました。これは、非常に大きな入力、入れ子になった量指定子によって生じた過剰なバックトラッキング、前方参照などの要因を含む、さまざまな原因によって発生する可能性があります。 + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + パターンと入力文字列との照合中に、RegEx エンジンがタイムアウトしました。これは、非常に大きな入力、入れ子になった量指定子によって生じた過剰なバックトラッキング、前方参照などの要因を含む、さまざまな原因によって発生する可能性があります。 diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf index aa04d6190b0e18..c9f2586872984c 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ko.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - RegEx 엔진이 패턴을 입력 문자열에 일치시키는 동안 시간 초과되었습니다. 이 오류는 많은 입력, 중첩 수량자로 인한 과도한 역추적, 역참조, 기타 요인 등의 다양한 이유로 인해 발생할 수 있습니다. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + RegEx 엔진이 패턴을 입력 문자열에 일치시키는 동안 시간 초과되었습니다. 이 오류는 많은 입력, 중첩 수량자로 인한 과도한 역추적, 역참조, 기타 요인 등의 다양한 이유로 인해 발생할 수 있습니다. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf index 237d66466b87b9..06cbe46b86ac34 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pl.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Upłynął limit czasu podczas próby dopasowania przez aparat wyrażeń regularnych wzorca do ciągu wejściowego. Mogło to być spowodowane wieloma przyczynami, w tym bardzo dużą ilością danych wejściowych, nadmiernym wykorzystaniem algorytmu wycofywania związanym z kwantyfikatorami zagnieżdżonymi, odwołaniami wstecznymi i innymi czynnikami. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Upłynął limit czasu podczas próby dopasowania przez aparat wyrażeń regularnych wzorca do ciągu wejściowego. Mogło to być spowodowane wieloma przyczynami, w tym bardzo dużą ilością danych wejściowych, nadmiernym wykorzystaniem algorytmu wycofywania związanym z kwantyfikatorami zagnieżdżonymi, odwołaniami wstecznymi i innymi czynnikami. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf index d0d2e50388315e..9f6a7101fbaf5a 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.pt-BR.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - O mecanismo de RegEx esgotou o tempo limite ao tentar combinar um padrão com uma cadeia de caracteres de entrada. Isso pode ocorrer por vários motivos, incluindo entradas muito grandes ou acompanhamento inverso excessivo causado por quantificadores aninhados, referências inversas e outros fatores. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + O mecanismo de RegEx esgotou o tempo limite ao tentar combinar um padrão com uma cadeia de caracteres de entrada. Isso pode ocorrer por vários motivos, incluindo entradas muito grandes ou acompanhamento inverso excessivo causado por quantificadores aninhados, referências inversas e outros fatores. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf index 5495d38b6eb012..ce61c81def8f20 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.ru.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - Истекло время ожидания модуля RegEx при попытке сравнить шаблон со входной строкой. Это могло произойти по многим причинам, в том числе из-за очень большого объема входных данных или излишнего обратного отслеживания, вызванного вложенными квантификаторами, обратными ссылками и прочими факторами. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + Истекло время ожидания модуля RegEx при попытке сравнить шаблон со входной строкой. Это могло произойти по многим причинам, в том числе из-за очень большого объема входных данных или излишнего обратного отслеживания, вызванного вложенными квантификаторами, обратными ссылками и прочими факторами. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf index 3853a8375a07d0..4d19136042455c 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.tr.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - RegEx altyapısı bir deseni bir giriş dizesiyle eşleştirmeye çalışırken zaman aşımına uğradı. Bu birçok nedenle oluşabilir; buna çok büyük girişler ya da iç içe geçmiş miktar belirleyiciler, geri başvurular ve diğer faktörler nedeniyle oluşan aşırı geri dönüşler dahildir. + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + RegEx altyapısı bir deseni bir giriş dizesiyle eşleştirmeye çalışırken zaman aşımına uğradı. Bu birçok nedenle oluşabilir; buna çok büyük girişler ya da iç içe geçmiş miktar belirleyiciler, geri başvurular ve diğer faktörler nedeniyle oluşan aşırı geri dönüşler dahildir. diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf index 0ae9d3f93eeaf3..755ac679ad99c4 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - 尝试将模式与输入字符串匹配时,RegEx 引擎超时。许多原因均可能导致出现这种情况,包括由嵌套限定符、反向引用和其他因素引起的大量输入或过度回溯。 + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + 尝试将模式与输入字符串匹配时,RegEx 引擎超时。许多原因均可能导致出现这种情况,包括由嵌套限定符、反向引用和其他因素引起的大量输入或过度回溯。 diff --git a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf index df103dd22f2886..885f95256b3779 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.RegularExpressions/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -203,8 +203,8 @@ {Locked="Int32.MaxValue"} - The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. - 嘗試將模式對應至輸入字串時,RegEx 引擎發生逾時。有很多原因會導致這個情形,其中包括巢狀數量詞、反向參考及其他因素所造成的極大量輸入或過度使用回溯法。 + The RegEx engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors. + 嘗試將模式對應至輸入字串時,RegEx 引擎發生逾時。有很多原因會導致這個情形,其中包括巢狀數量詞、反向參考及其他因素所造成的極大量輸入或過度使用回溯法。 diff --git a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs index 8fd757cc84e637..0db4a8836ada8a 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs @@ -7,6 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -24,6 +26,7 @@ public class UpgradeToRegexGeneratorAnalyzer : DiagnosticAnalyzer { private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; + private const string RegexOptionsTypeName = "System.Text.RegularExpressions.RegexOptions"; internal const string PatternIndexName = "PatternIndex"; internal const string RegexOptionsIndexName = "RegexOptionsIndex"; @@ -37,7 +40,7 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(compilationContext => + context.RegisterCompilationStartAction(async compilationContext => { Compilation compilation = compilationContext.Compilation; @@ -46,12 +49,48 @@ public override void Initialize(AnalysisContext context) return; } + if (await ProjectUsesTopLevelStatements(compilation, compilationContext.CancellationToken).ConfigureAwait(false)) + { + return; + } + + INamedTypeSymbol regexOptionsTypeSymbol = compilation.GetTypeByMetadataName(RegexOptionsTypeName); + if (regexOptionsTypeSymbol == null) + { + return; + } + + HashSet methodsWithFourParameters = GetMethodSymbolHash(compilation, regexTypeSymbol, + new HashSet { "Count", "EnumerateMatches", "IsMatch", "Match", "Matches", "Split" }); + + HashSet replaceStaticMethods = GetMethodSymbolHash(compilation, regexTypeSymbol, new HashSet { "Replace" }); + // Register analysis of calls to the Regex constructors - compilationContext.RegisterOperationAction(context => AnalyzeObjectCreation(context, regexTypeSymbol), OperationKind.ObjectCreation); + compilationContext.RegisterOperationAction(context => AnalyzeObjectCreation(context, regexTypeSymbol, regexOptionsTypeSymbol), OperationKind.ObjectCreation); // Register analysis of calls to Regex static methods - compilationContext.RegisterOperationAction(context => AnalyzeInvocation(context, regexTypeSymbol), OperationKind.Invocation); + compilationContext.RegisterOperationAction(context => AnalyzeInvocation(context, regexTypeSymbol, regexOptionsTypeSymbol, methodsWithFourParameters, replaceStaticMethods), OperationKind.Invocation); }); + + static HashSet GetMethodSymbolHash(Compilation compilation, INamedTypeSymbol regexTypeSymbol, HashSet methodNames) + { +#pragma warning disable RS1024 // Compare symbols correctly + HashSet hash = new HashSet(SymbolEqualityComparer.Default); +#pragma warning restore RS1024 // Compare symbols correctly + ImmutableArray allMembers = regexTypeSymbol.GetMembers(); + + foreach(ISymbol member in allMembers) + { + if (member is IMethodSymbol method && + method.IsStatic && + methodNames.Contains(method.Name)) + { + hash.Add(method); + } + } + + return hash; + } } /// @@ -59,7 +98,7 @@ public override void Initialize(AnalysisContext context) /// and checks if they could be using the source generator instead. /// /// The compilation context representing the invocation. - private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol) + private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol, INamedTypeSymbol regexOptionsTypeSymbol, HashSet staticMethodsWithFourParameters, HashSet replaceStaticMethods) { // Ensure the invocation is a Regex static method. IInvocationOperation invocationOperation = (IInvocationOperation)context.Operation; @@ -73,7 +112,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy // code fixer can later use that property bag to generate the code fix and emit the RegexGenerator attribute. // Most static methods have the same parameter overloads which are covered by the first if block. Replace static method takes extra parameters so that one // is treated specially. - if (method.Name is "IsMatch" or "Match" or "Matches" or "Split" or "Count" or "EnumerateMatches") + if (staticMethodsWithFourParameters.Contains(method)) { // if the static method invocation has a timeout, then don't emit a diagnostic. if (invocationOperation.Arguments.Length > 3) @@ -84,7 +123,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy for (int i = 1; i < invocationOperation.Arguments.Length; i++) { // Ensure that all inputs to the static method are constant. - if (!IsConstant(invocationOperation.Arguments[i])) + if (!IsConstant(invocationOperation.Arguments[i], regexOptionsTypeSymbol)) { return; } @@ -102,7 +141,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy Debug.Assert(syntaxNodeForDiagnostic != null); context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); } - else if (method.Name is "Replace") + else if (replaceStaticMethods.Contains(method)) { // if the static method invocation has a timeout, then don't emit a diagnostic. if (invocationOperation.Arguments.Length > 4) @@ -119,7 +158,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy } // Ensure that all inputs to the static method are constant. - if (!IsConstant(invocationOperation.Arguments[i])) + if (!IsConstant(invocationOperation.Arguments[i], regexOptionsTypeSymbol)) { return; } @@ -144,7 +183,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy /// and checks if they could be using the source generator instead. /// /// The object creation context. - private static void AnalyzeObjectCreation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol) + private static void AnalyzeObjectCreation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol, INamedTypeSymbol regexOptionsTypeSymbol) { // Ensure the object creation is a call to the Regex constructor. IObjectCreationOperation operation = (IObjectCreationOperation)context.Operation; @@ -162,7 +201,7 @@ private static void AnalyzeObjectCreation(OperationAnalysisContext context, INam // Ensure that all inputs to the constructor are constant. foreach (IArgumentOperation argument in operation.Arguments) { - if (!IsConstant(argument)) + if (!IsConstant(argument, regexOptionsTypeSymbol)) { return; } @@ -187,17 +226,38 @@ private static void AnalyzeObjectCreation(OperationAnalysisContext context, INam /// /// The argument to be analyzed. /// if the argument is constant; otherwise, . - private static bool IsConstant(IArgumentOperation argument) + private static bool IsConstant(IArgumentOperation argument, INamedTypeSymbol regexOptions) { IOperation valueOperation = argument.Value; if (valueOperation.ConstantValue.HasValue) { + // If using RegexOptions.NonBacktracking we should not emit a diagnostic. + if (SymbolEqualityComparer.Default.Equals(argument.Value.Type, regexOptions)) + { + RegexOptions value = (RegexOptions)((int)valueOperation.ConstantValue.Value); + if ((value & RegexOptions.NonBacktracking) > 0) + { + return false; + } + } + return true; } return false; } + private static async Task ProjectUsesTopLevelStatements(Compilation compilation, CancellationToken cancellationToken) + { + SyntaxNode? root = await compilation.SyntaxTrees.FirstOrDefault().GetRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + { + return false; + } + + return root.DescendantNodesAndSelf().Where(node => node.IsKind(SyntaxKind.GlobalStatement)).Any(); + } + /// /// Ensures that the compilation can find the Regex and RegexAttribute types, and also validates that the /// LangVersion of the project is >= 10.0 (which is the current requirement for the Regex source generator. diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs index 83ce1218adb7ce..648eaff1c202b3 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/CSharpCodeFixVerifier`2.cs @@ -92,11 +92,6 @@ public Test(ReferenceAssemblies? references, bool usePreviewLanguageVersion, int SolutionTransforms.Add((solution, projectId) => { - Microsoft.CodeAnalysis.CompilationOptions compilationOptions = solution.GetProject(projectId).CompilationOptions; - compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( - compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); - solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); - if (usePreviewLanguageVersion) { CSharpParseOptions parseOptions = solution.GetProject(projectId).ParseOptions as CSharpParseOptions; @@ -109,30 +104,4 @@ public Test(ReferenceAssemblies? references, bool usePreviewLanguageVersion, int } } } - - internal static class CSharpVerifierHelper - { - /// - /// By default, the compiler reports diagnostics for nullable reference types at - /// , and the analyzer test framework defaults to only validating - /// diagnostics at . This map contains all compiler diagnostic IDs - /// related to nullability mapped to , which is then used to enable all - /// of these warnings for default validation during analyzer and code fix tests. - /// - internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); - - private static ImmutableDictionary GetNullableWarningsFromCompiler() - { - string[] args = { "/warnaserror:nullable" }; - CSharpCommandLineArguments commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); - ImmutableDictionary nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; - - // Workaround for https://github.com/dotnet/roslyn/issues/41610 - nullableWarnings = nullableWarnings - .SetItem("CS8632", ReportDiagnostic.Error) - .SetItem("CS8669", ReportDiagnostic.Error); - - return nullableWarnings; - } - } } diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs index dc123275c84f5b..4c45d4a16a2446 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs @@ -620,6 +620,24 @@ public void Foo() await VerifyCS.VerifyCodeFixAsync(test, expectedDiagnostic, fixedSource); } + [Theory] + [MemberData(nameof(InvocationTypes))] + public async Task NoDiagnosticForRegexOptionsNonBacktracking(InvocationType invocationType) + { + string isMatchInvocation = invocationType == InvocationType.Constructor ? @".IsMatch("""")" : string.Empty; + string test = @"using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var isMatch = " + ConstructRegexInvocation(invocationType, "\"\"", "RegexOptions.IgnoreCase | RegexOptions.NonBacktracking") + isMatchInvocation + @"; + } +}"; + + await VerifyCS.VerifyAnalyzerAsync(test); + } + [Fact] public async Task AnayzerSupportsMultipleDiagnostics() { From c69ccaa8cacd4fc645c5ee79a43c4b1171cdfb4c Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 25 May 2022 12:50:23 -0700 Subject: [PATCH 5/6] Addressing PR Feedback --- .../gen/UpgradeToRegexGeneratorAnalyzer.cs | 186 +++++++++--------- .../gen/UpgradeToRegexGeneratorCodeFixer.cs | 3 +- .../UpgradeToRegexGeneratorAnalyzerTests.cs | 65 +++++- 3 files changed, 157 insertions(+), 97 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs index 0db4a8836ada8a..c40ef8dae7dd9f 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorAnalyzer.cs @@ -22,11 +22,10 @@ namespace System.Text.RegularExpressions.Generator /// If so, it will emit an informational diagnostic to suggest use the Regex Generator. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class UpgradeToRegexGeneratorAnalyzer : DiagnosticAnalyzer + public sealed class UpgradeToRegexGeneratorAnalyzer : DiagnosticAnalyzer { private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; - private const string RegexOptionsTypeName = "System.Text.RegularExpressions.RegexOptions"; internal const string PatternIndexName = "PatternIndex"; internal const string RegexOptionsIndexName = "RegexOptionsIndex"; @@ -44,36 +43,40 @@ public override void Initialize(AnalysisContext context) { Compilation compilation = compilationContext.Compilation; + // Validate that the project supports the Regex Source Generator based on target framework, + // language version, etc. if (!ProjectSupportsRegexSourceGenerator(compilation, out INamedTypeSymbol? regexTypeSymbol)) { return; } + // Validate that the project is not using top-level statements, since if it were, the code-fixer + // can't easily convert to the source generator without having to make the program not use top-level + // statements any longer. if (await ProjectUsesTopLevelStatements(compilation, compilationContext.CancellationToken).ConfigureAwait(false)) { return; } - INamedTypeSymbol regexOptionsTypeSymbol = compilation.GetTypeByMetadataName(RegexOptionsTypeName); - if (regexOptionsTypeSymbol == null) - { - return; - } - - HashSet methodsWithFourParameters = GetMethodSymbolHash(compilation, regexTypeSymbol, - new HashSet { "Count", "EnumerateMatches", "IsMatch", "Match", "Matches", "Split" }); - - HashSet replaceStaticMethods = GetMethodSymbolHash(compilation, regexTypeSymbol, new HashSet { "Replace" }); + // Pre-compute a hash with all of the method symbols that we want to analyze for possibly emitting + // a diagnostic. + HashSet staticMethodsToDetect = GetMethodSymbolHash(regexTypeSymbol, + new HashSet { "Count", "EnumerateMatches", "IsMatch", "Match", "Matches", "Split", "Replace" }); // Register analysis of calls to the Regex constructors - compilationContext.RegisterOperationAction(context => AnalyzeObjectCreation(context, regexTypeSymbol, regexOptionsTypeSymbol), OperationKind.ObjectCreation); + compilationContext.RegisterOperationAction(context => AnalyzeObjectCreation(context, regexTypeSymbol), OperationKind.ObjectCreation); // Register analysis of calls to Regex static methods - compilationContext.RegisterOperationAction(context => AnalyzeInvocation(context, regexTypeSymbol, regexOptionsTypeSymbol, methodsWithFourParameters, replaceStaticMethods), OperationKind.Invocation); + compilationContext.RegisterOperationAction(context => AnalyzeInvocation(context, regexTypeSymbol, staticMethodsToDetect), OperationKind.Invocation); }); - static HashSet GetMethodSymbolHash(Compilation compilation, INamedTypeSymbol regexTypeSymbol, HashSet methodNames) + // Creates a HashSet of all of the method Symbols containing the static methods to analyze. + static HashSet GetMethodSymbolHash(INamedTypeSymbol regexTypeSymbol, HashSet methodNames) { + // This warning is due to a false positive bug https://github.com/dotnet/roslyn-analyzers/issues/5804 + // This issue has now been fixed, but we are not yet consuming the fix and getting this package + // as a transitive dependency from Microsoft.CodeAnalysis.CSharp.Workspaces. Once that dependency + // is updated at the repo-level, we should come and remove the pragma disable. #pragma warning disable RS1024 // Compare symbols correctly HashSet hash = new HashSet(SymbolEqualityComparer.Default); #pragma warning restore RS1024 // Compare symbols correctly @@ -98,7 +101,7 @@ static HashSet GetMethodSymbolHash(Compilation compilation, IName /// and checks if they could be using the source generator instead. /// /// The compilation context representing the invocation. - private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol, INamedTypeSymbol regexOptionsTypeSymbol, HashSet staticMethodsWithFourParameters, HashSet replaceStaticMethods) + private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol, HashSet staticMethodsToDetect) { // Ensure the invocation is a Regex static method. IInvocationOperation invocationOperation = (IInvocationOperation)context.Operation; @@ -108,72 +111,29 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy return; } - // Depending on the static method being called, we need to save the parameters as properties so that we can save them onto the diagnostic so that the + // We need to save the parameters as properties so that we can save them onto the diagnostic so that the // code fixer can later use that property bag to generate the code fix and emit the RegexGenerator attribute. - // Most static methods have the same parameter overloads which are covered by the first if block. Replace static method takes extra parameters so that one - // is treated specially. - if (staticMethodsWithFourParameters.Contains(method)) + if (staticMethodsToDetect.Contains(method)) { - // if the static method invocation has a timeout, then don't emit a diagnostic. - if (invocationOperation.Arguments.Length > 3) - { - return; - } + string? patternArgumentIndex = null; + string? optionsArgumentIndex = null; - for (int i = 1; i < invocationOperation.Arguments.Length; i++) - { - // Ensure that all inputs to the static method are constant. - if (!IsConstant(invocationOperation.Arguments[i], regexOptionsTypeSymbol)) - { - return; - } - } - - // Create the property bag. - ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] - { - new KeyValuePair(PatternIndexName, "1"), - new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 2 ? "2" : null) - }); - - // Report the diagnostic. - SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax; - Debug.Assert(syntaxNodeForDiagnostic != null); - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); - } - else if (replaceStaticMethods.Contains(method)) - { - // if the static method invocation has a timeout, then don't emit a diagnostic. - if (invocationOperation.Arguments.Length > 4) + // Validate that arguments pattern and options are constant and timeout was not passed in. + if (!TryValidateParametersAndExtractArgumentIndices(invocationOperation.Arguments, ref patternArgumentIndex, ref optionsArgumentIndex)) { return; } - for (int i = 1; i < invocationOperation.Arguments.Length; i++) - { - // Skip the third parameter as that is the parameter to be used as replacement and doesn't affect the source generator. - if (i == 2) - { - continue; - } - - // Ensure that all inputs to the static method are constant. - if (!IsConstant(invocationOperation.Arguments[i], regexOptionsTypeSymbol)) - { - return; - } - } - // Create the property bag. ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] { - new KeyValuePair(PatternIndexName, "1"), - new KeyValuePair(RegexOptionsIndexName, invocationOperation.Arguments.Length > 3 ? "3" : null) + new KeyValuePair(PatternIndexName, patternArgumentIndex), + new KeyValuePair(RegexOptionsIndexName, optionsArgumentIndex) }); // Report the diagnostic. SyntaxNode? syntaxNodeForDiagnostic = invocationOperation.Syntax; - Debug.Assert(syntaxNodeForDiagnostic is not null); + Debug.Assert(syntaxNodeForDiagnostic != null); context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseRegexSourceGeneration, syntaxNodeForDiagnostic.GetLocation(), properties)); } } @@ -183,7 +143,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy /// and checks if they could be using the source generator instead. /// /// The object creation context. - private static void AnalyzeObjectCreation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol, INamedTypeSymbol regexOptionsTypeSymbol) + private static void AnalyzeObjectCreation(OperationAnalysisContext context, INamedTypeSymbol regexTypeSymbol) { // Ensure the object creation is a call to the Regex constructor. IObjectCreationOperation operation = (IObjectCreationOperation)context.Operation; @@ -198,20 +158,19 @@ private static void AnalyzeObjectCreation(OperationAnalysisContext context, INam return; } - // Ensure that all inputs to the constructor are constant. - foreach (IArgumentOperation argument in operation.Arguments) + string? patternArgumentIndex = null; + string? optionsArgumentIndex = null; + + if (!TryValidateParametersAndExtractArgumentIndices(operation.Arguments, ref patternArgumentIndex, ref optionsArgumentIndex)) { - if (!IsConstant(argument, regexOptionsTypeSymbol)) - { - return; - } + return; } // Create the property bag. ImmutableDictionary properties = ImmutableDictionary.CreateRange(new[] { - new KeyValuePair(PatternIndexName, "0"), - new KeyValuePair(RegexOptionsIndexName, operation.Arguments.Length > 1 ? "1" : null) + new KeyValuePair(PatternIndexName, patternArgumentIndex), + new KeyValuePair(RegexOptionsIndexName, optionsArgumentIndex) }); // Report the diagnostic. @@ -221,32 +180,79 @@ private static void AnalyzeObjectCreation(OperationAnalysisContext context, INam } /// - /// Ensures that the input to the constructor or invocation is constant at compile time - /// which is a requirement in order to be able to use the source generator. + /// Validates the operation arguments ensuring they all have constant values, and if so it stores the argument + /// indices for the pattern and options. If timeout argument was used, then this returns false. /// - /// The argument to be analyzed. - /// if the argument is constant; otherwise, . - private static bool IsConstant(IArgumentOperation argument, INamedTypeSymbol regexOptions) + private static bool TryValidateParametersAndExtractArgumentIndices(ImmutableArray arguments, ref string? patternArgumentIndex, ref string? optionsArgumentIndex) { - IOperation valueOperation = argument.Value; - if (valueOperation.ConstantValue.HasValue) + const string timeoutArgumentName = "timeout"; + const string matchTimeoutArgumentName = "matchTimeout"; + const string patternArgumentName = "pattern"; + const string optionsArgumentName = "options"; + + if (arguments == null) + { + return false; + } + + for (int i = 0; i < arguments.Length; i++) { - // If using RegexOptions.NonBacktracking we should not emit a diagnostic. - if (SymbolEqualityComparer.Default.Equals(argument.Value.Type, regexOptions)) + IArgumentOperation argument = arguments[i]; + string argumentName = argument.Parameter.Name; + + // If one of the arguments is a timeout, then we don't emit a diagnostic. + if (argumentName.Equals(timeoutArgumentName, StringComparison.OrdinalIgnoreCase) || + argumentName.Equals(matchTimeoutArgumentName, StringComparison.OrdinalIgnoreCase)) { - RegexOptions value = (RegexOptions)((int)valueOperation.ConstantValue.Value); - if ((value & RegexOptions.NonBacktracking) > 0) + return false; + } + + // If the argument is the pattern, then we validate that it is constant and we store the index. + if (argumentName.Equals(patternArgumentName, StringComparison.OrdinalIgnoreCase)) + { + if (!IsConstant(argument)) { return false; } + + patternArgumentIndex = i.ToString(); + continue; } - return true; + // If the argument is the options, then we validate that it is constant, that it doesn't have RegexOptions.NonBacktracking, and we store the index. + if (argumentName.Equals(optionsArgumentName, StringComparison.OrdinalIgnoreCase)) + { + if (!IsConstant(argument)) + { + return false; + } + + RegexOptions value = (RegexOptions)((int)argument.Value.ConstantValue.Value); + if ((value & RegexOptions.NonBacktracking) > 0) + { + return false; + } + + optionsArgumentIndex = i.ToString(); + continue; + } } - return false; + return true; } + /// + /// Ensures that the input to the constructor or invocation is constant at compile time + /// which is a requirement in order to be able to use the source generator. + /// + /// The argument to be analyzed. + /// if the argument is constant; otherwise, . + private static bool IsConstant(IArgumentOperation argument) + => argument.Value.ConstantValue.HasValue; + + /// + /// Detects whether or not the current project is using top-level statements. + /// private static async Task ProjectUsesTopLevelStatements(Compilation compilation, CancellationToken cancellationToken) { SyntaxNode? root = await compilation.SyntaxTrees.FirstOrDefault().GetRootAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs index a5c4df1d4b3891..105bc2dcb3d3b8 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/UpgradeToRegexGeneratorCodeFixer.cs @@ -25,7 +25,7 @@ namespace System.Text.RegularExpressions.Generator /// source generation. /// [ExportCodeFixProvider(LanguageNames.CSharp)] - public class UpgradeToRegexGeneratorCodeFixer : CodeFixProvider + public sealed class UpgradeToRegexGeneratorCodeFixer : CodeFixProvider { private const string RegexTypeName = "System.Text.RegularExpressions.Regex"; private const string RegexGeneratorTypeName = "System.Text.RegularExpressions.RegexGeneratorAttribute"; @@ -47,7 +47,6 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) } SyntaxNode nodeToFix = root.FindNode(context.Span, getInnermostNodeForTie: false); - if (nodeToFix is null) { return; diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs index 4c45d4a16a2446..bd93f5293af824 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs @@ -22,10 +22,9 @@ public class UpgradeToRegexGeneratorAnalyzerTests public async Task NoDiagnosticsForEmpty() => await VerifyCS.VerifyAnalyzerAsync(source: string.Empty); - [Fact] - public async Task NoDiagnosticForConstructorWithTimeout() + public static IEnumerable ConstructorWithTimeoutTestData() { - string test = @"using System; + yield return new object[] { @"using System; using System.Text.RegularExpressions; public class Program @@ -34,9 +33,35 @@ public static void Main(string[] args) { var regex = new Regex("""", RegexOptions.None, TimeSpan.FromSeconds(10)); } -} -"; +}" }; + + yield return new object[] { @"using System; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var regex = new Regex("""", timeout: TimeSpan.FromSeconds(10)); + } +}" }; + + yield return new object[] { @"using System; +using System.Text.RegularExpressions; + +public class Program +{ + public static void Main(string[] args) + { + var regex = new Regex(timeout: TimeSpan.FromSeconds(10), pattern: """"); + } +}" }; + } + [Theory] + [MemberData(nameof(ConstructorWithTimeoutTestData))] + public async Task NoDiagnosticForConstructorWithTimeout(string test) + { await VerifyCS.VerifyAnalyzerAsync(test); } @@ -678,6 +703,36 @@ public static void Main() await VerifyCS.VerifyCodeFixAsync(test, expectedDiagnostics, fixedSource, 2); } + [Fact] + public async Task CodeFixerSupportsNamedParameters() + { + string test = @"using System.Text.RegularExpressions; + +class Program +{ + static void Main(string[] args) + { + Regex r = {|#0:new Regex(options: RegexOptions.None, pattern: ""a|b"")|}; + } +}"; + DiagnosticResult expectedDiagnostic = VerifyCS.Diagnostic(DiagnosticDescriptors.UseRegexSourceGeneration.Id).WithLocation(0); + + string fixedSource = @"using System.Text.RegularExpressions; + +partial class Program +{ + static void Main(string[] args) + { + Regex r = MyRegex(); + } + + [RegexGenerator(""a|b"", RegexOptions.None)] + private static partial Regex MyRegex(); +}"; + + await VerifyCS.VerifyCodeFixAsync(test, expectedDiagnostic, fixedSource); + } + #region Test helpers private static string ConstructRegexInvocation(InvocationType invocationType, string pattern, string? options = null) From 6cb1eb7174b73fb54e7f80e897885c2f20af3c39 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 25 May 2022 14:06:44 -0700 Subject: [PATCH 6/6] Disabling the tests for Mono --- .../UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj | 2 +- .../tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj index 77f1ad9eb8adec..f2e24cd806da39 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/System.Text.RegularExpressions.Unit.Tests.csproj @@ -9,7 +9,7 @@ true true $(DefineConstants);DEBUG - true + true diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs index bd93f5293af824..5ffe667edd962e 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/UpgradeToRegexGeneratorAnalyzerTests.cs @@ -16,6 +16,7 @@ namespace System.Text.RegularExpressions.Unit.Tests { + [ActiveIssue("https://github.com/dotnet/runtime/issues/69823", TestRuntimes.Mono)] public class UpgradeToRegexGeneratorAnalyzerTests { [Fact]