public void FoundOptionsInPatternIsCorrect(string pattern, RegexOptions expectedOptions) { RegexOptions foundOptions = RegexParser.ParseOptionsInPattern(pattern, RegexOptions.None); Assert.Equal(expectedOptions, foundOptions); }
/// <summary> /// Takes a <see cref="Document"/> and a <see cref="Diagnostic"/> and returns a new <see cref="Document"/> with the replaced /// nodes in order to apply the code fix to the diagnostic. /// </summary> /// <param name="document">The original document.</param> /// <param name="root">The root of the syntax tree.</param> /// <param name="nodeToFix">The node to fix. This is where the diagnostic was produced.</param> /// <param name="diagnostic">The diagnostic to fix.</param> /// <param name="cancellationToken">The cancellation token for the async operation.</param> /// <returns>The new document with the replaced nodes after applying the code fix.</returns> private static async Task <Document> ConvertToSourceGenerator(Document document, SyntaxNode root, SyntaxNode nodeToFix, 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 GeneratedRegexAttribute types. INamedTypeSymbol?regexSymbol = compilation.GetTypeByMetadataName(RegexTypeName); INamedTypeSymbol?generatedRegexAttributeSymbol = compilation.GetTypeByMetadataName(GeneratedRegexTypeName); if (regexSymbol is null || generatedRegexAttributeSymbol is null) { return(document); } // 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 Regex 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. SyntaxNode?typeDeclarationOrCompilationUnit = nodeToFix.Ancestors().OfType <TypeDeclarationSyntax>().FirstOrDefault(); if (typeDeclarationOrCompilationUnit is null) { typeDeclarationOrCompilationUnit = await nodeToFix.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); } // Calculate what name should be used for the generated static partial method string methodName = DefaultRegexMethodName; INamedTypeSymbol?typeSymbol = typeDeclarationOrCompilationUnit is TypeDeclarationSyntax typeDeclaration? semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) : semanticModel.GetDeclaredSymbol((CompilationUnitSyntax)typeDeclarationOrCompilationUnit, cancellationToken)?.ContainingType; if (typeSymbol is not null) { IEnumerable <ISymbol> 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 <TypeDeclarationSyntax>(), (_, 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), getInnermostNodeForTie: true); if (nodeToFix is null) { return(document); } // We need to find the typeDeclaration again, but now using the new root. typeDeclarationOrCompilationUnit = typeDeclarationOrCompilationUnit is TypeDeclarationSyntax? nodeToFix.Ancestors().OfType <TypeDeclarationSyntax>().FirstOrDefault() : await nodeToFix.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); Debug.Assert(typeDeclarationOrCompilationUnit is not null); SyntaxNode newTypeDeclarationOrCompilationUnit = typeDeclarationOrCompilationUnit; // 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; // Generate the modified type declaration depending on whether the callsite was a Regex constructor call // or a Regex static method invocation. SyntaxNode replacement = generator.InvocationExpression(generator.IdentifierName(methodName)); ImmutableArray <IArgumentOperation> operationArguments; if (operation is IInvocationOperation invocationOperation) // When using a Regex static method { operationArguments = invocationOperation.Arguments; IEnumerable <SyntaxNode> arguments = operationArguments .Where(arg => arg.Parameter.Name is not(UpgradeToGeneratedRegexAnalyzer.OptionsArgumentName or UpgradeToGeneratedRegexAnalyzer.PatternArgumentName)) .Select(arg => arg.Syntax); replacement = generator.InvocationExpression(generator.MemberAccessExpression(replacement, invocationOperation.TargetMethod.Name), arguments); } else { operationArguments = ((IObjectCreationOperation)operation).Arguments; } newTypeDeclarationOrCompilationUnit = newTypeDeclarationOrCompilationUnit.ReplaceNode(nodeToFix, WithTrivia(replacement, nodeToFix)); // Initialize the inputs for the GeneratedRegex attribute. SyntaxNode?patternValue = GetNode(operationArguments, generator, UpgradeToGeneratedRegexAnalyzer.PatternArgumentName); SyntaxNode?regexOptionsValue = GetNode(operationArguments, generator, UpgradeToGeneratedRegexAnalyzer.OptionsArgumentName); // 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())); // We now need to check if we have to pass in the cultureName parameter. This parameter will be required in case the option // RegexOptions.IgnoreCase is set for this Regex. To determine that, we first get the passed in options (if any), and then, // we also need to parse the pattern in case there are options that were specified inside the pattern via the `(?i)` switch. SyntaxNode? cultureNameValue = null; RegexOptions regexOptions = regexOptionsValue is not null?GetRegexOptionsFromArgument(operationArguments) : RegexOptions.None; string pattern = GetRegexPatternFromArgument(operationArguments); regexOptions |= RegexParser.ParseOptionsInPattern(pattern, regexOptions); // If the options include IgnoreCase and don't specify CultureInvariant then we will have to calculate the user's current culture in order to pass // it in as a parameter. If the user specified IgnoreCase, but also selected CultureInvariant, then we skip as the default is to use Invariant culture. if ((regexOptions & RegexOptions.IgnoreCase) != 0 && (regexOptions & RegexOptions.CultureInvariant) == 0) { // If CultureInvariant wasn't specified as options, we default to the current culture. cultureNameValue = generator.LiteralExpression(CultureInfo.CurrentCulture.Name); // If options weren't passed in, then we need to define it as well in order to use the three parameter constructor. if (regexOptionsValue is null) { regexOptionsValue = generator.MemberAccessExpression(SyntaxFactory.IdentifierName("RegexOptions"), "None"); } } // Generate the GeneratedRegex attribute syntax node with the specified parameters. SyntaxNode attributes = generator.Attribute(generator.TypeExpression(generatedRegexAttributeSymbol), attributeArguments: (patternValue, regexOptionsValue, cultureNameValue) switch { ({ }, null, null) => new[] { patternValue },
// Returns null if nothing to do, Diagnostic if there's an error to report, or RegexType if the type was analyzed successfully. private static object?GetSemanticTargetForGeneration( GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; SemanticModel sm = context.SemanticModel; Compilation compilation = sm.Compilation; INamedTypeSymbol?regexSymbol = compilation.GetBestTypeByMetadataName(RegexName); INamedTypeSymbol?generatedRegexAttributeSymbol = compilation.GetBestTypeByMetadataName(GeneratedRegexAttributeName); if (regexSymbol is null || generatedRegexAttributeSymbol is null) { // Required types aren't available return(null); } TypeDeclarationSyntax?typeDec = methodSyntax.Parent as TypeDeclarationSyntax; if (typeDec is null) { return(null); } IMethodSymbol regexMethodSymbol = context.TargetSymbol as IMethodSymbol; if (regexMethodSymbol is null) { return(null); } ImmutableArray <AttributeData>?boundAttributes = regexMethodSymbol.GetAttributes(); if (boundAttributes is null || boundAttributes.Value.Length == 0) { return(null); } bool attributeFound = false; string?pattern = null; int? options = null; int? matchTimeout = null; string?cultureName = string.Empty; foreach (AttributeData attributeData in boundAttributes) { if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, generatedRegexAttributeSymbol)) { continue; } if (attributeData.ConstructorArguments.Any(ca => ca.Kind == TypedConstantKind.Error)) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, methodSyntax.GetLocation())); } if (pattern is not null) { return(Diagnostic.Create(DiagnosticDescriptors.MultipleGeneratedRegexAttributes, methodSyntax.GetLocation())); } ImmutableArray <TypedConstant> items = attributeData.ConstructorArguments; if (items.Length == 0 || items.Length > 4) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidGeneratedRegexAttribute, methodSyntax.GetLocation())); } attributeFound = true; pattern = items[0].Value as string; if (items.Length >= 2) { options = items[1].Value as int?; if (items.Length == 4) { matchTimeout = items[2].Value as int?; cultureName = items[3].Value as string; } // If there are 3 parameters, we need to check if the third argument is // int matchTimeoutMilliseconds, or string cultureName. else if (items.Length == 3) { if (items[2].Type.SpecialType == SpecialType.System_Int32) { matchTimeout = items[2].Value as int?; } else { cultureName = items[2].Value as string; } } } } if (!attributeFound) { return(null); } if (pattern is null || cultureName is null) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, methodSyntax.GetLocation(), "(null)")); } if (!regexMethodSymbol.IsPartialDefinition || regexMethodSymbol.IsAbstract || regexMethodSymbol.Parameters.Length != 0 || regexMethodSymbol.Arity != 0 || !SymbolEqualityComparer.Default.Equals(regexMethodSymbol.ReturnType, regexSymbol)) { return(Diagnostic.Create(DiagnosticDescriptors.RegexMethodMustHaveValidSignature, methodSyntax.GetLocation())); } RegexOptions regexOptions = options is not null ? (RegexOptions)options : RegexOptions.None; // If RegexOptions.IgnoreCase was specified or the inline ignore case option `(?i)` is present in the pattern, then we will (in priority order): // - If a culture name was passed in: // - If RegexOptions.CultureInvariant was also passed in, then we emit a diagnostic due to the explicit conflict. // - We try to initialize a culture using the passed in culture name to be used for case-sensitive comparisons. If // the culture name is invalid, we'll emit a diagnostic. // - Default to use Invariant Culture if no culture name was passed in. CultureInfo culture = CultureInfo.InvariantCulture; RegexOptions regexOptionsWithPatternOptions; try { regexOptionsWithPatternOptions = regexOptions | RegexParser.ParseOptionsInPattern(pattern, regexOptions); } catch (Exception e) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, methodSyntax.GetLocation(), e.Message)); } if ((regexOptionsWithPatternOptions & RegexOptions.IgnoreCase) != 0 && !string.IsNullOrEmpty(cultureName)) { if ((regexOptions & RegexOptions.CultureInvariant) != 0) { // User passed in both a culture name and set RegexOptions.CultureInvariant which causes an explicit conflict. return(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, methodSyntax.GetLocation(), "cultureName")); } try { culture = CultureInfo.GetCultureInfo(cultureName); } catch (CultureNotFoundException) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, methodSyntax.GetLocation(), "cultureName")); } } // Validate the options const RegexOptions SupportedOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ECMAScript | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.NonBacktracking | RegexOptions.RightToLeft | RegexOptions.Singleline; if ((regexOptions & ~SupportedOptions) != 0) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, methodSyntax.GetLocation(), "options")); } // Validate the timeout if (matchTimeout is 0 or < -1) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, methodSyntax.GetLocation(), "matchTimeout")); } // Parse the input pattern RegexTree regexTree; AnalysisResults analysis; try { regexTree = RegexParser.Parse(pattern, regexOptions | RegexOptions.Compiled, culture); // make sure Compiled is included to get all optimizations applied to it analysis = RegexTreeAnalyzer.Analyze(regexTree); } catch (Exception e) { return(Diagnostic.Create(DiagnosticDescriptors.InvalidRegexArguments, methodSyntax.GetLocation(), e.Message)); } // Determine the namespace the class is declared in, if any string?ns = regexMethodSymbol.ContainingType?.ContainingNamespace?.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)); var regexType = new RegexType( typeDec is RecordDeclarationSyntax rds ? $"{typeDec.Keyword.ValueText} {rds.ClassOrStructKeyword}" : typeDec.Keyword.ValueText, ns ?? string.Empty, $"{typeDec.Identifier}{typeDec.TypeParameterList}"); var regexMethod = new RegexMethod( regexType, methodSyntax, regexMethodSymbol.Name, methodSyntax.Modifiers.ToString(), pattern, regexOptions, matchTimeout, regexTree, analysis); RegexType current = regexType; var parent = typeDec.Parent as TypeDeclarationSyntax; while (parent is not null && IsAllowedKind(parent.Kind())) { current.Parent = new RegexType( parent is RecordDeclarationSyntax rds2 ? $"{parent.Keyword.ValueText} {rds2.ClassOrStructKeyword}" : parent.Keyword.ValueText, ns ?? string.Empty, $"{parent.Identifier}{parent.TypeParameterList}"); current = current.Parent; parent = parent.Parent as TypeDeclarationSyntax; } return(regexMethod);