File: Microsoft.NetCore.Analyzers\Runtime\CSharpAvoidRedundantRegexIsMatchBeforeMatch.Fixer.cs
Web Access
Project: src\src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.CSharp.NetAnalyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.csproj (Microsoft.CodeAnalysis.CSharp.NetAnalyzers)
// 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.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
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.Formatting;
using Microsoft.NetCore.Analyzers;
using Microsoft.NetCore.Analyzers.Runtime;

namespace Microsoft.NetCore.CSharp.Analyzers.Runtime
{
    /// <summary>
    /// C#-specific fixer for <see cref="AvoidRedundantRegexIsMatchBeforeMatch"/>.
    /// Transforms:
    /// <code>
    /// if (Regex.IsMatch(input, pattern))
    /// {
    ///     Match m = Regex.Match(input, pattern);
    ///     // use m
    /// }
    /// </code>
    /// Into:
    /// <code>
    /// if (Regex.Match(input, pattern) is { Success: true } m)
    /// {
    ///     // use m
    /// }
    /// </code>
    /// </summary>
    [ExportCodeFixProvider(LanguageNames.CSharp), Shared]
    public sealed class CSharpAvoidRedundantRegexIsMatchBeforeMatchFixer : CodeFixProvider
    {
        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
            ImmutableArray.Create(AvoidRedundantRegexIsMatchBeforeMatch.RuleId);

        public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var diagnostic = context.Diagnostics[0];

            Document doc = context.Document;
            SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

            // Require C# 8.0+ for property patterns (is { Success: true } m)
            if (root.SyntaxTree.Options is CSharpParseOptions parseOptions &&
                parseOptions.LanguageVersion < LanguageVersion.CSharp8)
            {
                return;
            }

            // Find the IsMatch invocation from the primary diagnostic location.
            if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode isMatchNode)
            {
                return;
            }

            // Find the Match invocation from the additional location.
            if (diagnostic.AdditionalLocations.Count < 1)
            {
                return;
            }

            var matchLocation = diagnostic.AdditionalLocations[0];
            if (root.FindNode(matchLocation.SourceSpan, getInnermostNodeForTie: true) is not SyntaxNode matchNode)
            {
                return;
            }

            // The IsMatch call must be the condition of an if statement.
            var ifStatement = isMatchNode.FirstAncestorOrSelf<IfStatementSyntax>();
            if (ifStatement is null)
            {
                return;
            }

