File: Microsoft.NetCore.Analyzers\Runtime\TestForEmptyStringsUsingStringLength.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 System;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA1820: <inheritdoc cref="TestForEmptyStringsUsingStringLengthTitle"/>
    /// <para>
    /// Comparing strings using the <see cref="string.Length"/> property or the <see cref="string.IsNullOrEmpty"/> method is significantly faster than using <see cref="string.Equals(string)"/>.
    /// This is because Equals executes significantly more MSIL instructions than either IsNullOrEmpty or the number of instructions executed to retrieve the Length property value and compare it to zero.
    /// </para>
    /// <remarks>NOTE: This rule is not supported for VisualBasic. See https://github.com/dotnet/roslyn-analyzers/issues/2684 for details.</remarks>
    /// </summary>
#pragma warning disable RS1004 // Recommend adding language support to diagnostic analyzer
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
#pragma warning restore RS1004 // Recommend adding language support to diagnostic analyzer
    public sealed class TestForEmptyStringsUsingStringLengthAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1820";
        private const string StringEmptyFieldName = "Empty";

        private static readonly DiagnosticDescriptor s_rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(TestForEmptyStringsUsingStringLengthTitle)),
            CreateLocalizableResourceString(nameof(TestForEmptyStringsUsingStringLengthMessage)),
            DiagnosticCategory.Performance,
            RuleLevel.Disabled,    // Benefits provided might not outweight noise in test code.
            description: CreateLocalizableResourceString(nameof(TestForEmptyStringsUsingStringLengthDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(s_rule);

        public sealed override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(context =>
            {
                var linqExpressionType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqExpressionsExpression1);

                context.RegisterOperationAction(
                    operationAnalysisContext => AnalyzeInvocationExpression(
                        (IInvocationOperation)operationAnalysisContext.Operation, linqExpressionType,
                        static (context, diagnostic) => context.ReportDiagnostic(diagnostic),
                        operationAnalysisContext),
                    OperationKind.Invocation);

                context.RegisterOperationAction(
                    operationAnalysisContext => AnalyzeBinaryExpression(
                        (IBinaryOperation)operationAnalysisContext.Operation, linqExpressionType,
                        static (context, diagnostic) => context.ReportDiagnostic(diagnostic),
                        operationAnalysisContext),
                    OperationKind.BinaryOperator);
            });
        }

        /// <summary>
        /// Check to see if we have an invocation to string.Equals that has an empty string as an argument.
        /// </summary>
        private static void AnalyzeInvocationExpression<TContext>(IInvocationOperation invocationOperation,
            INamedTypeSymbol? linqExpressionTreeType, Action<TContext, Diagnostic> reportDiagnostic, TContext context)
        {
            if (!invocationOperation.Arguments.IsEmpty)
            {
                IMethodSymbol methodSymbol = invocationOperation.TargetMethod;
                if (methodSymbol == null
                    || !IsStringEqualsMethod(methodSymbol)
                    || !HasAnEmptyStringArgument(invocationOperation))
                {
                    return;
                }

                // Check if we are in a Expression<Func<T...>> context, in which case it is possible
                // that the underlying call doesn't have the helper so we want to bail-out.
                if (!invocationOperation.IsWithinExpressionTree(linqExpressionTreeType))
                {
                    reportDiagnostic(context, invocationOperation.Syntax.CreateDiagnostic(s_rule));
                }
            }
        }

        /// <summary>
        /// Check to see if we have a equals or not equals expression where an empty string is being
        /// compared.
        /// </summary>
        private static void AnalyzeBinaryExpression<TContext>(IBinaryOperation binaryOperation,
            INamedTypeSymbol? linqExpressionTreeType, Action<TContext, Diagnostic> reportDiagnostic, TContext context)
        {
            if (binaryOperation.OperatorKind is not BinaryOperatorKind.Equals and
                not BinaryOperatorKind.NotEquals)
            {
                return;
            }

            if (binaryOperation.LeftOperand.Type?.SpecialType != SpecialType.System_String ||
                binaryOperation.RightOperand.Type?.SpecialType != SpecialType.System_String)
            {
                return;
            }

            if (!IsEmptyString(binaryOperation.LeftOperand)
                && !IsEmptyString(binaryOperation.RightOperand))
            {
                return;
            }

            // Check if we are in a Expression<Func<T...>> context, in which case it is possible
            // that the underlying call doesn't have the helper so we want to bail-out.
            if (!binaryOperation.IsWithinExpressionTree(linqExpressionTreeType))
            {
                reportDiagnostic(context, binaryOperation.Syntax.CreateDiagnostic(s_rule));
            }
        }

        /// <summary>
        /// Checks if the given method is the string.Equals method.
        /// </summary>
        private static bool IsStringEqualsMethod(IMethodSymbol methodSymbol)
        {
            return string.Equals(methodSymbol.Name, WellKnownMemberNames.ObjectEquals, StringComparison.Ordinal) &&
                   methodSymbol.ContainingType.SpecialType == SpecialType.System_String;
        }

        /// <summary>
        /// Checks if the given expression something that evaluates to a constant string
        /// or the string.Empty field
        /// </summary>
        private static bool IsEmptyString(IOperation expression)
        {
            if (expression == null)
            {
                return false;
            }

            Optional<object?> constantValueOpt = expression.ConstantValue;
            if (constantValueOpt.HasValue)
            {
                return constantValueOpt.Value is string { Length: 0 };
            }

            if (expression.Kind == OperationKind.FieldReference)
            {
                IFieldSymbol field = ((IFieldReferenceOperation)expression).Field;
                return string.Equals(field.Name, StringEmptyFieldName, StringComparison.Ordinal) &&
                    field.Type.SpecialType == SpecialType.System_String;
            }

            return false;
        }

        /// <summary>
        /// Checks if the given invocation has an argument that is an empty string.
        /// </summary>
        private static bool HasAnEmptyStringArgument(IInvocationOperation invocation)
        {
            return invocation.Arguments.Any(arg => IsEmptyString(arg.Value));
        }
    }
}