File: Microsoft.NetCore.Analyzers\Runtime\TestForEmptyStringsUsingStringLength.Fixer.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis.CodeFixes;
using System.Collections.Immutable;

using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.NetAnalyzers;
using System.Threading;
using Analyzer.Utilities;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    /// <summary>
    /// CA1820: Test for empty strings using string length
    /// </summary>
    public abstract class TestForEmptyStringsUsingStringLengthFixer : CodeFixProvider
    {
        private const string TestForEmptyStringCorrectlyUsingIsNullOrEmpty = nameof(TestForEmptyStringCorrectlyUsingIsNullOrEmpty);
        private const string TestForEmptyStringCorrectlyUsingStringLength = nameof(TestForEmptyStringCorrectlyUsingStringLength);

        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(TestForEmptyStringsUsingStringLengthAnalyzer.RuleId);

        //  Two fixes are offered for the same diagnostic, so the equivalence key decides which one a
        //  fix-all applies. SyntaxEditorFixAllProvider does not filter on it.
        public sealed override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<string?>(context => context.CodeActionEquivalenceKey, ApplyFixAsync);

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            SyntaxNode node = root.FindNode(context.Span);

            SyntaxNode expressionSyntax = GetExpression(node);

            if (!IsFixableBinaryExpression(expressionSyntax) && !IsFixableInvocationExpression(expressionSyntax))
            {
                return;
            }

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

            FixResolution? resolution = TryGetFixResolution(expressionSyntax, model, context.CancellationToken);

            if (resolution != null)
            {
                context.RegisterCodeFix(CreateCodeAction(context, TestForEmptyStringCorrectlyUsingIsNullOrEmpty), context.Diagnostics);
                context.RegisterCodeFix(CreateCodeAction(context, TestForEmptyStringCorrectlyUsingStringLength), context.Diagnostics);
            }
        }

        private CodeAction CreateCodeAction(CodeFixContext context, string equivalenceKey)
        {
            Document document = context.Document;
            ImmutableArray<Diagnostic> diagnostics = context.Diagnostics;

            return CodeAction.Create(
                MicrosoftNetCoreAnalyzersResources.TestForEmptyStringsUsingStringLengthMessage,
                ct => SyntaxEditorFixAllProvider.ApplyFixesAsync(document, diagnostics, (doc, diagnostic, editor, token) => ApplyFixAsync(doc, diagnostic, editor, equivalenceKey, token), ct),
                equivalenceKey);
        }

        private async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken)
        {
            SyntaxNode expressionSyntax = GetExpression(editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan));

            if (!IsFixableBinaryExpression(expressionSyntax) && !IsFixableInvocationExpression(expressionSyntax))
            {
                return;
            }

            SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

            if (TryGetFixResolution(expressionSyntax, model, cancellationToken) is not FixResolution resolution)
            {
                return;
            }

            if (equivalenceKey == TestForEmptyStringCorrectlyUsingIsNullOrEmpty)
            {
                ConvertToMethodInvocation(editor, resolution);
            }
            else if (equivalenceKey == TestForEmptyStringCorrectlyUsingStringLength)
            {
                ConvertToStringLengthComparison(editor, resolution);
            }
        }

        private FixResolution? TryGetFixResolution(SyntaxNode expressionSyntax, SemanticModel model, CancellationToken cancellationToken)
        {
            if (IsFixableBinaryExpression(expressionSyntax))
            {
                bool isEqualsOperator = IsEqualsOperator(expressionSyntax);
                SyntaxNode leftOperand = GetLeftOperand(expressionSyntax);
                SyntaxNode rightOperand = GetRightOperand(expressionSyntax);

                if (ContainsSystemStringEmpty(leftOperand, model, cancellationToken) || ContainsEmptyStringLiteral(leftOperand, model, cancellationToken))
                {
                    return new FixResolution(expressionSyntax, rightOperand, isEqualsOperator);
                }

                if (ContainsSystemStringEmpty(rightOperand, model, cancellationToken) || ContainsEmptyStringLiteral(rightOperand, model, cancellationToken))
                {
                    return new FixResolution(expressionSyntax, leftOperand, isEqualsOperator);
                }
            }
            else if (IsFixableInvocationExpression(expressionSyntax))
            {
                SyntaxNode? target = GetInvocationTarget(expressionSyntax);

                if (target == null)
                {
                    return null;
                }

                return new FixResolution(expressionSyntax, target, true);
            }

            return null;
        }

        private static bool ContainsSystemStringEmpty(SyntaxNode expressionSyntax, SemanticModel model, CancellationToken cancellationToken)
        {
            if (model.GetSymbolInfo(expressionSyntax, cancellationToken).Symbol is IFieldSymbol fieldSymbol)
            {
                if (fieldSymbol.Type.SpecialType == SpecialType.System_String)
                {
                    return fieldSymbol.IsReadOnly && fieldSymbol.Name == "Empty";
                }
            }

            return false;
        }

        private static void ConvertToMethodInvocation(SyntaxEditor editor, FixResolution fixResolution)
        {
            //  The replacement carries the target over from inside the node being replaced, so track it:
            //  a nested violation may already have rewritten it.
            editor.TrackNode(fixResolution.Target);

            editor.ReplaceNode(fixResolution.ExpressionSyntax, (currentNode, generator) =>
            {
                SyntaxNode target = currentNode.GetCurrentNode(fixResolution.Target) ?? fixResolution.Target;

                SyntaxNode typeNameSyntax = generator.TypeExpression(SpecialType.System_String);
                SyntaxNode nullOrEmptyMemberSyntax = generator.MemberAccessExpression(typeNameSyntax, "IsNullOrEmpty");
                SyntaxNode nullOrEmptyInvocationSyntax = generator.InvocationExpression(nullOrEmptyMemberSyntax, target.WithoutTrailingTrivia());

                SyntaxNode replacementSyntax = fixResolution.UsesEqualsOperator ? nullOrEmptyInvocationSyntax : generator.LogicalNotExpression(nullOrEmptyInvocationSyntax);

                return replacementSyntax.WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(currentNode);
            });
        }

        private void ConvertToStringLengthComparison(SyntaxEditor editor, FixResolution fixResolution)
        {
            SyntaxNode originalLeftOperand = GetLeftOperand(fixResolution.ExpressionSyntax);
            SyntaxNode originalRightOperand = GetRightOperand(fixResolution.ExpressionSyntax);
            bool targetIsLeftOperand = originalLeftOperand == fixResolution.Target;

            editor.TrackNode(originalLeftOperand);
            editor.TrackNode(originalRightOperand);

            editor.ReplaceNode(fixResolution.ExpressionSyntax, (currentNode, generator) =>
            {
                SyntaxNode leftOperand = currentNode.GetCurrentNode(originalLeftOperand) ?? originalLeftOperand;
                SyntaxNode rightOperand = currentNode.GetCurrentNode(originalRightOperand) ?? originalRightOperand;

                // Take the below example:
                //   if (f == String.Empty) ...
                // The comparison operand, f, will now become 'f.Length' and a the other operand will become '0'
                SyntaxNode zeroLengthSyntax = generator.LiteralExpression(0);
                if (targetIsLeftOperand)
                {
                    leftOperand = generator.MemberAccessExpression(leftOperand, "Length");
                    rightOperand = zeroLengthSyntax.WithTriviaFrom(rightOperand);
                }
                else
                {
                    leftOperand = zeroLengthSyntax;
                    rightOperand = generator.MemberAccessExpression(rightOperand.WithoutTrivia(), "Length");
                }

                SyntaxNode replacementSyntax = fixResolution.UsesEqualsOperator ?
                    generator.ValueEqualsExpression(leftOperand, rightOperand) :
                    generator.ValueNotEqualsExpression(leftOperand, rightOperand);

                return replacementSyntax.WithAdditionalAnnotations(Formatter.Annotation);
            });
        }

        private static bool ContainsEmptyStringLiteral(SyntaxNode node, SemanticModel model, CancellationToken cancellationToken)
            => model.GetConstantValue(node, cancellationToken) is Optional<object> optionalValue &&
            optionalValue.HasValue && optionalValue.Value is string value && value.Length == 0;

        protected abstract SyntaxNode GetExpression(SyntaxNode node);
        protected abstract bool IsFixableBinaryExpression(SyntaxNode node);
        protected abstract bool IsFixableInvocationExpression(SyntaxNode node);
        protected abstract bool IsEqualsOperator(SyntaxNode node);
        protected abstract bool IsNotEqualsOperator(SyntaxNode node);
        protected abstract SyntaxNode GetLeftOperand(SyntaxNode binaryExpressionSyntax);
        protected abstract SyntaxNode GetRightOperand(SyntaxNode binaryExpressionSyntax);
        protected abstract SyntaxNode? GetInvocationTarget(SyntaxNode node);

        private sealed class FixResolution
        {
            public SyntaxNode ExpressionSyntax { get; }
            public SyntaxNode Target { get; }
            public bool UsesEqualsOperator { get; }

            public FixResolution(SyntaxNode expressionSyntax, SyntaxNode target, bool usesEqualsOperator)
            {
                ExpressionSyntax = expressionSyntax;
                Target = target;
                UsesEqualsOperator = usesEqualsOperator;
            }
        }
    }
}