// 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.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; 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.Performance { using static MicrosoftNetCoreAnalyzersResources; /// <summary> /// CA1862: Prefer the StringComparison method overloads to perform case-insensitive string comparisons. /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class RecommendCaseInsensitiveStringComparisonAnalyzer : DiagnosticAnalyzer { internal const string RuleId = "CA1862"; internal const string StringComparisonInvariantCultureIgnoreCaseName = "InvariantCultureIgnoreCase"; internal const string StringComparisonCurrentCultureIgnoreCaseName = "CurrentCultureIgnoreCase"; internal const string StringToLowerMethodName = "ToLower"; internal const string StringToUpperMethodName = "ToUpper"; internal const string StringToLowerInvariantMethodName = "ToLowerInvariant"; internal const string StringToUpperInvariantMethodName = "ToUpperInvariant"; internal const string StringContainsMethodName = "Contains"; internal const string StringIndexOfMethodName = "IndexOf"; internal const string StringStartsWithMethodName = "StartsWith"; internal const string StringCompareToMethodName = "CompareTo"; internal const string StringEqualsMethodName = "Equals"; internal const string StringParameterName = "value"; internal const string StringComparisonParameterName = "comparisonType"; internal const string LeftOffendingMethodName = "LeftOffendingMethod"; internal const string RightOffendingMethodName = "RightOffendingMethod"; internal static readonly DiagnosticDescriptor RecommendCaseInsensitiveStringComparisonRule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringComparisonTitle)), CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringComparisonMessage)), DiagnosticCategory.Performance, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringComparisonDescription)), isPortedFxCopRule: false, isDataflowRule: false); internal static readonly DiagnosticDescriptor RecommendCaseInsensitiveStringComparerRule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringComparisonTitle)), CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringComparerMessage)), DiagnosticCategory.Performance, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringComparerDescription)), isPortedFxCopRule: false, isDataflowRule: false); internal static readonly DiagnosticDescriptor RecommendCaseInsensitiveStringEqualsRule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringComparisonTitle)), CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringEqualsMessage)), DiagnosticCategory.Performance, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(RecommendCaseInsensitiveStringEqualsDescription)), isPortedFxCopRule: false, isDataflowRule: false); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create( RecommendCaseInsensitiveStringComparisonRule, RecommendCaseInsensitiveStringComparerRule, RecommendCaseInsensitiveStringEqualsRule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterCompilationStartAction(AnalyzeCompilationStart); } private void AnalyzeCompilationStart(CompilationStartAnalysisContext context) { // Retrieve the essential types: string, StringComparison, StringComparer INamedTypeSymbol stringType = context.Compilation.GetSpecialType(SpecialType.System_String); INamedTypeSymbol int32Type = context.Compilation.GetSpecialType(SpecialType.System_Int32); if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemStringComparison, out INamedTypeSymbol? stringComparisonType)) { return; } if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemStringComparer, out INamedTypeSymbol? stringComparerType)) { return; } // Retrieve the offending parameterless methods: ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant IMethodSymbol? toLowerParameterlessMethod = stringType.GetMembers(StringToLowerMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(); if (toLowerParameterlessMethod == null) { return; } IMethodSymbol? toLowerInvariantParameterlessMethod = stringType.GetMembers(StringToLowerInvariantMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(); if (toLowerInvariantParameterlessMethod == null) { return; } IMethodSymbol? toUpperParameterlessMethod = stringType.GetMembers(StringToUpperMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(); if (toUpperParameterlessMethod == null) { return; } IMethodSymbol? toUpperInvariantParameterlessMethod = stringType.GetMembers(StringToUpperInvariantMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(); if (toUpperInvariantParameterlessMethod == null) { return; } // Create the different expected parameter combinations ParameterInfo[] stringParameter = new[] { ParameterInfo.GetParameterInfo(stringType) }; // Equals(string) IMethodSymbol? stringEqualsStringMethod = stringType.GetMembers(StringEqualsMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(stringParameter); if (stringEqualsStringMethod == null) { return; } // Retrieve the diagnosable string overload methods: Contains, IndexOf (3 overloads), StartsWith, CompareTo // Contains(string) IMethodSymbol? containsStringMethod = stringType.GetMembers(StringContainsMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(stringParameter); if (containsStringMethod == null) { return; } // StartsWith(string) IMethodSymbol? startsWithStringMethod = stringType.GetMembers(StringStartsWithMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(stringParameter); if (startsWithStringMethod == null) { return; } IEnumerable<IMethodSymbol> indexOfMethods = stringType.GetMembers(StringIndexOfMethodName).OfType<IMethodSymbol>(); // IndexOf(string) IMethodSymbol? indexOfStringMethod = indexOfMethods.GetFirstOrDefaultMemberWithParameterInfos(stringParameter); if (indexOfStringMethod == null) { return; } ParameterInfo[] stringInt32Parameters = new[] { ParameterInfo.GetParameterInfo(stringType), ParameterInfo.GetParameterInfo(int32Type) }; // IndexOf(string, int startIndex) IMethodSymbol? indexOfStringInt32Method = indexOfMethods.GetFirstOrDefaultMemberWithParameterInfos(stringInt32Parameters); if (indexOfStringInt32Method == null) { return; } ParameterInfo[] stringInt32Int32Parameters = new[] { ParameterInfo.GetParameterInfo(stringType), ParameterInfo.GetParameterInfo(int32Type), ParameterInfo.GetParameterInfo(int32Type) }; // IndexOf(string, int startIndex, int count) IMethodSymbol? indexOfStringInt32Int32Method = indexOfMethods.GetFirstOrDefaultMemberWithParameterInfos(stringInt32Int32Parameters); if (indexOfStringInt32Int32Method == null) { return; } // CompareTo(string) IMethodSymbol? compareToStringMethod = stringType.GetMembers(StringCompareToMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(stringParameter); if (compareToStringMethod == null) { return; } // Retrieve the StringComparer properties that need to be flagged: CurrentCultureIgnoreCase, InvariantCultureIgnoreCase IEnumerable<IPropertySymbol> ccicPropertyGroup = stringComparerType.GetMembers(StringComparisonCurrentCultureIgnoreCaseName).OfType<IPropertySymbol>(); if (!ccicPropertyGroup.Any()) { return; } IEnumerable<IPropertySymbol> icicPropertyGroup = stringComparerType.GetMembers(StringComparisonInvariantCultureIgnoreCaseName).OfType<IPropertySymbol>(); if (!icicPropertyGroup.Any()) { return; } ParameterInfo[] stringStringComparisonParameters = { ParameterInfo.GetParameterInfo(stringType), ParameterInfo.GetParameterInfo(stringComparisonType) }; IMethodSymbol? containsStringWithStringComparisonMethod = stringType.GetMembers(StringContainsMethodName).OfType<IMethodSymbol>().GetFirstOrDefaultMemberWithParameterInfos(stringStringComparisonParameters); // a.ToLower().Method(b.ToLower()) context.RegisterOperationAction(context => { IInvocationOperation invocation = (IInvocationOperation)context.Operation; AnalyzeInvocation(context, invocation, stringType, containsStringMethod, containsStringWithStringComparisonMethod, startsWithStringMethod, compareToStringMethod, indexOfStringMethod, indexOfStringInt32Method, indexOfStringInt32Int32Method); }, OperationKind.Invocation); // a.ToLower() == b.ToLower() context.RegisterOperationAction(context => { IBinaryOperation binaryOperation = (IBinaryOperation)context.Operation; AnalyzeBinaryOperation(context, binaryOperation, stringType); }, OperationKind.Binary); } private static void AnalyzeInvocation(OperationAnalysisContext context, IInvocationOperation invocation, INamedTypeSymbol stringType, IMethodSymbol containsStringMethod, IMethodSymbol? containsStringWithStringComparisonMethod, IMethodSymbol startsWithStringMethod, IMethodSymbol compareToStringMethod, IMethodSymbol indexOfStringMethod, IMethodSymbol indexOfStringInt32Method, IMethodSymbol indexOfStringInt32Int32Method) { IMethodSymbol diagnosableMethod = invocation.TargetMethod; DiagnosticDescriptor? chosenRule; if (diagnosableMethod.Equals(containsStringMethod) && containsStringWithStringComparisonMethod is not null || diagnosableMethod.Equals(startsWithStringMethod) || diagnosableMethod.Equals(indexOfStringMethod) || diagnosableMethod.Equals(indexOfStringInt32Method) || diagnosableMethod.Equals(indexOfStringInt32Int32Method)) { chosenRule = RecommendCaseInsensitiveStringComparisonRule; } else if (diagnosableMethod.Equals(compareToStringMethod)) { chosenRule = RecommendCaseInsensitiveStringComparerRule; } else { return; } bool atLeastOneOffendingInvocation = false; // First check if this is a case where the instance is a string that resulted from an offending // invocation, like {a.ToLower()}.Contains(), in which case we can collect the left side. string? leftOffendingMethodName = null; if (TryGetInvocationWithoutParentheses(invocation.Instance, out IInvocationOperation? maybeLeftOffendingInvocation)) { atLeastOneOffendingInvocation = IsOffendingMethod(maybeLeftOffendingInvocation, stringType, out leftOffendingMethodName); } // Now check if the first argument of Contains|StartsWith|IndexOf is an invocation on a string // instance of one of the offending methods, in which case, we can collect the right side. Debug.Assert(!invocation.Arguments.IsEmpty); string? rightOffendingMethodName = null; if (TryGetInvocationWithoutParentheses(invocation.Arguments[0].Value, out IInvocationOperation? maybeRightOffendingInvocation)) { atLeastOneOffendingInvocation |= IsOffendingMethod(maybeRightOffendingInvocation, stringType, out rightOffendingMethodName); } if (!atLeastOneOffendingInvocation) { // For a diagnosis on an invocation operation, either the instance of the invocation // or the string instance of the first argument need to be an offending method. return; } ImmutableDictionary<string, string?> dict = new Dictionary<string, string?>() { { LeftOffendingMethodName, leftOffendingMethodName }, { RightOffendingMethodName, rightOffendingMethodName } }.ToImmutableDictionary(); context.ReportDiagnostic(invocation.CreateDiagnostic(chosenRule, dict, diagnosableMethod)); } private static void AnalyzeBinaryOperation(OperationAnalysisContext context, IBinaryOperation binaryOperation, INamedTypeSymbol stringType) { if (binaryOperation.OperatorKind is not BinaryOperatorKind.Equals and not BinaryOperatorKind.NotEquals) { return; } bool atLeastOneOffendingInvocation = false; string? leftOffendingMethodName = null; if (TryGetInvocationWithoutParentheses(binaryOperation.LeftOperand, out IInvocationOperation? leftInvocation)) { atLeastOneOffendingInvocation = IsOffendingMethod(leftInvocation, stringType, out leftOffendingMethodName); } string? rightOffendingMethodName = null; if (TryGetInvocationWithoutParentheses(binaryOperation.RightOperand, out IInvocationOperation? rightInvocation)) { atLeastOneOffendingInvocation |= IsOffendingMethod(rightInvocation, stringType, out rightOffendingMethodName); } if (!atLeastOneOffendingInvocation) { // For a diagnosis on a binary operation, at least one of the two sides needs to // be an invocation of an offending method over a string instance. return; } ImmutableDictionary<string, string?> dict = new Dictionary<string, string?>() { { LeftOffendingMethodName, leftOffendingMethodName }, { RightOffendingMethodName, rightOffendingMethodName } }.ToImmutableDictionary(); context.ReportDiagnostic(binaryOperation.CreateDiagnostic(RecommendCaseInsensitiveStringEqualsRule, dict)); } private static bool TryGetInvocationWithoutParentheses(IOperation? operation, [NotNullWhen(returnValue: true)] out IInvocationOperation? diagnosableInvocation) { diagnosableInvocation = null; IOperation? descendant = operation; while (descendant is IParenthesizedOperation parenthesizedOperation) { descendant = parenthesizedOperation.Operand; } if (descendant is IInvocationOperation invocationDescendant) { diagnosableInvocation = invocationDescendant; } else if (descendant is IArgumentOperation argumentDescendant && argumentDescendant.Value is IInvocationOperation argumentInvocationDescendant) { diagnosableInvocation = argumentInvocationDescendant; } return diagnosableInvocation != null; } private static bool IsOffendingMethod(IInvocationOperation invocation, ITypeSymbol stringType, [NotNullWhen(returnValue: true)] out string? offendingMethodName) { offendingMethodName = null; if (invocation.Instance == null || invocation.Instance.Type == null) { return false; } if (!invocation.Instance.Type.Equals(stringType)) { return false; } if (!invocation.TargetMethod.Name.Equals(StringToLowerMethodName, StringComparison.Ordinal) && !invocation.TargetMethod.Name.Equals(StringToLowerInvariantMethodName, StringComparison.Ordinal) && !invocation.TargetMethod.Name.Equals(StringToUpperMethodName, StringComparison.Ordinal) && !invocation.TargetMethod.Name.Equals(StringToUpperInvariantMethodName, StringComparison.Ordinal)) { return false; } offendingMethodName = invocation.TargetMethod.Name; return true; } } }