            SemanticModel model = await doc.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);

            // Path 1: Match m = Regex.Match(...); — local declaration in if body
            var matchDeclarationStatement = matchNode.FirstAncestorOrSelf<LocalDeclarationStatementSyntax>();
            if (matchDeclarationStatement is not null)
            {
                TryRegisterDeclarationFix(context, diagnostic, doc, model, ifStatement, matchDeclarationStatement, matchNode);
                return;
            }

            // Path 2: m = Regex.Match(...); — assignment to pre-declared variable
            var assignmentExpression = matchNode.FirstAncestorOrSelf<AssignmentExpressionSyntax>();
            if (assignmentExpression is not null)
            {
                TryRegisterAssignmentFix(context, diagnostic, doc, model, ifStatement, assignmentExpression, matchNode);
            }
        }

        /// <summary>
        /// Path 1: The Match result is assigned via a local declaration in the if body:
        /// <c>Match m = Regex.Match(...);</c>
        /// </summary>
        private static void TryRegisterDeclarationFix(
            CodeFixContext context,
            Diagnostic diagnostic,
            Document doc,
            SemanticModel model,
            IfStatementSyntax ifStatement,
            LocalDeclarationStatementSyntax matchDeclarationStatement,
            SyntaxNode matchNode)
        {
            var declaration = matchDeclarationStatement.Declaration;
            if (declaration.Variables.Count != 1)
            {
                return;
            }

            var declarator = declaration.Variables[0];
            if (declarator.Initializer?.Value is null)
            {
                return;
            }

            // Verify the initializer is exactly the Match invocation reported by the analyzer
            // (unwrapping any parentheses/casts). If the Match call is embedded inside a larger
            // expression (e.g., SomeMethod(Regex.Match(...))), the fix would change semantics.
            if (!IsMatchNode(declarator.Initializer.Value, matchNode))
            {
                return;
            }

            // Only apply fixer when the declared type is 'var' or exactly
            // System.Text.RegularExpressions.Match. If the user wrote a wider type
            // (e.g., Group, Capture, object), the pattern variable would change
            // the static type and could alter overload resolution.
            if (!IsVarOrMatchType(declaration.Type, model, context.CancellationToken))
            {
                return;
            }

            string variableName = declarator.Identifier.ValueText;

            // Only apply fixer when the Match declaration is the first executable statement
            // in the if body. If there are preceding statements, moving Match into the
            // condition would change their execution order relative to the Match call.
            if (ifStatement.Statement is BlockSyntax block)
            {
                var firstStatement = block.Statements.FirstOrDefault();
                if (firstStatement != matchDeclarationStatement)
                {
                    return;
                }
            }

            if (!PassesNameConflictChecks(ifStatement, variableName))
            {
                return;
            }

            string title = MicrosoftNetCoreAnalyzersResources.AvoidRedundantRegexIsMatchBeforeMatchFix;
            context.RegisterCodeFix(
                CodeAction.Create(
                    title,
                    createChangedDocument: ct => ApplyDeclarationFixAsync(doc, ifStatement, matchDeclarationStatement, variableName, ct),
                    equivalenceKey: title),
                diagnostic);
        }

        /// <summary>
        /// Path 2: The Match result is assigned to a pre-declared variable in the if body:
        /// <c>Match m; if (IsMatch(...)) { m = Regex.Match(...); }</c>
        /// Transforms to: <c>if (Regex.Match(...) is { Success: true } m) { }</c>
        /// Only applies when the pre-existing declaration is immediately before the if
        /// and the variable is not referenced after the if statement.
        /// </summary>
        private static void TryRegisterAssignmentFix(
            CodeFixContext context,
            Diagnostic diagnostic,
            Document doc,
            SemanticModel model,
            IfStatementSyntax ifStatement,
            AssignmentExpressionSyntax assignmentExpression,
            SyntaxNode matchNode)
        {
            // The left side must be a simple identifier (the variable being assigned).
            if (assignmentExpression.Left is not IdentifierNameSyntax identName)
            {
                return;
            }

            // Verify the right side is exactly the Match invocation.
            if (!IsMatchNode(assignmentExpression.Right, matchNode))
            {
                return;
            }

            // The assignment must be in an expression statement.
            var assignmentStatement = assignmentExpression.FirstAncestorOrSelf<ExpressionStatementSyntax>();
            if (assignmentStatement is null)
            {
                return;
            }

            // The assignment statement must be the first statement in a block body.
            if (ifStatement.Statement is not BlockSyntax block ||
                block.Statements.FirstOrDefault() != assignmentStatement)
            {
                return;
            }

            // The if must be inside a block so we can find the preceding declaration.
            if (ifStatement.Parent is not BlockSyntax parentBlock)
            {
                return;
            }

            string variableName = identName.Identifier.ValueText;

            // Find the pre-existing declaration of the variable immediately before the if.
            int ifIndex = parentBlock.Statements.IndexOf(ifStatement);
            if (ifIndex <= 0)
            {
                return;
            }

            if (parentBlock.Statements[ifIndex - 1] is not LocalDeclarationStatementSyntax preDecl)
            {
                return;
            }

            if (preDecl.Declaration.Variables.Count != 1)
            {
                return;
            }

            var preVar = preDecl.Declaration.Variables[0];
            if (preVar.Identifier.ValueText != variableName)
            {
                return;
            }

            // The pre-existing declaration must have no initializer, or be initialized
            // to a constant default expression (`null`, `default`, or `default(T)`) so
            // removing it doesn't lose meaningful computation.
            if (preVar.Initializer is not null)
            {
                var initValue = preVar.Initializer.Value;
                bool acceptable = initValue switch
                {
                    LiteralExpressionSyntax literal =>
                        literal.IsKind(SyntaxKind.NullLiteralExpression) ||
                        literal.IsKind(SyntaxKind.DefaultLiteralExpression),
                    DefaultExpressionSyntax => true,
                    _ => false,
                };

                if (!acceptable)
                {
                    return;
                }
            }

            // Verify the declared type is 'var' or exactly Match.
            if (!IsVarOrMatchType(preDecl.Declaration.Type, model, context.CancellationToken))
            {
                return;
            }

            // The variable must not be referenced in any statement after the if
            // or in the else branch, because the pattern variable won't be
            // definitely assigned there.
            if (IsVariableReferencedAfterIf(parentBlock, ifIndex, variableName) ||
                IsVariableReferencedInElse(ifStatement, variableName))
            {
                return;
            }

            if (!PassesNameConflictChecks(ifStatement, variableName))
            {
                return;
            }

            string title = MicrosoftNetCoreAnalyzersResources.AvoidRedundantRegexIsMatchBeforeMatchFix;
            context.RegisterCodeFix(
                CodeAction.Create(
                    title,
                    createChangedDocument: ct => ApplyAssignmentFixAsync(doc, ifStatement, preDecl, assignmentStatement, variableName, ct),
                    equivalenceKey: title),
                diagnostic);
        }

        /// <summary>
        /// Checks whether <paramref name="expression"/> is exactly the Match invocation
        /// <paramref name="matchNode"/> after unwrapping parentheses and casts.
        /// </summary>
        private static bool IsMatchNode(ExpressionSyntax expression, SyntaxNode matchNode)
        {
            SyntaxNode core = expression;
            while (core is ParenthesizedExpressionSyntax parenExpr)
            {
                core = parenExpr.Expression;
            }

            while (core is CastExpressionSyntax castExpr)
            {
                core = castExpr.Expression;
            }

            return core.Span.Equals(matchNode.Span);
        }

        /// <summary>
        /// Returns true when the type syntax is <c>var</c> or exactly
        /// <c>System.Text.RegularExpressions.Match</c>.
        /// </summary>
        private static bool IsVarOrMatchType(
            TypeSyntax typeSyntax, SemanticModel model, CancellationToken cancellationToken)
        {
            if (typeSyntax.IsVar)
            {
                return true;
            }

            var typeInfo = model.GetTypeInfo(typeSyntax, cancellationToken);
            var matchType = model.Compilation.GetTypeByMetadataName("System.Text.RegularExpressions.Match");
            return typeInfo.Type is not null &&
                   matchType is not null &&
                   SymbolEqualityComparer.Default.Equals(typeInfo.Type, matchType);
        }

        /// <summary>
        /// Returns true when the variable name doesn't conflict with bindings in else
        /// branches or subsequent sibling statements.
        /// </summary>
        private static bool PassesNameConflictChecks(
            IfStatementSyntax ifStatement, string variableName)
        {
            if (ifStatement.Else is not null &&
                HasConflictingName(ifStatement.Else, variableName))
            {
                return false;
            }

            if (HasConflictingNameInSubsequentSiblings(ifStatement, variableName))
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Checks whether the variable is referenced in any statement after
        /// <paramref name="ifIndex"/> in the parent block.
        /// </summary>
        private static bool IsVariableReferencedAfterIf(
            BlockSyntax parentBlock, int ifIndex, string variableName)
        {
            for (int i = ifIndex + 1; i < parentBlock.Statements.Count; i++)
            {
                if (ContainsIdentifierReference(parentBlock.Statements[i], variableName))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsVariableReferencedInElse(
            IfStatementSyntax ifStatement, string variableName)
        {
            if (ifStatement.Else is null)
            {
                return false;
            }

            return ContainsIdentifierReference(ifStatement.Else, variableName);
        }

        /// <summary>
        /// Returns true when <paramref name="node"/> contains an identifier that
        /// could plausibly bind to a local named <paramref name="variableName"/>.
        /// Excludes identifiers that are syntactically the name of a member access
        /// (e.g. <c>obj.m</c>) or a qualified name suffix, since those bind to a
        /// member rather than the local. Conservative — does not consult the
        /// semantic model, so it can still over-report (e.g. type names, nameof).
        /// </summary>
        private static bool ContainsIdentifierReference(SyntaxNode node, string variableName)
        {
            foreach (var id in node.DescendantNodesAndSelf().OfType<IdentifierNameSyntax>())
            {
                if (id.Identifier.ValueText != variableName)
                {
                    continue;
                }

                // Skip `something.m` (right-hand side of a member access) — `m` here is
                // a member name, not a reference to the local.
                if (id.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == id)
                {
                    continue;
                }

                // Skip `Foo.m` where `m` is the right-hand side of a qualified name.
                if (id.Parent is QualifiedNameSyntax qualified && qualified.Right == id)
                {
                    continue;
                }

                // Skip `M(m: value)` — `m` here is a parameter name label, not a reference.
                if (id.Parent is NameColonSyntax nameColon && nameColon.Name == id)
                {
                    continue;
                }

                return true;
            }

            return false;
        }

        /// <summary>
        /// Applies the fix for Path 1 (local declaration in if body).
        /// </summary>
        private static async Task<Document> ApplyDeclarationFixAsync(
            Document document,
            IfStatementSyntax ifStatement,
            LocalDeclarationStatementSyntax matchDeclarationStatement,
            string variableName,
            CancellationToken cancellationToken)
        {
            var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
            var matchCallExpression = matchDeclarationStatement.Declaration.Variables[0].Initializer!.Value;

            editor.ReplaceNode(ifStatement.Condition, BuildIsPatternCondition(ifStatement, matchCallExpression, variableName));
            editor.RemoveNode(matchDeclarationStatement);

            return editor.GetChangedDocument();
        }

        /// <summary>
        /// Applies the fix for Path 2 (assignment to pre-declared variable).
        /// </summary>
        private static async Task<Document> ApplyAssignmentFixAsync(
            Document document,
            IfStatementSyntax ifStatement,
            LocalDeclarationStatementSyntax preDeclaration,
            ExpressionStatementSyntax assignmentStatement,
            string variableName,
            CancellationToken cancellationToken)
        {
            var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
            var assignmentExpr = (AssignmentExpressionSyntax)assignmentStatement.Expression;
            var matchCallExpression = assignmentExpr.Right;

            editor.ReplaceNode(ifStatement.Condition, BuildIsPatternCondition(ifStatement, matchCallExpression, variableName));
            editor.RemoveNode(assignmentStatement);
            editor.RemoveNode(preDeclaration);

            return editor.GetChangedDocument();
        }

        /// <summary>
        /// Builds: <c>Regex.Match(input, pattern) is { Success: true } m</c>
        /// </summary>
        private static IsPatternExpressionSyntax BuildIsPatternCondition(
            IfStatementSyntax ifStatement,
            ExpressionSyntax matchCallExpression,
            string variableName)
        {
            var successPattern = SyntaxFactory.RecursivePattern()
                .WithPropertyPatternClause(
                    SyntaxFactory.PropertyPatternClause(
                        SyntaxFactory.SeparatedList(new[]
                        {
                            SyntaxFactory.Subpattern(
                                SyntaxFactory.NameColon(SyntaxFactory.IdentifierName("Success")),
                                SyntaxFactory.ConstantPattern(
                                    SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression)))
                        })))
                .WithDesignation(
                    SyntaxFactory.SingleVariableDesignation(
                        SyntaxFactory.Identifier(variableName)))
                .NormalizeWhitespace();

            return SyntaxFactory.IsPatternExpression(
                matchCallExpression.WithoutTrivia(),
                successPattern)
                .WithLeadingTrivia(ifStatement.Condition.GetLeadingTrivia())
                .WithTrailingTrivia(SyntaxFactory.TriviaList())
                .WithAdditionalAnnotations(Formatter.Annotation);
        }

        /// <summary>
        /// Checks whether the given syntax node (typically an else clause) contains any
        /// variable binding with the specified name — including variable declarators,
        /// pattern designations (is/switch patterns), out var, foreach, and catch.
        /// </summary>
        private static bool HasConflictingName(SyntaxNode node, string variableName)
        {
            foreach (var descendant in node.DescendantNodes())
            {
                if (descendant is VariableDeclaratorSyntax declarator &&
                    declarator.Identifier.ValueText == variableName)
                {
                    return true;
                }

                if (descendant is SingleVariableDesignationSyntax designation &&
                    designation.Identifier.ValueText == variableName)
                {
                    return true;
                }

                if (descendant is ForEachStatementSyntax forEach &&
                    forEach.Identifier.ValueText == variableName)
                {
                    return true;
                }

                // Deconstruction foreach: foreach (var (x, y) in ...)
                if (descendant is ForEachVariableStatementSyntax forEachVariable &&
                    forEachVariable.Variable
                        .DescendantNodesAndSelf()
                        .OfType<SingleVariableDesignationSyntax>()
                        .Any(d => d.Identifier.ValueText == variableName))
                {
                    return true;
                }

                if (descendant is CatchDeclarationSyntax catchDecl &&
                    catchDecl.Identifier.ValueText == variableName)
                {
                    return true;
                }

                // Lambda, anonymous method, and local function parameters
                if (descendant is ParameterSyntax parameter &&
                    parameter.Identifier.ValueText == variableName)
                {
                    return true;
                }

                // LINQ query range variables. Pattern variables introduced by
                // `is { ... } m` scope to the enclosing block, so any subsequent
                // query that already binds the same name would start conflicting.
                if (descendant is FromClauseSyntax fromClause &&
                    fromClause.Identifier.ValueText == variableName)
                {
                    return true;
                }

                if (descendant is LetClauseSyntax letClause &&
                    letClause.Identifier.ValueText == variableName)
                {
                    return true;
                }

                if (descendant is JoinClauseSyntax joinClause &&
                    joinClause.Identifier.ValueText == variableName)
                {
                    return true;
                }

                if (descendant is JoinIntoClauseSyntax joinIntoClause &&
                    joinIntoClause.Identifier.ValueText == variableName)
                {
                    return true;
                }

                if (descendant is QueryContinuationSyntax queryCont &&
                    queryCont.Identifier.ValueText == variableName)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Checks whether any statement after the given if statement in its parent block
        /// declares a variable with the specified name. Pattern variables from the if
        /// condition scope to the entire enclosing block, so later declarations conflict.
        /// For parent containers other than <see cref="BlockSyntax"/>, conservatively
        /// assume a conflict because this helper only scans block statements.
        /// </summary>
        private static bool HasConflictingNameInSubsequentSiblings(
            IfStatementSyntax ifStatement, string variableName)
        {
            // Walk up through else-if chains to find the outermost if statement.
            // Pattern variables scope to the enclosing block, so for an "else if" we
            // must check siblings after the outermost if in that chain.
            SyntaxNode current = ifStatement;
            while (current.Parent is ElseClauseSyntax elseClause &&
                   elseClause.Parent is IfStatementSyntax parentIf)
            {
                current = parentIf;
            }

            if (current.Parent is not BlockSyntax parentBlock)
            {
                return true;
            }

            bool foundIf = false;
            foreach (var statement in parentBlock.Statements)
            {
                if (statement == current)
                {
                    foundIf = true;
                    continue;
                }

                if (foundIf && HasConflictingName(statement, variableName))
                {
                    return true;
                }
            }

            return false;
        }
    }
}