Пример #1
0
        public void FoundOptionsInPatternIsCorrect(string pattern, RegexOptions expectedOptions)
        {
            RegexOptions foundOptions = RegexParser.ParseOptionsInPattern(pattern, RegexOptions.None);

            Assert.Equal(expectedOptions, foundOptions);
        }
Пример #2
0
        /// <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 },
Пример #3
0
        // 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);