diff --git a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md index 52661da90e..d8b1907018 100644 --- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md +++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md @@ -14,6 +14,7 @@ CA1844 | Performance | Info | ProvideStreamMemoryBasedAsyncOverrides, [Documenta CA1845 | Performance | Info | UseSpanBasedStringConcat, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845) CA1846 | Performance | Info | PreferAsSpanOverSubstring, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846) CA2250 | Usage | Info | UseCancellationTokenThrowIfCancellationRequested, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250) +CA2251 | Usage | Hidden | UseStringEqualsOverStringCompare, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251) ### Removed Rules diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 01e3b55deb..9c032952eb 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -1635,4 +1635,16 @@ Replace 'WhenAll' call with argument + + Use 'string.Equals' + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + Use 'string.Equals' + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs new file mode 100644 index 0000000000..df579cd385 --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +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.Editing; +using Microsoft.CodeAnalysis.Operations; + +using Resx = Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources; +using RequiredSymbols = Microsoft.NetCore.Analyzers.Runtime.UseStringEqualsOverStringCompare.RequiredSymbols; + +namespace Microsoft.NetCore.Analyzers.Runtime +{ + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] + public sealed class UseStringEqualsOverStringCompareFixer : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(UseStringEqualsOverStringCompare.RuleId); + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var token = context.CancellationToken; + var semanticModel = await document.GetSemanticModelAsync(token).ConfigureAwait(false); + + _ = RequiredSymbols.TryGetSymbols(semanticModel.Compilation, out var symbols); + RoslynDebug.Assert(symbols is not null); + + var root = await document.GetSyntaxRootAsync(token).ConfigureAwait(false); + var node = root.FindNode(context.Span, getInnermostNodeForTie: true); + if (semanticModel.GetOperation(node, token) is not IBinaryOperation violation) + return; + + // Get the replacer that applies to the reported violation. + var replacer = GetOperationReplacers(symbols).First(x => x.IsMatch(violation)); + + var codeAction = CodeAction.Create( + Resx.UseStringEqualsOverStringCompareCodeFixTitle, + CreateChangedDocument, + nameof(Resx.UseStringEqualsOverStringCompareCodeFixTitle)); + context.RegisterCodeFix(codeAction, context.Diagnostics); + return; + + // Local functions + + async Task CreateChangedDocument(CancellationToken token) + { + var editor = await DocumentEditor.CreateAsync(document, token).ConfigureAwait(false); + var replacementNode = replacer.CreateReplacementExpression(violation, editor.Generator); + editor.ReplaceNode(violation.Syntax, replacementNode); + + return editor.GetChangedDocument(); + } + } + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + private static ImmutableArray GetOperationReplacers(RequiredSymbols symbols) + { + return ImmutableArray.Create( + new StringStringCaseReplacer(symbols), + new StringStringBoolReplacer(symbols), + new StringStringStringComparisonReplacer(symbols)); + } + + /// + /// Base class for an object that generate the replacement code for a reported violation. + /// + private abstract class OperationReplacer + { + protected OperationReplacer(RequiredSymbols symbols) + { + Symbols = symbols; + } + + protected RequiredSymbols Symbols { get; } + + /// + /// Indicates whether the current applies to the specified violation. + /// + /// The at the location reported by the analyzer. + /// True if the current applies to the specified violation. + public abstract bool IsMatch(IBinaryOperation violation); + + /// + /// Creates a replacement node for a violation that the current applies to. + /// Asserts if the current does not apply to the specified violation. + /// + /// The obtained at the location reported by the analyzer. + /// must return for this operation. + /// + /// + public abstract SyntaxNode CreateReplacementExpression(IBinaryOperation violation, SyntaxGenerator generator); + + protected SyntaxNode CreateEqualsMemberAccess(SyntaxGenerator generator) + { + var stringTypeExpression = generator.TypeExpressionForStaticMemberAccess(Symbols.StringType); + return generator.MemberAccessExpression(stringTypeExpression, nameof(string.Equals)); + } + + protected static IInvocationOperation GetInvocation(IBinaryOperation violation) + { + var result = UseStringEqualsOverStringCompare.GetInvocationFromEqualityCheckWithLiteralZero(violation); + + RoslynDebug.Assert(result is not null); + + return result; + } + + protected static SyntaxNode InvertIfNotEquals(SyntaxNode stringEqualsInvocationExpression, IBinaryOperation equalsOrNotEqualsOperation, SyntaxGenerator generator) + { + return equalsOrNotEqualsOperation.OperatorKind is BinaryOperatorKind.NotEquals ? + generator.LogicalNotExpression(stringEqualsInvocationExpression) : + stringEqualsInvocationExpression; + } + } + + /// + /// Replaces violations. + /// + private sealed class StringStringCaseReplacer : OperationReplacer + { + public StringStringCaseReplacer(RequiredSymbols symbols) + : base(symbols) + { } + + public override bool IsMatch(IBinaryOperation violation) => UseStringEqualsOverStringCompare.IsStringStringCase(violation, Symbols); + + public override SyntaxNode CreateReplacementExpression(IBinaryOperation violation, SyntaxGenerator generator) + { + RoslynDebug.Assert(IsMatch(violation)); + + var compareInvocation = GetInvocation(violation); + var equalsInvocationSyntax = generator.InvocationExpression( + CreateEqualsMemberAccess(generator), + compareInvocation.Arguments.GetArgumentsInParameterOrder().Select(x => x.Value.Syntax)); + + return InvertIfNotEquals(equalsInvocationSyntax, violation, generator); + } + } + + /// + /// Replaces violations. + /// + private sealed class StringStringBoolReplacer : OperationReplacer + { + public StringStringBoolReplacer(RequiredSymbols symbols) + : base(symbols) + { } + + public override bool IsMatch(IBinaryOperation violation) => UseStringEqualsOverStringCompare.IsStringStringBoolCase(violation, Symbols); + + public override SyntaxNode CreateReplacementExpression(IBinaryOperation violation, SyntaxGenerator generator) + { + RoslynDebug.Assert(IsMatch(violation)); + + var compareInvocation = GetInvocation(violation); + + // We know that the 'ignoreCase' argument in 'string.Compare(string, string, bool)' is a boolean literal + // because we've asserted that 'IsMatch' returns true. + var ignoreCaseLiteral = (ILiteralOperation)compareInvocation.Arguments.GetArgumentForParameterAtIndex(2).Value; + + // If the violation contains a call to 'string.Compare(x, y, true)' then we + // replace it with a call to 'string.Equals(x, y, StringComparison.CurrentCultureIgnoreCase)'. + // If the violation contains a call to 'string.Compare(x, y, false)' then we + // replace it with a call to 'string.Equals(x, y, StringComparison.CurrentCulture)'. + var stringComparisonEnumMemberName = (bool)ignoreCaseLiteral.ConstantValue.Value ? + nameof(StringComparison.CurrentCultureIgnoreCase) : + nameof(StringComparison.CurrentCulture); + var stringComparisonMemberAccessSyntax = generator.MemberAccessExpression( + generator.TypeExpressionForStaticMemberAccess(Symbols.StringComparisonType), + stringComparisonEnumMemberName); + + var equalsInvocationSyntax = generator.InvocationExpression( + CreateEqualsMemberAccess(generator), + compareInvocation.Arguments.GetArgumentForParameterAtIndex(0).Value.Syntax, + compareInvocation.Arguments.GetArgumentForParameterAtIndex(1).Value.Syntax, + stringComparisonMemberAccessSyntax); + + return InvertIfNotEquals(equalsInvocationSyntax, violation, generator); + } + } + + /// + /// Replaces violations. + /// + private sealed class StringStringStringComparisonReplacer : OperationReplacer + { + public StringStringStringComparisonReplacer(RequiredSymbols symbols) + : base(symbols) + { } + + public override bool IsMatch(IBinaryOperation violation) => UseStringEqualsOverStringCompare.IsStringStringStringComparisonCase(violation, Symbols); + + public override SyntaxNode CreateReplacementExpression(IBinaryOperation violation, SyntaxGenerator generator) + { + RoslynDebug.Assert(IsMatch(violation)); + + var invocation = GetInvocation(violation); + var equalsInvocationSyntax = generator.InvocationExpression( + CreateEqualsMemberAccess(generator), + invocation.Arguments.GetArgumentsInParameterOrder().Select(x => x.Value.Syntax)); + + return InvertIfNotEquals(equalsInvocationSyntax, violation, generator); + } + } + } +} diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.cs new file mode 100644 index 0000000000..98b59abdfa --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.cs @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +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; +using Resx = Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources; + +namespace Microsoft.NetCore.Analyzers.Runtime +{ + /// + /// Reports a diagnostic on any that: + /// + /// Is an equals or not-equals operation + /// One operand is a literal zero + /// The other operand is an of an eligible + /// string.Compare overload. + /// + /// See all the Is...Case methods to see the string.Compare overloads that are supported. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class UseStringEqualsOverStringCompare : DiagnosticAnalyzer + { + internal const string RuleId = "CA2251"; + + private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(Resx.UseStringEqualsOverStringCompareTitle), Resx.ResourceManager, typeof(Resx)); + private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(Resx.UseStringEqualsOverStringCompareMessage), Resx.ResourceManager, typeof(Resx)); + private static readonly LocalizableString s_localizableDescription = new LocalizableResourceString(nameof(Resx.UseStringEqualsOverStringCompareDescription), Resx.ResourceManager, typeof(Resx)); + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + RuleId, + s_localizableTitle, + s_localizableMessage, + DiagnosticCategory.Usage, + RuleLevel.IdeHidden_BulkConfigurable, + s_localizableDescription, + isPortedFxCopRule: false, + isDataflowRule: false); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols)) + return; + context.RegisterOperationAction(AnalyzeOperation, OperationKind.Binary); + return; + + // Local functions + + void AnalyzeOperation(OperationAnalysisContext context) + { + var operation = (IBinaryOperation)context.Operation; + foreach (var selector in CaseSelectors) + { + if (selector(operation, symbols)) + { + context.ReportDiagnostic(operation.CreateDiagnostic(Rule)); + return; + } + } + } + } + + internal sealed class RequiredSymbols + { + private RequiredSymbols( + INamedTypeSymbol stringType, + INamedTypeSymbol boolType, + INamedTypeSymbol stringComparisonType, + IMethodSymbol? compareStringString, + IMethodSymbol? compareStringStringBool, + IMethodSymbol? compareStringStringStringComparison, + IMethodSymbol? equalsStringString, + IMethodSymbol? equalsStringStringStringComparison) + { + StringType = stringType; + BoolType = boolType; + StringComparisonType = stringComparisonType; + CompareStringString = compareStringString; + CompareStringStringBool = compareStringStringBool; + CompareStringStringStringComparison = compareStringStringStringComparison; + EqualsStringString = equalsStringString; + EqualsStringStringStringComparison = equalsStringStringStringComparison; + } + + public static bool TryGetSymbols(Compilation compilation, [NotNullWhen(true)] out RequiredSymbols? symbols) + { + symbols = default; + + var stringType = compilation.GetSpecialType(SpecialType.System_String); + var boolType = compilation.GetSpecialType(SpecialType.System_Boolean); + + if (stringType is null || boolType is null) + return false; + + if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemStringComparison, out var stringComparisonType)) + return false; + + var compareMethods = stringType.GetMembers(nameof(string.Compare)) + .OfType() + .Where(x => x.IsStatic); + var compareStringString = compareMethods.GetFirstOrDefaultMemberWithParameterTypes(stringType, stringType); + var compareStringStringBool = compareMethods.GetFirstOrDefaultMemberWithParameterTypes(stringType, stringType, boolType); + var compareStringStringStringComparison = compareMethods.GetFirstOrDefaultMemberWithParameterTypes(stringType, stringType, stringComparisonType); + + var equalsMethods = stringType.GetMembers(nameof(string.Equals)) + .OfType() + .Where(x => x.IsStatic); + var equalsStringString = equalsMethods.GetFirstOrDefaultMemberWithParameterTypes(stringType, stringType); + var equalsStringStringStringComparison = equalsMethods.GetFirstOrDefaultMemberWithParameterTypes(stringType, stringType, stringComparisonType); + + // Bail if we do not have at least one complete pair of Compare-Equals methods in the compilation. + if ((compareStringString is null || equalsStringString is null) && + (compareStringStringBool is null || equalsStringStringStringComparison is null) && + (compareStringStringStringComparison is null || equalsStringStringStringComparison is null)) + { + return false; + } + + symbols = new RequiredSymbols( + stringType, boolType, stringComparisonType, + compareStringString, compareStringStringBool, compareStringStringStringComparison, + equalsStringString, equalsStringStringStringComparison); + return true; + } + + public INamedTypeSymbol StringType { get; } + public INamedTypeSymbol BoolType { get; } + public INamedTypeSymbol StringComparisonType { get; } + public IMethodSymbol? CompareStringString { get; } + public IMethodSymbol? CompareStringStringBool { get; } + public IMethodSymbol? CompareStringStringStringComparison { get; } + public IMethodSymbol? EqualsStringString { get; } + public IMethodSymbol? EqualsStringStringStringComparison { get; } + } + + /// + /// If the specified : + /// + /// Is an equals or not-equals operation + /// One operand is a literal zero + /// The other operand is any + /// + /// then this method returns the . + /// Otherwise, returns null. + /// + /// + /// + internal static IInvocationOperation? GetInvocationFromEqualityCheckWithLiteralZero(IBinaryOperation binaryOperation) + { + if (binaryOperation.OperatorKind is not (BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals)) + return default; + + if (IsLiteralZero(binaryOperation.LeftOperand)) + return binaryOperation.RightOperand as IInvocationOperation; + else if (IsLiteralZero(binaryOperation.RightOperand)) + return binaryOperation.LeftOperand as IInvocationOperation; + else + return default; + + // Local functions + + static bool IsLiteralZero(IOperation? operation) + { + return operation is ILiteralOperation literal && literal.ConstantValue.Value is 0; + } + } + + /// + /// Returns true if the specified : + /// + /// Is an equals or not-equals operation + /// One operand is a literal zero + /// The other operand is any invocation of + /// + /// + /// + /// + /// + internal static bool IsStringStringCase(IBinaryOperation binaryOperation, RequiredSymbols symbols) + { + // Don't report a diagnostic if either the string.Compare overload or the + // corrasponding string.Equals overload is missing. + if (symbols.CompareStringString is null || + symbols.EqualsStringString is null) + { + return false; + } + + var invocation = GetInvocationFromEqualityCheckWithLiteralZero(binaryOperation); + + return invocation is not null && + invocation.TargetMethod.Equals(symbols.CompareStringString, SymbolEqualityComparer.Default); + } + + /// + /// Returns true if the specified : + /// + /// Is an equals or not-equals operation + /// One operand is a literal zero + /// The other operand is an invocation of + /// The ignoreCase argument is a boolean literal + /// + /// + /// + /// + /// + internal static bool IsStringStringBoolCase(IBinaryOperation binaryOperation, RequiredSymbols symbols) + { + // Don't report a diagnostic if either the string.Compare overload or the + // corrasponding string.Equals overload is missing. + if (symbols.CompareStringStringBool is null || + symbols.EqualsStringStringStringComparison is null) + { + return false; + } + + var invocation = GetInvocationFromEqualityCheckWithLiteralZero(binaryOperation); + + // Only report a diagnostic if the 'ignoreCase' argument is a boolean literal. + return invocation is not null && + invocation.TargetMethod.Equals(symbols.CompareStringStringBool, SymbolEqualityComparer.Default) && + invocation.Arguments.GetArgumentForParameterAtIndex(2).Value is ILiteralOperation literal && + literal.ConstantValue.Value is bool; + } + + /// + /// Returns true if the specified : + /// + /// Is an equals or not-equals operation + /// One operand is a literal zero + /// The other operand is any invocation of + /// + /// + /// + /// + /// + internal static bool IsStringStringStringComparisonCase(IBinaryOperation binaryOperation, RequiredSymbols symbols) + { + // Don't report a diagnostic if either the string.Compare overload or the + // corrasponding string.Equals overload is missing. + if (symbols.CompareStringStringStringComparison is null || + symbols.EqualsStringStringStringComparison is null) + { + return false; + } + + var invocation = GetInvocationFromEqualityCheckWithLiteralZero(binaryOperation); + + return invocation is not null && + invocation.TargetMethod.Equals(symbols.CompareStringStringStringComparison, SymbolEqualityComparer.Default); + } + + // No IOperation instances are being stored here. +#pragma warning disable RS1008 // Avoid storing per-compilation data into the fields of a diagnostic analyzer + private static readonly ImmutableArray> CaseSelectors = +#pragma warning restore RS1008 // Avoid storing per-compilation data into the fields of a diagnostic analyzer + ImmutableArray.Create>( + IsStringStringCase, + IsStringStringBoolCase, + IsStringStringStringComparisonCase); + } +} diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf index 68812c7030..8d0b4c0415 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -2397,6 +2397,26 @@ Pokud je to možné, zvažte použití řízení přístupu Azure na základě role namísto sdíleného přístupového podpisu (SAS). Pokud i přesto potřebujete používat sdílený přístupový podpis, zadejte SharedAccessProtocol.HttpsOnly. + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf index 0785cce804..06dd0a3669 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -2397,6 +2397,26 @@ Erwägen Sie (sofern möglich) die Verwendung der rollenbasierten Zugriffssteuerung von Azure anstelle einer Shared Access Signature (SAS). Wenn Sie weiterhin eine SAS benötigen, verwenden Sie "SharedAccessProtocol.HttpsOnly". + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf index 0dddd9fbe3..a18be2a983 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -2397,6 +2397,26 @@ Considere la posibilidad de usar el control de acceso basado en rol de Azure en lugar de una firma de acceso compartido (SAS), si es posible. Si tiene que usar una firma de acceso compartido, especifique SharedAccessProtocol.HttpsOnly. + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf index bab5e55d08..69cf068ab7 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -2397,6 +2397,26 @@ Si possible, utilisez le contrôle d'accès en fonction du rôle d'Azure à la place d'une signature d'accès partagé. Si vous devez quand même utiliser une signature d'accès partagé, spécifiez SharedAccessProtocol.HttpsOnly. + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf index 08c50ace13..9e63ed4e7d 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -2397,6 +2397,26 @@ Se possibile, provare a usare il controllo degli accessi in base al ruolo di Azure, invece della firma di accesso condiviso. Se è necessario usare una firma di accesso condiviso, specificare SharedAccessProtocol.HttpsOnly. + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf index e13f8d5717..cd0589990c 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -2397,6 +2397,26 @@ 可能な場合は、Shared Access Signature (SAS) の代わりに、Azure のロールベースのアクセス制御を使用することを検討してください。依然として SAS を使用する必要がある場合は、SharedAccessProtocol.HttpsOnly を指定します。 + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf index e28b4caccc..c7b513992b 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -2397,6 +2397,26 @@ 가능한 경우 SAS(공유 액세스 서명) 대신 Azure의 역할 기반 액세스 제어를 사용하는 것이 좋습니다. 계속 SAS를 사용해야 하는 경우 SharedAccessProtocol.HttpsOnly를 지정하세요. + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' @@ -2509,4 +2529,4 @@ - + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf index 9e552a899d..6271fd5434 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -2397,6 +2397,26 @@ Jeśli to możliwe, rozważ użycie kontroli dostępu opartej na rolach platformy Azure zamiast sygnatury dostępu współdzielonego (SAS). Jeśli nadal chcesz używać sygnatury SAS, określ właściwość SharedAccessProtocol.HttpsOnly. + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Use 'AsSpan' with 'string.Concat' Use 'AsSpan' with 'string.Concat' diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf index 8d9726c1af..2e8e56cd0c 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf @@ -2417,6 +2417,26 @@ Use span-based 'string.Concat' + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Platform compatibility analyzer requires a valid platform name and version. Platform compatibility analyzer requires a valid platform name and version. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf index 3ff7eabf40..12d935ff34 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -2417,6 +2417,26 @@ Use span-based 'string.Concat' + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Platform compatibility analyzer requires a valid platform name and version. Platform compatibility analyzer requires a valid platform name and version. @@ -2509,4 +2529,4 @@ - + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf index 694c65a137..f1bb64bf6f 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -2417,6 +2417,26 @@ Use span-based 'string.Concat' + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Platform compatibility analyzer requires a valid platform name and version. Platform compatibility analyzer requires a valid platform name and version. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf index e2b26bf1fb..097192f666 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf @@ -2417,6 +2417,26 @@ Use span-based 'string.Concat' + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Platform compatibility analyzer requires a valid platform name and version. Platform compatibility analyzer requires a valid platform name and version. diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf index c69cab2ec6..b937d4428f 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf @@ -2417,6 +2417,26 @@ Use span-based 'string.Concat' + + Use 'string.Equals' + Use 'string.Equals' + + + + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + + + + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + Use 'string.Equals' instead of comparing the result of 'string.Compare' to 0 + + + + Use 'string.Equals' + Use 'string.Equals' + + Platform compatibility analyzer requires a valid platform name and version. Platform compatibility analyzer requires a valid platform name and version. diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index cb6537e39b..cd989b22ed 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1968,6 +1968,18 @@ Calls to 'string.IndexOf' where the result is used to check for the presence/abs |CodeFix|True| --- +## [CA2251](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251): Use 'string.Equals' + +It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero. + +|Item|Value| +|-|-| +|Category|Usage| +|Enabled|True| +|Severity|Hidden| +|CodeFix|True| +--- + ## [CA2300](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2300): Do not use insecure deserializer BinaryFormatter The method '{0}' is insecure when deserializing untrusted data. If you need to instead detect BinaryFormatter deserialization without a SerializationBinder set, then disable rule CA2300, and enable rules CA2301 and CA2302. diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif index f67315150f..4543e36fe3 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -3470,6 +3470,26 @@ ] } }, + "CA2251": { + "id": "CA2251", + "shortDescription": "Use 'string.Equals'", + "fullDescription": "It is both clearer and likely faster to use 'string.Equals' instead of comparing the result of 'string.Compare' to zero.", + "defaultLevel": "hidden", + "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251", + "properties": { + "category": "Usage", + "isEnabledByDefault": true, + "typeName": "UseStringEqualsOverStringCompare", + "languages": [ + "C#", + "Visual Basic" + ], + "tags": [ + "Telemetry", + "EnabledRuleInAggressiveMode" + ] + } + }, "CA2300": { "id": "CA2300", "shortDescription": "Do not use insecure deserializer BinaryFormatter", diff --git a/src/NetAnalyzers/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md index 2201c424f8..62c7305ce1 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -11,3 +11,4 @@ CA1843 | | Provide memory-based overrides of async methods when subclassing 'Stream' | CA1845 | | Use span-based 'string.Concat' | CA1846 | | Prefer 'AsSpan' over 'Substring' | +CA2251 | | Use 'string.Equals' | diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompareTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompareTests.cs new file mode 100644 index 0000000000..e3d1e28566 --- /dev/null +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompareTests.cs @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Xunit; + +using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< + Microsoft.NetCore.Analyzers.Runtime.UseStringEqualsOverStringCompare, + Microsoft.NetCore.Analyzers.Runtime.UseStringEqualsOverStringCompareFixer>; +using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier< + Microsoft.NetCore.Analyzers.Runtime.UseStringEqualsOverStringCompare, + Microsoft.NetCore.Analyzers.Runtime.UseStringEqualsOverStringCompareFixer>; + +namespace Microsoft.NetCore.Analyzers.Runtime.UnitTests +{ + public class UseStringEqualsOverStringCompareTests + { + #region Test Data + private static DiagnosticDescriptor Rule => UseStringEqualsOverStringCompare.Rule; + + private static readonly (string CompareCall, string EqualsCall)[] CS_ComparisonEqualityMethodCallPairs = new[] + { + ("string.Compare(x, y)", "string.Equals(x, y)"), + ("string.Compare(x, y, false)", "string.Equals(x, y, StringComparison.CurrentCulture)"), + ("string.Compare(x, y, true)", "string.Equals(x, y, StringComparison.CurrentCultureIgnoreCase)"), + ("string.Compare(x, y, StringComparison.CurrentCulture)", "string.Equals(x, y, StringComparison.CurrentCulture)"), + ("string.Compare(x, y, StringComparison.Ordinal)", "string.Equals(x, y, StringComparison.Ordinal)"), + ("string.Compare(x, y, StringComparison.OrdinalIgnoreCase)", "string.Equals(x, y, StringComparison.OrdinalIgnoreCase)"), + }; + + private static readonly (string CompareCall, string EqualsCall)[] VB_ComparisonEqualityMethodPairs = new[] + { + ("String.Compare(x, y)", "String.Equals(x, y)"), + ("String.Compare(x, y, false)", "String.Equals(x, y, StringComparison.CurrentCulture)"), + ("String.Compare(x, y, true)", "String.Equals(x, y, StringComparison.CurrentCultureIgnoreCase)"), + ("String.Compare(x, y, StringComparison.CurrentCulture)", "String.Equals(x, y, StringComparison.CurrentCulture)"), + ("String.Compare(x, y, StringComparison.Ordinal)", "String.Equals(x, y, StringComparison.Ordinal)"), + ("String.Compare(x, y, StringComparison.OrdinalIgnoreCase)", "String.Equals(x, y, StringComparison.OrdinalIgnoreCase)"), + }; + + public static IEnumerable CS_ComparisonLeftOfLiteralTestData { get; } = CS_ComparisonEqualityMethodCallPairs + .Select(pair => new object[] { $"{pair.CompareCall} == 0", pair.EqualsCall }); + + public static IEnumerable VB_ComparisonLeftOfLiteralTestData { get; } = VB_ComparisonEqualityMethodPairs + .Select(pair => new object[] { $"{pair.CompareCall} = 0", pair.EqualsCall }); + + public static IEnumerable CS_ComparisonRightOfLiteralTestData { get; } = CS_ComparisonEqualityMethodCallPairs + .Select(pair => new object[] { $"0 == {pair.CompareCall}", pair.EqualsCall }); + + public static IEnumerable VB_ComparisonRightOfLiteralTestData { get; } = VB_ComparisonEqualityMethodPairs + .Select(pair => new object[] { $"0 = {pair.CompareCall}", pair.EqualsCall }); + + public static IEnumerable CS_InvertedComparisonLeftOfLiteralTestData { get; } = CS_ComparisonEqualityMethodCallPairs + .Select(pair => new object[] { $"{pair.CompareCall} != 0", $"!{pair.EqualsCall}" }); + + public static IEnumerable VB_InvertedComparisonLeftOfLiteralTestData { get; } = VB_ComparisonEqualityMethodPairs + .Select(pair => new object[] { $"{pair.CompareCall} <> 0", $"Not {pair.EqualsCall}" }); + + public static IEnumerable CS_InvertedComparisonRightOfLiteralTestData { get; } = CS_ComparisonEqualityMethodCallPairs + .Select(pair => new object[] { $"0 != {pair.CompareCall}", $"!{pair.EqualsCall}" }); + + public static IEnumerable VB_InvertedComparisonRightOfLiteralTestData { get; } = VB_ComparisonEqualityMethodPairs + .Select(pair => new object[] { $"0 <> {pair.CompareCall}", $"Not {pair.EqualsCall}" }); + + public static IEnumerable CS_StringCompareExpressionsTestData { get; } = CS_ComparisonEqualityMethodCallPairs + .Select(pair => new object[] { pair.CompareCall }); + + public static IEnumerable VB_StringCompareExpressionsTestData { get; } = VB_ComparisonEqualityMethodPairs + .Select(pair => new object[] { pair.CompareCall }); + + public static IEnumerable CS_IneligibleStringCompareOverloadTestData + { + get + { + yield return new[] { "string.Compare(x, y, true, System.Globalization.CultureInfo.InvariantCulture)" }; + yield return new[] { "string.Compare(x, y, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.CompareOptions.None)" }; + } + } + + public static IEnumerable VB_IneligibleStringCompareOverloadTestData + { + get + { + yield return new[] { "String.Compare(x, y, true, System.Globalization.CultureInfo.InvariantCulture)" }; + yield return new[] { "String.Compare(x, y, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.CompareOptions.None)" }; + } + } + + #endregion + + [Theory] + [MemberData(nameof(CS_ComparisonLeftOfLiteralTestData))] + [MemberData(nameof(CS_ComparisonRightOfLiteralTestData))] + [MemberData(nameof(CS_InvertedComparisonLeftOfLiteralTestData))] + [MemberData(nameof(CS_InvertedComparisonRightOfLiteralTestData))] + public Task StringCompareResult_CompareToZero_Diagnostic_CS(string testExpression, string fixedExpression) + { + string testCode = $@" +using System; + +public class Testopolis +{{ + public bool Huh(string x, string y) + {{ + return {{|#0:{testExpression}|}}; + }} +}}"; + string fixedCode = $@" +using System; + +public class Testopolis +{{ + public bool Huh(string x, string y) + {{ + return {fixedExpression}; + }} +}}"; + + return VerifyCS.VerifyCodeFixAsync(testCode, VerifyCS.Diagnostic(Rule).WithLocation(0), fixedCode); + } + + [Theory] + [MemberData(nameof(VB_ComparisonLeftOfLiteralTestData))] + [MemberData(nameof(VB_ComparisonRightOfLiteralTestData))] + [MemberData(nameof(VB_InvertedComparisonLeftOfLiteralTestData))] + [MemberData(nameof(VB_InvertedComparisonRightOfLiteralTestData))] + public Task StringCompareResult_CompareToZero_Diagnostic_VB(string testExpression, string fixedExpression) + { + string testCode = $@" +Imports System + +Public Class Testopolis + + Public Function Huh(x As String, y As String) As Boolean + Return {{|#0:{testExpression}|}} + End Function +End Class"; + string fixedCode = $@" +Imports System + +Public Class Testopolis + + Public Function Huh(x As String, y As String) As Boolean + Return {fixedExpression} + End Function +End Class"; + + return VerifyVB.VerifyCodeFixAsync(testCode, VerifyVB.Diagnostic(Rule).WithLocation(0), fixedCode); + } + + [Theory] + [MemberData(nameof(CS_StringCompareExpressionsTestData))] + public Task StringCompareResult_CompareToNonLiteralZero_NoDiagnostic_CS(string expression) + { + string code = $@" +using System; + +public class Testopolis +{{ + private const int Zero = 0; + + public void Method(string x, string y) + {{ + bool a = {expression} == Zero; + bool b = {expression} != Zero; + bool c = Zero == {expression}; + bool d = Zero != {expression}; + }} +}}"; + + return VerifyCS.VerifyAnalyzerAsync(code); + } + + [Theory] + [MemberData(nameof(VB_StringCompareExpressionsTestData))] + public Task StringCompareResult_CompareToNonLiteralZero_NoDiagnostic_VB(string expression) + { + var code = $@" +Imports System + +Public Class Testopolis + Private Const Zero As Integer = 0 + + Public Sub Method(x As String, y As String) + Dim a = {expression} = Zero + Dim b = {expression} <> Zero + Dim c = Zero = {expression} + Dim d = Zero <> {expression} + End Sub +End Class"; + + return VerifyVB.VerifyAnalyzerAsync(code); + } + + [Theory] + [MemberData(nameof(CS_StringCompareExpressionsTestData))] + public Task StringCompareResult_CompareToLiteralNonZero_NoDiagnostic_CS(string expression) + { + string code = $@" +using System; + +public class Testopolis +{{ + public void Method(string x, string y) + {{ + bool a = {expression} == 1; + bool b = {expression} != 1; + bool c = 1 == {expression}; + bool d = 1 != {expression}; + }} +}}"; + + return VerifyCS.VerifyAnalyzerAsync(code); + } + + [Theory] + [MemberData(nameof(VB_StringCompareExpressionsTestData))] + public Task StringCompareResult_CompareToLiteralNonZero_NoDiagnostic_VB(string expression) + { + string code = $@" +Imports System + +Public Class Testopolis + Public Sub Method(x As String, y As String) + Dim a = {expression} = 1 + Dim b = {expression} <> 1 + Dim c = 1 = {expression} + Dim d = 1 <> {expression} + End Sub +End Class"; + + return VerifyVB.VerifyAnalyzerAsync(code); + } + + [Theory] + [MemberData(nameof(CS_IneligibleStringCompareOverloadTestData))] + public Task IneligibleStringCompareOverload_NoDiagnostic_CS(string expression) + { + string code = $@" +using System; + +public class Testopolis +{{ + public void Method(string x, string y) + {{ + bool a = {expression} == 0; + bool b = {expression} != 0; + bool c = 0 == {expression}; + bool d = 0 != {expression}; + }} +}}"; + + return VerifyCS.VerifyAnalyzerAsync(code); + } + + [Theory] + [MemberData(nameof(VB_IneligibleStringCompareOverloadTestData))] + public Task IneligibleStringCompareOverload_NoDiagnostic_VB(string expression) + { + string code = $@" +Imports System + +Public Class Testopolis + Public Sub Method(x As String, y As String) + Dim a = {expression} = 0 + Dim b = {expression} <> 0 + Dim c = 0 = {expression} + Dim d = 0 <> {expression} + End Sub +End Class"; + + return VerifyVB.VerifyAnalyzerAsync(code); + } + } +} diff --git a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index fe9aa34aba..fd2c973cdf 100644 --- a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -14,7 +14,7 @@ Globalization: CA2101, CA1300-CA1310 Mobility: CA1600-CA1601 Performance: HA, CA1800-CA1846 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5403 -Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2250 +Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2251 Naming: CA1700-CA1726 Interoperability: CA1400-CA1418 Maintainability: CA1500-CA1509 diff --git a/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs b/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs index 647069c5cf..793ed3902d 100644 --- a/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs +++ b/src/Utilities/Compiler/Extensions/IEnumerableOfIMethodSymbolExtensions.cs @@ -100,6 +100,34 @@ public static IEnumerable GetMethodOverloadsWithDesiredParameterA return GetMethodOverloadsWithDesiredParameterAtLeadingOrTrailing(methods, selectedOverload, expectedTrailingParameterType, trailingOnly: true); } + /// + /// Gets the in the sequence who's parameters match . + /// + /// The sequence of s to search. + /// The types of the parameters, in order. + /// + /// The first in the sequence who's parameters match , or null if + /// no method was found. + /// + public static IMethodSymbol? GetFirstOrDefaultMemberWithParameterTypes(this IEnumerable? members, params ITypeSymbol[] expectedParameterTypesInOrder) + { + return members?.FirstOrDefault(member => + { + if (member.Parameters.Length != expectedParameterTypesInOrder.Length) + return false; + + for (int i = 0; i < expectedParameterTypesInOrder.Length; ++i) + { + var parameterType = member.Parameters[i].Type; + + if (!expectedParameterTypesInOrder[i].Equals(parameterType)) + return false; + } + + return true; + }); + } + /// /// Given a , this method returns the method symbol which /// matches the expectedParameterTypesInOrder parameter requirement