// 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.Generic; using System.Collections.Immutable; using System.Diagnostics; 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.Editing; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.NetCore.Analyzers.Performance { using RCISCAnalyzer = RecommendCaseInsensitiveStringComparisonAnalyzer; /// <summary> /// CA1862: Prefer the StringComparison method overloads to perform case-insensitive string comparisons. /// </summary> public abstract class RecommendCaseInsensitiveStringComparisonFixer : CodeFixProvider { protected const string StringTypeName = "String"; protected abstract IEnumerable<SyntaxNode> GetNewArgumentsForInvocation(SyntaxGenerator generator, string caseChangingApproachValue, IInvocationOperation mainInvocationOperation, INamedTypeSymbol stringComparisonType, string? leftOffendingMethod, string? rightOffendingMethod, out SyntaxNode? mainInvocationInstance); protected abstract IEnumerable<SyntaxNode> GetNewArgumentsForBinary(SyntaxGenerator generator, SyntaxNode rightNode, SyntaxNode typeMemberAccess); public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(RCISCAnalyzer.RuleId); public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public override async Task RegisterCodeFixesAsync(CodeFixContext context) { CancellationToken ct = context.CancellationToken; Document doc = context.Document; SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(ct).ConfigureAwait(false); if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode node) { return; } SemanticModel model = await doc.GetRequiredSemanticModelAsync(ct).ConfigureAwait(false); if (model.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemStringComparison) is not INamedTypeSymbol stringComparisonType) { return; } IOperation? operation = model.GetOperation(node, ct); if (operation == null) { return; } SyntaxGenerator generator = SyntaxGenerator.GetGenerator(doc); ImmutableDictionary<string, string?> dict = context.Diagnostics[0].Properties; // The dictionary should contain the keys for both left and right offending methods, // and at least one of them should not be null. if (!dict.TryGetValue(RCISCAnalyzer.LeftOffendingMethodName, out string? leftOffendingMethod) || !dict.TryGetValue(RCISCAnalyzer.RightOffendingMethodName, out string? rightOffendingMethod) || (leftOffendingMethod == null && rightOffendingMethod == null)) { return; } bool leftIsToLowerOrToUpper = leftOffendingMethod is RCISCAnalyzer.StringToLowerMethodName or RCISCAnalyzer.StringToUpperMethodName; bool rightIsToLowerOrToUpper = rightOffendingMethod is RCISCAnalyzer.StringToLowerMethodName or RCISCAnalyzer.StringToUpperMethodName; bool leftIsToLowerInvariantOrToUpperInvariant = leftOffendingMethod is RCISCAnalyzer.StringToLowerInvariantMethodName or RCISCAnalyzer.StringToUpperInvariantMethodName; bool rightIsToLowerInvariantOrToUpperInvariant = rightOffendingMethod is RCISCAnalyzer.StringToLowerInvariantMethodName or RCISCAnalyzer.StringToUpperInvariantMethodName; // If the cultures of the two strings are incompatible, do not offer a fix if ((leftIsToLowerOrToUpper && rightIsToLowerInvariantOrToUpperInvariant) || (rightIsToLowerOrToUpper && leftIsToLowerInvariantOrToUpperInvariant)) { return; } string? caseChangingApproachValue = null; if (leftIsToLowerOrToUpper || rightIsToLowerOrToUpper) { caseChangingApproachValue = RCISCAnalyzer.StringComparisonCurrentCultureIgnoreCaseName; } else if (leftIsToLowerInvariantOrToUpperInvariant || rightIsToLowerInvariantOrToUpperInvariant) { caseChangingApproachValue = RCISCAnalyzer.StringComparisonInvariantCultureIgnoreCaseName; } Debug.Assert(caseChangingApproachValue != null, "Unexpected offending methods"); if (operation is IInvocationOperation invocation) { if (invocation.TargetMethod.Name is RCISCAnalyzer.StringCompareToMethodName) { // Never offer a fix for CompareTo return; } Task<Document> createChangedDocument(CancellationToken _) => FixInvocationAsync(generator, doc, root, invocation, stringComparisonType, invocation.TargetMethod.Name, caseChangingApproachValue!, leftOffendingMethod, rightOffendingMethod); string title = string.Format(System.Globalization.CultureInfo.CurrentCulture, MicrosoftNetCoreAnalyzersResources.RecommendCaseInsensitiveStringComparerStringComparisonCodeFixTitle, invocation.TargetMethod.Name); context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument, equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.RecommendCaseInsensitiveStringComparerStringComparisonCodeFixTitle)), context.Diagnostics); } else if (operation is IBinaryOperation binaryOperation && binaryOperation.LeftOperand != null && binaryOperation.RightOperand != null) { Task<Document> createChangedDocument(CancellationToken _) => FixBinaryAsync(generator, doc, root, binaryOperation, stringComparisonType, caseChangingApproachValue!); string title = MicrosoftNetCoreAnalyzersResources.RecommendCaseInsensitiveStringEqualsCodeFixTitle; context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument, equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.RecommendCaseInsensitiveStringEqualsCodeFixTitle)), context.Diagnostics); } } private Task<Document> FixInvocationAsync(SyntaxGenerator generator, Document doc, SyntaxNode root, IInvocationOperation mainInvocation, INamedTypeSymbol stringComparisonType, string diagnosableMethodName, string caseChangingApproachValue, string? leftOffendingMethod, string? rightOffendingMethod) { // Defensive check: The max number of arguments is held by IndexOf Debug.Assert(mainInvocation.Arguments.Length <= 3); // For the Diagnosable methods Contains(string) and StartsWith(string) // If we have this code ('a' and 'b' are string instances): // A) a.CaseChanging().Diagnosable(b); // B) a.Diagnosable(b.CaseChanging()); // We want to convert any of them to: // a.Diagnosable(b, StringComparison.DesiredCultureDesiredCase); // For IndexOf we have 3 options: // A.1) a.CaseChanging().IndexOf(b) // A.2) a.IndexOf(b.CaseChanging()) // B.1) a.CaseChanging().IndexOf(b, startIndex: n) // B.2) a.IndexOf(b.CaseChanging(), startIndex: n) // C.1) a.CaseChanging().IndexOf(b, startIndex: n, count: m) // C.2) a.IndexOf(b.CaseChanging(), startIndex: n, count: m) // We want to convert them to: // A) a.IndexOf(b, StringComparison.Desired) // B) a.IndexOf(b, startIndex: n, StringComparison.Desired) // C) a.IndexOf(b, startIndex: n, count: m, StringComparison.Desired) // Defensive check: Should not fix string.CompareTo (or any other method) Debug.Assert(diagnosableMethodName is RCISCAnalyzer.StringContainsMethodName or RCISCAnalyzer.StringIndexOfMethodName or RCISCAnalyzer.StringStartsWithMethodName); IEnumerable<SyntaxNode> newArguments = GetNewArgumentsForInvocation(generator, caseChangingApproachValue, mainInvocation, stringComparisonType, leftOffendingMethod, rightOffendingMethod, out SyntaxNode? mainInvocationInstance); SyntaxNode stringMemberAccessExpression = generator.MemberAccessExpression(mainInvocationInstance, mainInvocation.TargetMethod.Name); SyntaxNode newInvocation = generator.InvocationExpression(stringMemberAccessExpression, newArguments).WithTriviaFrom(mainInvocation.Syntax); SyntaxNode newRoot = generator.ReplaceNode(root, mainInvocation.Syntax, newInvocation.WithTriviaFrom(mainInvocation.Syntax)); return Task.FromResult(doc.WithSyntaxRoot(newRoot)); } private Task<Document> FixBinaryAsync(SyntaxGenerator generator, Document doc, SyntaxNode root, IBinaryOperation binaryOperation, INamedTypeSymbol stringComparisonType, string caseChangingApproachValue) { SyntaxNode leftNode = binaryOperation.LeftOperand is IInvocationOperation leftInvocation ? leftInvocation.Instance!.Syntax : binaryOperation.LeftOperand.Syntax; SyntaxNode rightNode = binaryOperation.RightOperand is IInvocationOperation rightInvocation ? rightInvocation.Instance!.Syntax : binaryOperation.RightOperand.Syntax; SyntaxNode memberAccess = generator.MemberAccessExpression(leftNode, RCISCAnalyzer.StringEqualsMethodName).WithTriviaFrom(leftNode); SyntaxNode stringComparisonTypeExpression = generator.TypeExpressionForStaticMemberAccess(stringComparisonType); SyntaxNode typeMemberAccess = generator.MemberAccessExpression(stringComparisonTypeExpression, caseChangingApproachValue); IEnumerable<SyntaxNode> newArguments = GetNewArgumentsForBinary(generator, rightNode, typeMemberAccess); SyntaxNode equalsInvocation = generator.InvocationExpression(memberAccess, newArguments); // Determine if it should be a.Equals or !a.Equals var replacement = binaryOperation.OperatorKind == BinaryOperatorKind.NotEquals ? generator.LogicalNotExpression(equalsInvocation) : equalsInvocation; SyntaxNode newRoot = generator.ReplaceNode(root, binaryOperation.Syntax, replacement.WithTriviaFrom(binaryOperation.Syntax)); return Task.FromResult(doc.WithSyntaxRoot(newRoot)); } protected static SyntaxNode GetNewStringComparisonArgument(SyntaxGenerator generator, INamedTypeSymbol stringComparisonType, string caseChangingApproachValue, bool isAnyArgumentNamed) { // Generate the enum access expression for "StringComparison.DesiredCultureDesiredCase" SyntaxNode stringComparisonEnumValueAccess = generator.MemberAccessExpression( generator.TypeExpressionForStaticMemberAccess(stringComparisonType), generator.IdentifierName(caseChangingApproachValue)); // Convert the above into an argument node, then append it to the argument list: "b, StringComparison.DesiredCultureDesiredCase" // If at least one of the pre-existing arguments is named, then the StringComparison enum value needs to be named too SyntaxNode stringComparisonArgument = isAnyArgumentNamed ? generator.Argument(name: RCISCAnalyzer.StringComparisonParameterName, RefKind.None, stringComparisonEnumValueAccess) : generator.Argument(stringComparisonEnumValueAccess); return stringComparisonArgument; } } }