From 4d68b55eff534c332588b5a63a388e3f26b83240 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 1 Dec 2022 22:58:30 +0200 Subject: [PATCH 01/26] Implement Use 'StartsWith' instead of 'IndexOf' analyzer --- .../Core/AnalyzerReleases.Unshipped.md | 1 + .../MicrosoftNetCoreAnalyzersResources.resx | 9 + ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 46 +++++ ...sWithInsteadOfIndexOfComparisonWithZero.cs | 91 ++++++++++ .../MicrosoftNetCoreAnalyzersResources.cs.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.de.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.es.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.fr.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.it.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.ja.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.ko.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.pl.xlf | 15 ++ ...crosoftNetCoreAnalyzersResources.pt-BR.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.ru.xlf | 15 ++ .../MicrosoftNetCoreAnalyzersResources.tr.xlf | 15 ++ ...osoftNetCoreAnalyzersResources.zh-Hans.xlf | 15 ++ ...osoftNetCoreAnalyzersResources.zh-Hant.xlf | 15 ++ .../Microsoft.CodeAnalysis.NetAnalyzers.md | 12 ++ .../Microsoft.CodeAnalysis.NetAnalyzers.sarif | 20 +++ ...InsteadOfIndexOfComparisonWithZeroTests.cs | 162 ++++++++++++++++++ .../DiagnosticCategoryAndIdRanges.txt | 2 +- 21 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs create mode 100644 src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs create mode 100644 src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs diff --git a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md index 4cb7d58e49..52cc32937e 100644 --- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md +++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md @@ -6,3 +6,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- CA1856 | Performance | Error | ConstantExpectedAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1856) CA1857 | Performance | Warning | ConstantExpectedAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1857) +CA1858 | Performance | Info | UseStartsWithInsteadOfIndexOfComparisonWithZero, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index bd0da36e43..155731c4c9 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -2031,4 +2031,13 @@ Starting with .NET 7 the explicit conversion '{0}' will throw when overflowing in a checked context. Wrap the expression with an 'unchecked' statement to restore the .NET 6 behavior. + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + Use 'StartsWith' + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs new file mode 100644 index 0000000000..a8aa307587 --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] + public sealed class UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseStartsWithInsteadOfIndexOfComparisonWithZero.RuleId); + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var diagnostic = context.Diagnostics[0]; + var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var node = root.FindNode(context.Span, getInnermostNodeForTie: true); + var instance = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); + var argument = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); + + context.RegisterCodeFix( + CodeAction.Create(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle, + createChangedDocument: cancellationToken => + { + var generator = SyntaxGenerator.GetGenerator(document); + var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), argument); + + var shouldNegate = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ShouldNegateKey, out _); + if (shouldNegate) + { + expression = generator.LogicalNotExpression(expression); + } + + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, expression))); + }, + equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle)), + context.Diagnostics); + } + } +} diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs new file mode 100644 index 0000000000..3d5ad5e59b --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +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; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + using static MicrosoftNetCoreAnalyzersResources; + + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class UseStartsWithInsteadOfIndexOfComparisonWithZero : DiagnosticAnalyzer + { + internal const string RuleId = "CA1858"; + internal const string ShouldNegateKey = "ShouldNegate"; + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + id: "CA1858", + title: CreateLocalizableResourceString(nameof(UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle)), + messageFormat: CreateLocalizableResourceString(nameof(UseStartsWithInsteadOfIndexOfComparisonWithZeroMessage)), + category: DiagnosticCategory.Performance, + ruleLevel: RuleLevel.IdeSuggestion, + description: CreateLocalizableResourceString(nameof(UseStartsWithInsteadOfIndexOfComparisonWithZeroDescription)), + isPortedFxCopRule: false, + isDataflowRule: false + ); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(context => + { + var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); + if (stringType.GetMembers("StartsWith").FirstOrDefault() is not IMethodSymbol || + stringType.GetMembers("IndexOf").FirstOrDefault() is not IMethodSymbol indexOfSymbol) + { + return; + } + + context.RegisterOperationAction(context => + { + var binaryOperation = (IBinaryOperation)context.Operation; + if (binaryOperation.OperatorKind is not (BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals)) + { + return; + } + + if (IsIndexOfComparedWithZero(binaryOperation.LeftOperand, binaryOperation.RightOperand, indexOfSymbol, out var instanceLocation, out var argumentLocation) || + IsIndexOfComparedWithZero(binaryOperation.RightOperand, binaryOperation.LeftOperand, indexOfSymbol, out instanceLocation, out argumentLocation)) + { + var properties = ImmutableDictionary.Empty; + if (binaryOperation.OperatorKind == BinaryOperatorKind.NotEquals) + { + properties = properties.Add(ShouldNegateKey, ""); + } + + context.ReportDiagnostic(binaryOperation.CreateDiagnostic(Rule, additionalLocations: ImmutableArray.Create(instanceLocation, argumentLocation), properties: properties)); + } + + }, OperationKind.Binary); + }); + } + + private static bool IsIndexOfComparedWithZero(IOperation left, IOperation right, IMethodSymbol indexOfSymbol, [NotNullWhen(true)] out Location? instanceLocation, [NotNullWhen(true)] out Location? argumentLocation) + { + if (right.ConstantValue is not { HasValue: true, Value: 0 } || + left is not IInvocationOperation invocation || + invocation.Arguments.Length != 1 || + SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, indexOfSymbol)) + { + instanceLocation = null; + argumentLocation = null; + return false; + } + + + instanceLocation = invocation.Instance.Syntax.GetLocation(); + argumentLocation = invocation.Arguments[0].Syntax.GetLocation(); + return true; + } + } +} 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 586e5cbdf5..f8dffb2502 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -2987,6 +2987,21 @@ Preferovat možnost Vymazat před možností Fill + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' Použít string.Equals 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 921b6e398c..bd1f0176fd 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -2987,6 +2987,21 @@ „Clear“ vor „Fill“ bevorzugen + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' "string.Equals" verwenden 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 a7d90b1b4f..892c30c240 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -2987,6 +2987,21 @@ Preferir "Clear" en lugar de "Fill" + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' Use 'string.Equals' 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 8ed699dacb..ca0916b542 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -2987,6 +2987,21 @@ Préférer 'Effacer' à 'Remplissage' + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' Utilisez ’String. Equals 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 65dce268c3..d5106154ce 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -2987,6 +2987,21 @@ Preferisci 'Cancella' a 'Riempimento' + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' Usare 'string.Equals' 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 03e6e2f282..402368a101 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -2987,6 +2987,21 @@ '塗りつぶし' よりも 'クリア' を優先する + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' 'string.Equals' を使用します。 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 4b967e7f1d..e041fad261 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -2987,6 +2987,21 @@ '채우기'보다 '지우기' 선호 + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' 'string.Equals' 사용 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 e99801a948..b49bdde20b 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -2987,6 +2987,21 @@ Preferuj opcję „Wyczyść” niż „Wypełnij” + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' Użyj ciągu „string. Equals” 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 a8704d7bee..377ef40e0c 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 @@ -2987,6 +2987,21 @@ Preferir 'Clear' ao invés de 'Fill' + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' Use 'string. Igual a' 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 6998e6c6cd..861e2c66e7 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -2987,6 +2987,21 @@ Предпочитать "Clear" вместо "Fill" + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' Использовать "string.Equals" 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 e75d89d16d..ffccda1ae5 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -2987,6 +2987,21 @@ 'Fill' yerine 'Clear' tercih et + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' 'string.Equals' kullanın 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 3bc94fbb78..6e9ae69df1 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 @@ -2987,6 +2987,21 @@ 首选“清除”而不是“填充” + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' 使用 “string.Equals” 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 99e32fafd5..f1d2a09e7f 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 @@ -2987,6 +2987,21 @@ 優先使用 'Clear' 而不是 'Fill' + + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' + Use 'StartsWith' + + Use 'string.Equals' 請使用 'string.Equals' diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index 4a50157bff..6ac17fcece 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1608,6 +1608,18 @@ The parameter expects a constant for optimal performance. |CodeFix|False| --- +## [CA1858](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858): Use 'StartsWith' + +It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + +|Item|Value| +|-|-| +|Category|Performance| +|Enabled|True| +|Severity|Info| +|CodeFix|True| +--- + ## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead. diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif index 576fc974e9..e3ee845f68 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -2995,6 +2995,26 @@ ] } }, + "CA1858": { + "id": "CA1858", + "shortDescription": "Use 'StartsWith'", + "fullDescription": "It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero.", + "defaultLevel": "note", + "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858", + "properties": { + "category": "Performance", + "isEnabledByDefault": true, + "typeName": "UseStartsWithInsteadOfIndexOfComparisonWithZero", + "languages": [ + "C#", + "Visual Basic" + ], + "tags": [ + "Telemetry", + "EnabledRuleInAggressiveMode" + ] + } + }, "CA2000": { "id": "CA2000", "shortDescription": "Dispose objects before losing scope", diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs new file mode 100644 index 0000000000..9088e3d321 --- /dev/null +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< + Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZero, + Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; + +using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier< + Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZero, + Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; + +namespace Microsoft.NetCore.Analyzers.Performance.UnitTests +{ + public class UseStartsWithInsteadOfIndexOfComparisonWithZeroTests + { + [Fact] + public async Task SimpleScenario_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf("") == 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith(""); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task SimpleScenario_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf("abc") = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith("abc") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task Negated_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf("abc") != 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = !a.StartsWith("abc"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task Negated_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf("abc") <> 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = Not a.StartsWith("abc") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task InArgument_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + System.Console.WriteLine([|a.IndexOf("abc") != 0|]); + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + System.Console.WriteLine(!a.StartsWith("abc")); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task InArgument_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + System.Console.WriteLine([|a.IndexOf("abc") <> 0|]) + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + System.Console.WriteLine(Not a.StartsWith("abc")) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } + } +} diff --git a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index 00c64a2dd3..449bcc7894 100644 --- a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -12,7 +12,7 @@ Design: CA2210, CA1000-CA1070 Globalization: CA2101, CA1300-CA1311 Mobility: CA1600-CA1601 -Performance: HA, CA1800-CA1857 +Performance: HA, CA1800-CA1858 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5405 Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2260 Naming: CA1700-CA1727 From 3f41d018bc7a6f870e3c016fd0285c3ce65ffde5 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 1 Dec 2022 23:08:13 +0200 Subject: [PATCH 02/26] Support fix all --- ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 3 ++ src/NetAnalyzers/RulesMissingDocumentation.md | 1 + ...InsteadOfIndexOfComparisonWithZeroTests.cs | 52 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index a8aa307587..f7b9cb3794 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -15,6 +15,9 @@ public sealed class UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : Cod { public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseStartsWithInsteadOfIndexOfComparisonWithZero.RuleId); + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var document = context.Document; diff --git a/src/NetAnalyzers/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md index acfaceed87..8a66d28455 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -9,3 +9,4 @@ CA1853 | | Prefer 'Clear' over 'Fill' | CA1856 | | Incorrect usage of ConstantExpected attribute | CA1857 | | A constant is expected for the parameter | +CA1858 | | Use 'StartsWith' | diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index 9088e3d321..dfb686a9b3 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -158,5 +158,57 @@ End Class await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); } + + [Fact] + public async Task FixAll_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf("abc") != 0|]; + _ = [|a.IndexOf("abcd") != 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = !a.StartsWith("abc"); + _ = !a.StartsWith("abcd"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task FixAll_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused1 = [|a.IndexOf("abc") <> 0|] + Dim unused2 = [|a.IndexOf("abcd") <> 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused1 = Not a.StartsWith("abc") + Dim unused2 = Not a.StartsWith("abcd") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } } } From cd66a08767e4a75a1926185e44dfb3ec7e84842f Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 1 Dec 2022 23:14:54 +0200 Subject: [PATCH 03/26] Write a test --- ...InsteadOfIndexOfComparisonWithZeroTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index dfb686a9b3..a3463c01fc 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -210,5 +210,53 @@ End Class await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); } + + [Fact] + public async Task FixAllNested_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf(([|"abc2".IndexOf("abc3") == 0|]).ToString()) == 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith(("abc2".StartsWith("abc3")).ToString()); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task FixAllNested_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf(([|"abc2".IndexOf("abc3") = 0|]).ToString()) = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith(("abc2".StartsWith("abc3")).ToString()) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } } } From 64c5793210ce14699441818b703b528dda5ff5e3 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Fri, 2 Dec 2022 08:52:47 +0200 Subject: [PATCH 04/26] address feedback, more tests --- ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 11 +- ...sWithInsteadOfIndexOfComparisonWithZero.cs | 58 +++++-- ...InsteadOfIndexOfComparisonWithZeroTests.cs | 150 ++++++++++++++++++ 3 files changed, 200 insertions(+), 19 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index f7b9cb3794..22f8ab0652 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -24,15 +24,20 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnostic = context.Diagnostics[0]; var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var node = root.FindNode(context.Span, getInnermostNodeForTie: true); - var instance = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); - var argument = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); context.RegisterCodeFix( CodeAction.Create(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle, createChangedDocument: cancellationToken => { + var instance = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); + var arguments = new SyntaxNode[diagnostic.AdditionalLocations.Count - 1]; + for (int i = 1; i < diagnostic.AdditionalLocations.Count; i++) + { + arguments[i - 1] = root.FindNode(diagnostic.AdditionalLocations[i].SourceSpan); + } + var generator = SyntaxGenerator.GetGenerator(document); - var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), argument); + var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), arguments); var shouldNegate = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ShouldNegateKey, out _); if (shouldNegate) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index 3d5ad5e59b..9d3831534b 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -40,12 +40,34 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(context => { var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); + var indexOf = stringType.GetMembers("IndexOf").OfType(); + var indexOfString = indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }]); + var indexOfChar = indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]); + var indexOfStringStringComparison = indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }]); if (stringType.GetMembers("StartsWith").FirstOrDefault() is not IMethodSymbol || - stringType.GetMembers("IndexOf").FirstOrDefault() is not IMethodSymbol indexOfSymbol) + (indexOfString is null && indexOfChar is null && indexOfStringStringComparison is null)) { return; } + var indexOfMethodsBuilder = ImmutableArray.CreateBuilder(); + if (indexOfChar is not null) + { + indexOfMethodsBuilder.Add(indexOfChar); + } + + if (indexOfString is not null) + { + indexOfMethodsBuilder.Add(indexOfString); + } + + if (indexOfStringStringComparison is not null) + { + indexOfMethodsBuilder.Add(indexOfStringStringComparison); + } + + var indexOfMethods = indexOfMethodsBuilder.ToImmutable(); + context.RegisterOperationAction(context => { var binaryOperation = (IBinaryOperation)context.Operation; @@ -54,8 +76,8 @@ public override void Initialize(AnalysisContext context) return; } - if (IsIndexOfComparedWithZero(binaryOperation.LeftOperand, binaryOperation.RightOperand, indexOfSymbol, out var instanceLocation, out var argumentLocation) || - IsIndexOfComparedWithZero(binaryOperation.RightOperand, binaryOperation.LeftOperand, indexOfSymbol, out instanceLocation, out argumentLocation)) + if (IsIndexOfComparedWithZero(binaryOperation.LeftOperand, binaryOperation.RightOperand, indexOfMethods, out var additionalLocations) || + IsIndexOfComparedWithZero(binaryOperation.RightOperand, binaryOperation.LeftOperand, indexOfMethods, out additionalLocations)) { var properties = ImmutableDictionary.Empty; if (binaryOperation.OperatorKind == BinaryOperatorKind.NotEquals) @@ -63,29 +85,33 @@ public override void Initialize(AnalysisContext context) properties = properties.Add(ShouldNegateKey, ""); } - context.ReportDiagnostic(binaryOperation.CreateDiagnostic(Rule, additionalLocations: ImmutableArray.Create(instanceLocation, argumentLocation), properties: properties)); + context.ReportDiagnostic(binaryOperation.CreateDiagnostic(Rule, additionalLocations: additionalLocations, properties: properties)); } }, OperationKind.Binary); }); } - private static bool IsIndexOfComparedWithZero(IOperation left, IOperation right, IMethodSymbol indexOfSymbol, [NotNullWhen(true)] out Location? instanceLocation, [NotNullWhen(true)] out Location? argumentLocation) + private static bool IsIndexOfComparedWithZero(IOperation left, IOperation right, ImmutableArray indexOfMethods, out ImmutableArray additionalLocations) { - if (right.ConstantValue is not { HasValue: true, Value: 0 } || - left is not IInvocationOperation invocation || - invocation.Arguments.Length != 1 || - SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, indexOfSymbol)) + if (right.ConstantValue is { HasValue: true, Value: 0 } && + left is IInvocationOperation invocation) { - instanceLocation = null; - argumentLocation = null; - return false; + foreach (var indexOfMethod in indexOfMethods) + { + if (indexOfMethod.Parameters.Length == invocation.Arguments.Length && indexOfMethod.Equals(invocation.TargetMethod, SymbolEqualityComparer.Default)) + { + var locationsBuilder = ImmutableArray.CreateBuilder(); + locationsBuilder.Add(invocation.Instance.Syntax.GetLocation()); + locationsBuilder.AddRange(invocation.Arguments.Select(arg => arg.Syntax.GetLocation())); + additionalLocations = locationsBuilder.ToImmutable(); + return true; + } + } } - - instanceLocation = invocation.Instance.Syntax.GetLocation(); - argumentLocation = invocation.Arguments[0].Syntax.GetLocation(); - return true; + additionalLocations = ImmutableArray.Empty; + return false; } } } diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index a3463c01fc..8168287f6a 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -63,6 +63,54 @@ End Class await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); } + [Fact] + public async Task ZeroOnLeft_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|0 == a.IndexOf("")|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith(""); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task ZeroOnLeft_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|0 = a.IndexOf("abc")|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith("abc") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } + [Fact] public async Task Negated_CSharp_Diagnostic() { @@ -258,5 +306,107 @@ End Class await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); } + + [Fact] + public async Task StringStringComparison_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf("abc", System.StringComparison.Ordinal) == 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith("abc", System.StringComparison.Ordinal); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task StringStringComparison_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf("abc", System.StringComparison.Ordinal) = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith("abc", System.StringComparison.Ordinal) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task OutOfOrderNamedArguments_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf(comparisonType: System.StringComparison.Ordinal, value: "abc") == 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith(comparisonType: System.StringComparison.Ordinal, value: "abc"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task OutOfOrderNamedArguments_VB_Diagnostic() + { + // IInvocationOperation.Arguments appears to behave differently in C# vs VB. + // In C#, the order of arguments are preserved, as they appear in source. + // In VB, the order of arguments is the same as parameters order. + // If we wanted to make VB behavior similar to OutOfOrderNamedArguments_CSharp_Diagnostic, we will need + // to go back to syntax. This scenario doesn't seem important/common, so might be good for now until + // we hear any user feedback. + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf(comparisonType:=System.StringComparison.Ordinal, value:="abc") = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith(value:="abc", comparisonType:=System.StringComparison.Ordinal) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + } } } From 1af0b0e2dc94e8cb5f3330504282ad87508a46f8 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Fri, 2 Dec 2022 09:11:57 +0200 Subject: [PATCH 05/26] Remove unnecessary using --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index 9d3831534b..1193453f70 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; From 7df3273fbe9657cc1e16d1f351bd5de068868263 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 7 Dec 2022 11:58:51 +0200 Subject: [PATCH 06/26] Simplify --- ...sWithInsteadOfIndexOfComparisonWithZero.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index 1193453f70..a364ee34bf 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -50,20 +50,9 @@ public override void Initialize(AnalysisContext context) } var indexOfMethodsBuilder = ImmutableArray.CreateBuilder(); - if (indexOfChar is not null) - { - indexOfMethodsBuilder.Add(indexOfChar); - } - - if (indexOfString is not null) - { - indexOfMethodsBuilder.Add(indexOfString); - } - - if (indexOfStringStringComparison is not null) - { - indexOfMethodsBuilder.Add(indexOfStringStringComparison); - } + AddIfNotNull(indexOfMethodsBuilder, indexOfChar); + AddIfNotNull(indexOfMethodsBuilder, indexOfString); + AddIfNotNull(indexOfMethodsBuilder, indexOfStringStringComparison); var indexOfMethods = indexOfMethodsBuilder.ToImmutable(); @@ -88,6 +77,14 @@ public override void Initialize(AnalysisContext context) } }, OperationKind.Binary); + + static void AddIfNotNull(ImmutableArray.Builder builder, IMethodSymbol? symbol) + { + if (symbol is not null) + { + builder.Add(symbol); + } + } }); } From 01eaaf4fc72f8e2a248ebb0cba05c83888903e6c Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 7 Dec 2022 12:03:57 +0200 Subject: [PATCH 07/26] Refactor --- ...tsWithInsteadOfIndexOfComparisonWithZero.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index a364ee34bf..2f2fb4a2d5 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -39,20 +39,20 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(context => { var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); - var indexOf = stringType.GetMembers("IndexOf").OfType(); - var indexOfString = indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }]); - var indexOfChar = indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]); - var indexOfStringStringComparison = indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }]); - if (stringType.GetMembers("StartsWith").FirstOrDefault() is not IMethodSymbol || - (indexOfString is null && indexOfChar is null && indexOfStringStringComparison is null)) + if (stringType.GetMembers("StartsWith").FirstOrDefault() is not IMethodSymbol) { return; } + var indexOf = stringType.GetMembers("IndexOf").OfType(); var indexOfMethodsBuilder = ImmutableArray.CreateBuilder(); - AddIfNotNull(indexOfMethodsBuilder, indexOfChar); - AddIfNotNull(indexOfMethodsBuilder, indexOfString); - AddIfNotNull(indexOfMethodsBuilder, indexOfStringStringComparison); + AddIfNotNull(indexOfMethodsBuilder, indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }])); + AddIfNotNull(indexOfMethodsBuilder, indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }])); + AddIfNotNull(indexOfMethodsBuilder, indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }])); + if (indexOfMethodsBuilder.Count == 0) + { + return; + } var indexOfMethods = indexOfMethodsBuilder.ToImmutable(); From 2ce999908cbf136ef90d3c0ae82c29a00cf53077 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 7 Dec 2022 23:59:42 +0200 Subject: [PATCH 08/26] wip --- ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 34 +++- ...sWithInsteadOfIndexOfComparisonWithZero.cs | 41 +++-- ...InsteadOfIndexOfComparisonWithZeroTests.cs | 167 ++++++++++++++++-- 3 files changed, 208 insertions(+), 34 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index 22f8ab0652..edc78f1939 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Composition; +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; @@ -37,15 +38,38 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) } var generator = SyntaxGenerator.GetGenerator(document); - var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), arguments); - var shouldNegate = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ShouldNegateKey, out _); - if (shouldNegate) + + _ = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ExistingOverloadKey, out var overloadValue); + switch (overloadValue) { - expression = generator.LogicalNotExpression(expression); + // For 'IndexOf(string)' and 'IndexOf(string, stringComparison)', we replace with StartsWith(same arguments) + case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString: + case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString_StringComparison: + var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), arguments); + if (shouldNegate) + { + expression = generator.LogicalNotExpression(expression); + } + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, expression))); + + // For 'a.IndexOf(ch, stringComparison)', we use 'a.StartsWith(stackalloc char[1] { ch }, stringComparison)' + // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) + case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: + // TODO: + return Task.FromResult(document); + + // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' + case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar: + // TODO: + return Task.FromResult(document); + + default: + // Should never happen. + Debug.Fail("This should never happen."); + return Task.FromResult(document); } - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, expression))); }, equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle)), context.Diagnostics); diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index 2f2fb4a2d5..209b4709e9 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -18,6 +18,13 @@ public sealed class UseStartsWithInsteadOfIndexOfComparisonWithZero : Diagnostic internal const string RuleId = "CA1858"; internal const string ShouldNegateKey = "ShouldNegate"; + internal const string ExistingOverloadKey = "ExistingOverload"; + + internal const string OverloadString = "String"; + internal const string OverloadString_StringComparison = "String,StringComparison"; + internal const string OverloadChar = "Char"; + internal const string OverloadChar_StringComparison = "Char,StringComparison"; + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( id: "CA1858", title: CreateLocalizableResourceString(nameof(UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle)), @@ -45,10 +52,11 @@ public override void Initialize(AnalysisContext context) } var indexOf = stringType.GetMembers("IndexOf").OfType(); - var indexOfMethodsBuilder = ImmutableArray.CreateBuilder(); - AddIfNotNull(indexOfMethodsBuilder, indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }])); - AddIfNotNull(indexOfMethodsBuilder, indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }])); - AddIfNotNull(indexOfMethodsBuilder, indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }])); + var indexOfMethodsBuilder = ImmutableArray.CreateBuilder<(IMethodSymbol IndexOfSymbol, string OverloadPropertyValue)>(); + AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }]), OverloadString)); + AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]), OverloadChar)); + AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }]), OverloadString_StringComparison)); + AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }, { Name: "comparisonType" }]), OverloadChar_StringComparison)); if (indexOfMethodsBuilder.Count == 0) { return; @@ -64,36 +72,41 @@ public override void Initialize(AnalysisContext context) return; } - if (IsIndexOfComparedWithZero(binaryOperation.LeftOperand, binaryOperation.RightOperand, indexOfMethods, out var additionalLocations) || - IsIndexOfComparedWithZero(binaryOperation.RightOperand, binaryOperation.LeftOperand, indexOfMethods, out additionalLocations)) + if (IsIndexOfComparedWithZero(binaryOperation.LeftOperand, binaryOperation.RightOperand, indexOfMethods, out var additionalLocations, out var properties) || + IsIndexOfComparedWithZero(binaryOperation.RightOperand, binaryOperation.LeftOperand, indexOfMethods, out additionalLocations, out properties)) { - var properties = ImmutableDictionary.Empty; if (binaryOperation.OperatorKind == BinaryOperatorKind.NotEquals) { properties = properties.Add(ShouldNegateKey, ""); } - context.ReportDiagnostic(binaryOperation.CreateDiagnostic(Rule, additionalLocations: additionalLocations, properties: properties)); + context.ReportDiagnostic(binaryOperation.CreateDiagnostic(Rule, additionalLocations, properties)); } }, OperationKind.Binary); - static void AddIfNotNull(ImmutableArray.Builder builder, IMethodSymbol? symbol) + void AddIfNotNull((IMethodSymbol? Symbol, string OverloadPropertyValue) item) { - if (symbol is not null) + if (item.Symbol is not null) { - builder.Add(symbol); + indexOfMethodsBuilder.Add((item.Symbol, item.OverloadPropertyValue)); } } }); } - private static bool IsIndexOfComparedWithZero(IOperation left, IOperation right, ImmutableArray indexOfMethods, out ImmutableArray additionalLocations) + private static bool IsIndexOfComparedWithZero( + IOperation left, IOperation right, + ImmutableArray<(IMethodSymbol Symbol, string OverloadPropertyValue)> indexOfMethods, + out ImmutableArray additionalLocations, + out ImmutableDictionary properties) { + properties = ImmutableDictionary.Empty; + if (right.ConstantValue is { HasValue: true, Value: 0 } && left is IInvocationOperation invocation) { - foreach (var indexOfMethod in indexOfMethods) + foreach (var (indexOfMethod, overloadPropertyValue) in indexOfMethods) { if (indexOfMethod.Parameters.Length == invocation.Arguments.Length && indexOfMethod.Equals(invocation.TargetMethod, SymbolEqualityComparer.Default)) { @@ -101,6 +114,8 @@ private static bool IsIndexOfComparedWithZero(IOperation left, IOperation right, locationsBuilder.Add(invocation.Instance.Syntax.GetLocation()); locationsBuilder.AddRange(invocation.Arguments.Select(arg => arg.Syntax.GetLocation())); additionalLocations = locationsBuilder.ToImmutable(); + + properties = properties.Add(ExistingOverloadKey, overloadPropertyValue); return true; } } diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index 8168287f6a..0a06a9ab64 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -15,6 +15,26 @@ namespace Microsoft.NetCore.Analyzers.Performance.UnitTests { public class UseStartsWithInsteadOfIndexOfComparisonWithZeroTests { + private static async Task VerifyCodeFixVBAsync(string source, string fixedSource, ReferenceAssemblies referenceAssemblies) + { + await new VerifyVB.Test + { + TestCode = source, + FixedCode = fixedSource, + ReferenceAssemblies = referenceAssemblies, + }.RunAsync(); + } + + private static async Task VerifyCodeFixCSAsync(string source, string fixedSource, ReferenceAssemblies referenceAssemblies) + { + await new VerifyCS.Test + { + TestCode = source, + FixedCode = fixedSource, + ReferenceAssemblies = referenceAssemblies, + }.RunAsync(); + } + [Fact] public async Task SimpleScenario_CSharp_Diagnostic() { @@ -38,7 +58,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -60,7 +81,8 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -86,7 +108,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -108,7 +131,8 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -134,7 +158,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -156,7 +181,8 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -182,7 +208,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -204,7 +231,8 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -232,7 +260,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -256,7 +285,8 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -282,7 +312,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -304,7 +335,8 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -330,7 +362,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -352,9 +385,109 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task Char_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf('a') == 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith('a'); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task Char_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf("a"c) = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith("a"c) + End Sub + End Class + """; + + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf('a', System.StringComparison.Ordinal) == 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith('a', System.StringComparison.Ordinal); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf("a"c, System.StringComparison.Ordinal) = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith("a"c, System.StringComparison.Ordinal) + End Sub + End Class + """; + + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } + // TODO: Add OutOfOrderNamedArguments test for IndexOf(char, StringComparison) overload. [Fact] public async Task OutOfOrderNamedArguments_CSharp_Diagnostic() { @@ -378,7 +511,8 @@ void M(string a) } """; - await VerifyCS.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -406,7 +540,8 @@ End Sub End Class """; - await VerifyVB.VerifyCodeFixAsync(testCode, fixedCode); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } } } From c81a3e316f701e9185287c26ac9cd6f3a66588a3 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 8 Dec 2022 01:20:48 +0200 Subject: [PATCH 09/26] wip --- ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 17 +++ ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 50 ++++++--- ...InsteadOfIndexOfComparisonWithZeroTests.cs | 102 ++++++++++++++++-- ...nsteadOfIndexOfComparisonWithZero.Fixer.vb | 19 ++++ 4 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs create mode 100644 src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs new file mode 100644 index 0000000000..ada4279db9 --- /dev/null +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.NetCore.Analyzers.Performance; + +namespace Microsoft.NetCore.CSharp.Analyzers.Performance +{ + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] + public sealed class CSharpUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix + { + protected override SyntaxNode AppendElasticMarker(SyntaxNode replacement) + => replacement.WithTrailingTrivia(SyntaxFactory.ElasticMarker); + } +} diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index edc78f1939..16cc1f2f19 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Composition; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; @@ -11,8 +12,7 @@ namespace Microsoft.NetCore.Analyzers.Performance { - [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] - public sealed class UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : CodeFixProvider + public abstract class UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseStartsWithInsteadOfIndexOfComparisonWithZero.RuleId); @@ -28,7 +28,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle, - createChangedDocument: cancellationToken => + createChangedDocument: async cancellationToken => { var instance = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); var arguments = new SyntaxNode[diagnostic.AdditionalLocations.Count - 1]; @@ -46,33 +46,57 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // For 'IndexOf(string)' and 'IndexOf(string, stringComparison)', we replace with StartsWith(same arguments) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString: case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString_StringComparison: - var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), arguments); - if (shouldNegate) - { - expression = generator.LogicalNotExpression(expression); - } - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, expression))); + return document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate))); // For 'a.IndexOf(ch, stringComparison)', we use 'a.StartsWith(stackalloc char[1] { ch }, stringComparison)' // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: // TODO: - return Task.FromResult(document); + return document; // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' + // For negation, we use '.Length == 0 || [0] != ch' case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar: - // TODO: - return Task.FromResult(document); + var semanticModel = await document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + var startsWithSymbols = semanticModel.Compilation.GetSpecialType(SpecialType.System_String).GetMembers("StartsWith"); + if (startsWithSymbols.Any(s => s is IMethodSymbol { Parameters: [{ Type.SpecialType: SpecialType.System_Char }] })) + { + return document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate))); + } + + var lengthAccess = generator.MemberAccessExpression(instance, "Length"); + var zeroLiteral = generator.LiteralExpression(0); + + var indexed = generator.ElementAccessExpression(instance, zeroLiteral); + var ch = root.FindNode(arguments[0].Span, getInnermostNodeForTie: true); + + var replacement = shouldNegate + ? generator.LogicalOrExpression( + generator.ValueEqualsExpression(lengthAccess, zeroLiteral), + generator.ValueNotEqualsExpression(indexed, ch)) + : generator.LogicalAndExpression( + generator.GreaterThanExpression(lengthAccess, zeroLiteral), + generator.ValueEqualsExpression(indexed, ch)); + + return document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(replacement))); default: // Should never happen. Debug.Fail("This should never happen."); - return Task.FromResult(document); + return document; } }, equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle)), context.Diagnostics); } + + protected abstract SyntaxNode AppendElasticMarker(SyntaxNode replacement); + + private static SyntaxNode CreateStartsWithInvocationFromArguments(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate) + { + var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), arguments); + return shouldNegate ? generator.LogicalNotExpression(expression) : expression; + } } } diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index 0a06a9ab64..3005d57973 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -5,11 +5,11 @@ using Xunit; using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZero, - Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; + Microsoft.NetCore.CSharp.Analyzers.Performance.CSharpUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier< Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZero, - Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; + Microsoft.NetCore.VisualBasic.Analyzers.Performance.BasicUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; namespace Microsoft.NetCore.Analyzers.Performance.UnitTests { @@ -402,7 +402,17 @@ void M(string a) } """; - var fixedCode = """ + var fixedCode20 = """ + class C + { + void M(string a) + { + _ = a.Length > 0 && a[0] == 'a'; + } + } + """; + + var fixedCode21 = """ class C { void M(string a) @@ -412,8 +422,8 @@ void M(string a) } """; - await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); - await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + await VerifyCodeFixCSAsync(testCode, fixedCode20, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode21, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] @@ -427,7 +437,15 @@ End Sub End Class """; - var fixedCode = """ + var fixedCode20 = """ + Class C + Sub M(a As String) + Dim unused = a.Length > 0 AndAlso a(0) = "a"c + End Sub + End Class + """; + + var fixedCode21 = """ Class C Sub M(a As String) Dim unused = a.StartsWith("a"c) @@ -435,8 +453,76 @@ End Sub End Class """; - await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); - await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + await VerifyCodeFixVBAsync(testCode, fixedCode20, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode21, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task Char_Negation_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf('a') != 0|]; + } + } + """; + + var fixedCode20 = """ + class C + { + void M(string a) + { + _ = a.Length == 0 || a[0] != 'a'; + } + } + """; + + var fixedCode21 = """ + class C + { + void M(string a) + { + _ = !a.StartsWith('a'); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode20, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode21, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task Char_Negation_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf("a"c) <> 0|] + End Sub + End Class + """; + + var fixedCode20 = """ + Class C + Sub M(a As String) + Dim unused = a.Length = 0 OrElse a(0) <> "a"c + End Sub + End Class + """; + + var fixedCode21 = """ + Class C + Sub M(a As String) + Dim unused = Not a.StartsWith("a"c) + End Sub + End Class + """; + + await VerifyCodeFixVBAsync(testCode, fixedCode20, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode21, ReferenceAssemblies.NetStandard.NetStandard21); } [Fact] diff --git a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb new file mode 100644 index 0000000000..0918918448 --- /dev/null +++ b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb @@ -0,0 +1,19 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +Imports System.Composition +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeFixes +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.NetCore.Analyzers.Performance + +Namespace Microsoft.NetCore.VisualBasic.Analyzers.Performance + + + Public NotInheritable Class BasicUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix + Inherits UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix + + Protected Overrides Function AppendElasticMarker(replacement As SyntaxNode) As SyntaxNode + Return replacement.WithTrailingTrivia(SyntaxFactory.ElasticMarker) + End Function + End Class +End Namespace \ No newline at end of file From 451ba14d35eb8a2e68b1324c988340041649169b Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 8 Dec 2022 01:29:05 +0200 Subject: [PATCH 10/26] Redundant comment --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index 16cc1f2f19..e63d6289db 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -81,7 +81,6 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) return document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(replacement))); default: - // Should never happen. Debug.Fail("This should never happen."); return document; } From 4c43d0f1c9f1536bc971bb1d200805e020feca46 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 8 Dec 2022 09:59:46 +0200 Subject: [PATCH 11/26] Remove unused using directive --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index e63d6289db..d38face63f 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Immutable; -using System.Composition; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; From 400cde76e6dd387baba8c2872a0bf84ed3f2ed8a Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Fri, 9 Dec 2022 12:23:19 +0200 Subject: [PATCH 12/26] Handle some scenarios, fix tests, and add more tests --- ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 28 +++ ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 9 +- ...InsteadOfIndexOfComparisonWithZeroTests.cs | 170 +++++++++++++++++- ...nsteadOfIndexOfComparisonWithZero.Fixer.vb | 17 ++ 4 files changed, 216 insertions(+), 8 deletions(-) diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index ada4279db9..4c041d91bb 100644 --- a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. using System.Composition; +using Analyzer.Utilities; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; using Microsoft.NetCore.Analyzers.Performance; namespace Microsoft.NetCore.CSharp.Analyzers.Performance @@ -13,5 +16,30 @@ public sealed class CSharpUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix { protected override SyntaxNode AppendElasticMarker(SyntaxNode replacement) => replacement.WithTrailingTrivia(SyntaxFactory.ElasticMarker); + + protected override SyntaxNode HandleCharStringComparisonOverload(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate) + { + // For 'x.IndexOf(ch, stringComparison)', we switch to 'x.AsSpan().StartsWith(stackalloc char[1] { ch }, stringComparison)' + var (argumentSyntax, index) = GetCharacterArgumentAndIndex(arguments); + arguments[index] = argumentSyntax.WithExpression(SyntaxFactory.StackAllocArrayCreationExpression( + SyntaxFactory.ArrayType( + (TypeSyntax)generator.TypeExpression(SpecialType.System_Char), + SyntaxFactory.SingletonList(SyntaxFactory.ArrayRankSpecifier(SyntaxFactory.SingletonSeparatedList((ExpressionSyntax)generator.LiteralExpression(1))))), + SyntaxFactory.InitializerExpression(SyntaxKind.ArrayInitializerExpression, SyntaxFactory.SingletonSeparatedList(argumentSyntax.Expression)) + )); + instance = generator.InvocationExpression(generator.MemberAccessExpression(instance, "AsSpan")).WithAdditionalAnnotations(new SyntaxAnnotation("SymbolId", "System.MemoryExtensions")).WithAddImportsAnnotation(); + return CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate); + } + + private static (ArgumentSyntax Argument, int Index) GetCharacterArgumentAndIndex(SyntaxNode[] arguments) + { + var firstArgument = (ArgumentSyntax)arguments[0]; + if (firstArgument.NameColon is null or { Name.Identifier.Value: "value"}) + { + return (firstArgument, 0); + } + + return ((ArgumentSyntax)arguments[1], 1); + } } } diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index d38face63f..b7bff2dde1 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -47,11 +47,13 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString_StringComparison: return document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate))); - // For 'a.IndexOf(ch, stringComparison)', we use 'a.StartsWith(stackalloc char[1] { ch }, stringComparison)' + // For 'a.IndexOf(ch, stringComparison)': + // C#: Use 'a.AsSpan().StartsWith(stackalloc char[1] { ch }, stringComparison)' // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) + // VB: Use a.StartsWith(c.ToString(), stringComparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: // TODO: - return document; + return document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate))); // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' // For negation, we use '.Length == 0 || [0] != ch' @@ -89,9 +91,10 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) context.Diagnostics); } + protected abstract SyntaxNode HandleCharStringComparisonOverload(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate); protected abstract SyntaxNode AppendElasticMarker(SyntaxNode replacement); - private static SyntaxNode CreateStartsWithInvocationFromArguments(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate) + protected static SyntaxNode CreateStartsWithInvocationFromArguments(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate) { var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), arguments); return shouldNegate ? generator.LogicalNotExpression(expression) : expression; diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index 3005d57973..4b7ff8baf8 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -32,6 +32,7 @@ private static async Task VerifyCodeFixCSAsync(string source, string fixedSource TestCode = source, FixedCode = fixedSource, ReferenceAssemblies = referenceAssemblies, + LanguageVersion = CodeAnalysis.CSharp.LanguageVersion.CSharp8, }.RunAsync(); } @@ -526,7 +527,7 @@ End Class } [Fact] - public async Task CharStringComparison_CSharp_Diagnostic() + public async Task CharStringComparison_HardCodedChar_CSharp_Diagnostic() { var testCode = """ class C @@ -539,11 +540,15 @@ void M(string a) """; var fixedCode = """ + using System; + class C { void M(string a) { - _ = a.StartsWith('a', System.StringComparison.Ordinal); + _ = a.AsSpan().StartsWith(stackalloc char[1] { + 'a' + }, System.StringComparison.Ordinal); } } """; @@ -552,7 +557,7 @@ void M(string a) } [Fact] - public async Task CharStringComparison_VB_Diagnostic() + public async Task CharStringComparison_HardCodedChar_VB_Diagnostic() { var testCode = """ Class C @@ -565,7 +570,163 @@ End Class var fixedCode = """ Class C Sub M(a As String) - Dim unused = a.StartsWith("a"c, System.StringComparison.Ordinal) + Dim unused = a.StartsWith("a", System.StringComparison.Ordinal) + End Sub + End Class + """; + + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_Expression_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a, char exp) + { + _ = [|a.IndexOf(exp, System.StringComparison.Ordinal) == 0|]; + } + } + """; + + var fixedCode = """ + using System; + + class C + { + void M(string a, char exp) + { + _ = a.AsSpan().StartsWith(stackalloc char[1] { + exp + }, System.StringComparison.Ordinal); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_Expression_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String, exp As Char) + Dim unused = [|a.IndexOf(exp, System.StringComparison.Ordinal) = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String, exp As Char) + Dim unused = a.StartsWith(exp.ToString(), System.StringComparison.Ordinal) + End Sub + End Class + """; + + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_HardCodedChar_OutOfOrder_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf(comparisonType: System.StringComparison.Ordinal, value: 'a') == 0|]; + } + } + """; + + var fixedCode = """ + using System; + + class C + { + void M(string a) + { + _ = a.AsSpan().StartsWith(comparisonType: System.StringComparison.Ordinal, value: stackalloc char[1] { + 'a' + }); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_HardCodedChar_OutOfOrder_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = [|a.IndexOf(comparisonType:=System.StringComparison.Ordinal, value:="a"c) = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith(value:="a", comparisonType:=System.StringComparison.Ordinal) + End Sub + End Class + """; + + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_Expression_OutOfOrder_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a, char exp) + { + _ = [|a.IndexOf(comparisonType: System.StringComparison.Ordinal, value: exp) == 0|]; + } + } + """; + + var fixedCode = """ + using System; + + class C + { + void M(string a, char exp) + { + _ = a.AsSpan().StartsWith(comparisonType: System.StringComparison.Ordinal, value: stackalloc char[1] { + exp + }); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_Expression_OutOfOrder_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String, exp As Char) + Dim unused = [|a.IndexOf(comparisonType:=System.StringComparison.Ordinal, value:=exp) = 0|] + End Sub + End Class + """; + + var fixedCode = """ + Class C + Sub M(a As String, exp As Char) + Dim unused = a.StartsWith(value:=exp.ToString(), comparisonType:=System.StringComparison.Ordinal) End Sub End Class """; @@ -573,7 +734,6 @@ End Class await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); } - // TODO: Add OutOfOrderNamedArguments test for IndexOf(char, StringComparison) overload. [Fact] public async Task OutOfOrderNamedArguments_CSharp_Diagnostic() { diff --git a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb index 0918918448..55313f89fb 100644 --- a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb +++ b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb @@ -3,7 +3,9 @@ Imports System.Composition Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.CodeFixes +Imports Microsoft.CodeAnalysis.Editing Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Imports Microsoft.NetCore.Analyzers.Performance Namespace Microsoft.NetCore.VisualBasic.Analyzers.Performance @@ -15,5 +17,20 @@ Namespace Microsoft.NetCore.VisualBasic.Analyzers.Performance Protected Overrides Function AppendElasticMarker(replacement As SyntaxNode) As SyntaxNode Return replacement.WithTrailingTrivia(SyntaxFactory.ElasticMarker) End Function + + Protected Overrides Function HandleCharStringComparisonOverload(generator As SyntaxGenerator, instance As SyntaxNode, arguments As SyntaxNode(), shouldNegate As Boolean) As SyntaxNode + Dim charArgumentSyntax = DirectCast(arguments(0), SimpleArgumentSyntax) + If charArgumentSyntax.Expression.IsKind(SyntaxKind.CharacterLiteralExpression) Then + ' For 'x.IndexOf(hardCodedConstantChar, stringComparison) == 0', switch to x.StartsWith(hardCodedString, stringComparison) + Dim charValueAsString = DirectCast(charArgumentSyntax.Expression, LiteralExpressionSyntax).Token.Value.ToString() + arguments(0) = charArgumentSyntax.WithExpression(DirectCast(generator.LiteralExpression(charValueAsString), ExpressionSyntax)) + Else + ' The character isn't a hard-coded constant, it's some expression. We call `.ToString()` on it. + arguments(0) = charArgumentSyntax.WithExpression(DirectCast(generator.InvocationExpression(generator.MemberAccessExpression(charArgumentSyntax.Expression, "ToString")), ExpressionSyntax)) + End If + + + Return CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate) + End Function End Class End Namespace \ No newline at end of file From c9817ffe993747fc365acbbc9bd2f9bd8b3af9aa Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Fri, 9 Dec 2022 12:24:05 +0200 Subject: [PATCH 13/26] Remove TODO --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index b7bff2dde1..dfa0412c4c 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -52,7 +52,6 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) // VB: Use a.StartsWith(c.ToString(), stringComparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: - // TODO: return document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate))); // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' From 5b5026e972706aba6601190037ffb13b658a0562 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Fri, 9 Dec 2022 12:58:06 +0200 Subject: [PATCH 14/26] Fix formatting --- ...harpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index 4c041d91bb..e9cc0be020 100644 --- a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -34,7 +34,7 @@ protected override SyntaxNode HandleCharStringComparisonOverload(SyntaxGenerator private static (ArgumentSyntax Argument, int Index) GetCharacterArgumentAndIndex(SyntaxNode[] arguments) { var firstArgument = (ArgumentSyntax)arguments[0]; - if (firstArgument.NameColon is null or { Name.Identifier.Value: "value"}) + if (firstArgument.NameColon is null or { Name.Identifier.Value: "value" }) { return (firstArgument, 0); } From e7b76a0faaf4018a95dc9759af9e2a6b212c017d Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 07:25:00 +0200 Subject: [PATCH 15/26] Address review comments --- .../MicrosoftNetCoreAnalyzersResources.resx | 9 ++- ...nsteadOfIndexOfComparisonWithZero.Fixer.cs | 22 +++---- ...sWithInsteadOfIndexOfComparisonWithZero.cs | 65 ++++++++++++++----- .../MicrosoftNetCoreAnalyzersResources.cs.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.de.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.es.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.fr.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.it.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.ja.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.ko.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.pl.xlf | 13 ++-- ...crosoftNetCoreAnalyzersResources.pt-BR.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.ru.xlf | 13 ++-- .../MicrosoftNetCoreAnalyzersResources.tr.xlf | 13 ++-- ...osoftNetCoreAnalyzersResources.zh-Hans.xlf | 13 ++-- ...osoftNetCoreAnalyzersResources.zh-Hant.xlf | 13 ++-- ...InsteadOfIndexOfComparisonWithZeroTests.cs | 32 +++++++++ 17 files changed, 213 insertions(+), 84 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 41d3d2d443..71925b2dc4 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -2031,14 +2031,17 @@ Starting with .NET 7 the explicit conversion '{0}' will throw when overflowing in a checked context. Wrap the expression with an 'unchecked' statement to restore the .NET 6 behavior. + + Use 'StartsWith' + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' Use ArgumentNullException throw helper @@ -2061,4 +2064,4 @@ Use '{0}.{1}' - \ No newline at end of file + diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index dfa0412c4c..abb495506b 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -26,8 +26,8 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) var node = root.FindNode(context.Span, getInnermostNodeForTie: true); context.RegisterCodeFix( - CodeAction.Create(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle, - createChangedDocument: async cancellationToken => + CodeAction.Create(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle, + createChangedDocument: cancellationToken => { var instance = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan); var arguments = new SyntaxNode[diagnostic.AdditionalLocations.Count - 1]; @@ -38,30 +38,28 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) var generator = SyntaxGenerator.GetGenerator(document); var shouldNegate = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ShouldNegateKey, out _); - + var compilationHasStartsWithCharOverload = diagnostic.Properties.TryGetKey(UseStartsWithInsteadOfIndexOfComparisonWithZero.CompilationHasStartsWithCharOverloadKey, out _); _ = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ExistingOverloadKey, out var overloadValue); switch (overloadValue) { // For 'IndexOf(string)' and 'IndexOf(string, stringComparison)', we replace with StartsWith(same arguments) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString: case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString_StringComparison: - return document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate)))); // For 'a.IndexOf(ch, stringComparison)': // C#: Use 'a.AsSpan().StartsWith(stackalloc char[1] { ch }, stringComparison)' // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) // VB: Use a.StartsWith(c.ToString(), stringComparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: - return document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate)))); // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' // For negation, we use '.Length == 0 || [0] != ch' case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar: - var semanticModel = await document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - var startsWithSymbols = semanticModel.Compilation.GetSpecialType(SpecialType.System_String).GetMembers("StartsWith"); - if (startsWithSymbols.Any(s => s is IMethodSymbol { Parameters: [{ Type.SpecialType: SpecialType.System_Char }] })) + if (compilationHasStartsWithCharOverload) { - return document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate)))); } var lengthAccess = generator.MemberAccessExpression(instance, "Length"); @@ -78,15 +76,15 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) generator.GreaterThanExpression(lengthAccess, zeroLiteral), generator.ValueEqualsExpression(indexed, ch)); - return document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(replacement))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(replacement)))); default: Debug.Fail("This should never happen."); - return document; + return Task.FromResult(document); } }, - equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle)), + equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle)), context.Diagnostics); } diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index 209b4709e9..b535f1c05c 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -17,6 +17,7 @@ public sealed class UseStartsWithInsteadOfIndexOfComparisonWithZero : Diagnostic { internal const string RuleId = "CA1858"; internal const string ShouldNegateKey = "ShouldNegate"; + internal const string CompilationHasStartsWithCharOverloadKey = "CompilationHasStartsWithCharOverload"; internal const string ExistingOverloadKey = "ExistingOverload"; @@ -26,7 +27,7 @@ public sealed class UseStartsWithInsteadOfIndexOfComparisonWithZero : Diagnostic internal const string OverloadChar_StringComparison = "Char,StringComparison"; internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( - id: "CA1858", + id: RuleId, title: CreateLocalizableResourceString(nameof(UseStartsWithInsteadOfIndexOfComparisonWithZeroTitle)), messageFormat: CreateLocalizableResourceString(nameof(UseStartsWithInsteadOfIndexOfComparisonWithZeroMessage)), category: DiagnosticCategory.Performance, @@ -46,17 +47,51 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(context => { var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); - if (stringType.GetMembers("StartsWith").FirstOrDefault() is not IMethodSymbol) + var hasAnyStartsWith = false; + var hasStartsWithCharOverload = false; + foreach (var startsWith in stringType.GetMembers("StartsWith")) + { + if (startsWith is IMethodSymbol startsWithMethod) + { + hasAnyStartsWith = true; + if (startsWithMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]) + { + hasStartsWithCharOverload = true; + break; + } + } + + } + + if (!hasAnyStartsWith) { return; } - var indexOf = stringType.GetMembers("IndexOf").OfType(); var indexOfMethodsBuilder = ImmutableArray.CreateBuilder<(IMethodSymbol IndexOfSymbol, string OverloadPropertyValue)>(); - AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }]), OverloadString)); - AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]), OverloadChar)); - AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }]), OverloadString_StringComparison)); - AddIfNotNull((indexOf.SingleOrDefault(s => s.Parameters is [{ Type.SpecialType: SpecialType.System_Char }, { Name: "comparisonType" }]), OverloadChar_StringComparison)); + foreach (var indexOf in stringType.GetMembers("IndexOf")) + { + if (indexOf is IMethodSymbol indexOfMethod) + { + if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_String }]) + { + indexOfMethodsBuilder.Add((indexOfMethod, OverloadString)); + } + else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]) + { + indexOfMethodsBuilder.Add((indexOfMethod, OverloadChar)); + } + else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }]) + { + indexOfMethodsBuilder.Add((indexOfMethod, OverloadString_StringComparison)); + } + else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }, { Name: "comparisonType" }]) + { + indexOfMethodsBuilder.Add((indexOfMethod, OverloadChar_StringComparison)); + } + } + } + if (indexOfMethodsBuilder.Count == 0) { return; @@ -80,18 +115,14 @@ public override void Initialize(AnalysisContext context) properties = properties.Add(ShouldNegateKey, ""); } + if (hasStartsWithCharOverload) + { + properties = properties.Add(CompilationHasStartsWithCharOverloadKey, ""); + } + context.ReportDiagnostic(binaryOperation.CreateDiagnostic(Rule, additionalLocations, properties)); } - }, OperationKind.Binary); - - void AddIfNotNull((IMethodSymbol? Symbol, string OverloadPropertyValue) item) - { - if (item.Symbol is not null) - { - indexOfMethodsBuilder.Add((item.Symbol, item.OverloadPropertyValue)); - } - } }); } @@ -108,7 +139,7 @@ private static bool IsIndexOfComparedWithZero( { foreach (var (indexOfMethod, overloadPropertyValue) in indexOfMethods) { - if (indexOfMethod.Parameters.Length == invocation.Arguments.Length && indexOfMethod.Equals(invocation.TargetMethod, SymbolEqualityComparer.Default)) + if (indexOfMethod.Equals(invocation.TargetMethod, SymbolEqualityComparer.Default)) { var locationsBuilder = ImmutableArray.CreateBuilder(); locationsBuilder.Add(invocation.Instance.Syntax.GetLocation()); 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 e0b28f9569..88a3f7ce13 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -3007,9 +3007,14 @@ Preferovat možnost Vymazat před možností Fill + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 03772781fe..8e47a40332 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -3007,9 +3007,14 @@ „Clear“ vor „Fill“ bevorzugen + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 e118f362b1..ff962dd654 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -3007,9 +3007,14 @@ Preferir "Clear" en lugar de "Fill" + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 a94fe0bfb2..b0b9ffaefa 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -3007,9 +3007,14 @@ Préférer 'Effacer' à 'Remplissage' + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 963bfc5130..7ce91bfef0 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -3007,9 +3007,14 @@ Preferisci 'Cancella' a 'Riempimento' + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 2115e9a946..b807d29e7c 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -3007,9 +3007,14 @@ '塗りつぶし' よりも 'クリア' を優先する + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 fe6f73bc26..2a18e7d5a3 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -3007,9 +3007,14 @@ '채우기'보다 '지우기' 선호 + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 aa2c43c37a..1e59184dd2 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -3007,9 +3007,14 @@ Preferuj opcję „Wyczyść” niż „Wypełnij” + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 3078c26ee3..e948ebf455 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 @@ -3007,9 +3007,14 @@ Preferir 'Clear' ao invés de 'Fill' + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 38781f12d0..9a7ae915f4 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -3007,9 +3007,14 @@ Предпочитать "Clear" вместо "Fill" + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 78de1d1c33..b205b90c26 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -3007,9 +3007,14 @@ 'Fill' yerine 'Clear' tercih et + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 9f7d15c491..ea17fb5d07 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 @@ -3007,9 +3007,14 @@ 首选“清除”而不是“填充” + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' 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 714ec3180f..69a07d0587 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 @@ -3007,9 +3007,14 @@ 優先使用 'Clear' 而不是 'Fill' + + Use 'StartsWith' + Use 'StartsWith' + + - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. - It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. + It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. @@ -3018,8 +3023,8 @@ - Use 'StartsWith' - Use 'StartsWith' + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index 4b7ff8baf8..41baab4d29 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -36,6 +36,38 @@ private static async Task VerifyCodeFixCSAsync(string source, string fixedSource }.RunAsync(); } + [Fact] + public async Task GreaterThanZero_CSharp_NoDiagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = a.IndexOf("") > 0; + } + } + """; + + await VerifyCodeFixCSAsync(testCode, testCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, testCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task GreaterThanZero_VB_Diagnostic() + { + var testCode = """ + Class C + Sub M(a As String) + Dim unused = a.IndexOf("abc") > 0 + End Sub + End Class + """; + + await VerifyCodeFixVBAsync(testCode, testCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, testCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + [Fact] public async Task SimpleScenario_CSharp_Diagnostic() { From 6605ad0328f92fc30405d4283384b57826268769 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 07:59:46 +0200 Subject: [PATCH 16/26] Fix build --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 2 -- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.cs | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index abb495506b..836cffdb03 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -2,7 +2,6 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; @@ -82,7 +81,6 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) Debug.Fail("This should never happen."); return Task.FromResult(document); } - }, equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle)), context.Diagnostics); diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index b535f1c05c..47fc0c9ad2 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -59,8 +59,7 @@ public override void Initialize(AnalysisContext context) hasStartsWithCharOverload = true; break; } - } - + } } if (!hasAnyStartsWith) From ebb49f17acb7bc5e4c9839a2377e9595a6878d79 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Sat, 17 Dec 2022 10:08:43 +0200 Subject: [PATCH 17/26] Fix formatting --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index 47fc0c9ad2..6a7060ce1a 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -59,7 +59,7 @@ public override void Initialize(AnalysisContext context) hasStartsWithCharOverload = true; break; } - } + } } if (!hasAnyStartsWith) From e4dc78d603f2796721c119c9fc89531fb8e4ca66 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Sat, 17 Dec 2022 12:47:06 +0200 Subject: [PATCH 18/26] Fix build --- ...sicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb index 55313f89fb..4f19b783d3 100644 --- a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb +++ b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb @@ -29,8 +29,7 @@ Namespace Microsoft.NetCore.VisualBasic.Analyzers.Performance arguments(0) = charArgumentSyntax.WithExpression(DirectCast(generator.InvocationExpression(generator.MemberAccessExpression(charArgumentSyntax.Expression, "ToString")), ExpressionSyntax)) End If - Return CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate) End Function End Class -End Namespace \ No newline at end of file +End Namespace From e42cc0845380e512f80c72861019cee2579c9514 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Sat, 17 Dec 2022 13:31:44 +0200 Subject: [PATCH 19/26] Try with elastic marker --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index 836cffdb03..e9b9047c57 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -51,7 +51,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) // VB: Use a.StartsWith(c.ToString(), stringComparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate)))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate))))); // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' // For negation, we use '.Length == 0 || [0] != ch' From 58ce1d62924ccf24f003417c16cd2ff0a782a77c Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 13:32:01 +0200 Subject: [PATCH 20/26] Update generated files --- src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md | 2 +- src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index 39eff2effe..34d8cd80b2 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1658,7 +1658,7 @@ The parameter expects a constant for optimal performance. ## [CA1858](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858): Use 'StartsWith' -It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. +It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. |Item|Value| |-|-| diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif index a1e1087294..0a3002f289 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -3078,7 +3078,7 @@ "CA1858": { "id": "CA1858", "shortDescription": "Use 'StartsWith'", - "fullDescription": "It is both clearer and likely faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero.", + "fullDescription": "It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero.", "defaultLevel": "note", "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858", "properties": { From 167b826f3a9f34275e6fd120e4942474487a576b Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 13:33:51 +0200 Subject: [PATCH 21/26] Revert "Try with elastic marker" This reverts commit e42cc0845380e512f80c72861019cee2579c9514. --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index e9b9047c57..836cffdb03 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -51,7 +51,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) // VB: Use a.StartsWith(c.ToString(), stringComparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate))))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate)))); // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' // For negation, we use '.Length == 0 || [0] != ch' From 437db8203169c36a12f69bde58d7d3dc2e4190f6 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 13:35:06 +0200 Subject: [PATCH 22/26] Update test --- .../UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index 41baab4d29..fdfae33580 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -562,6 +562,8 @@ End Class public async Task CharStringComparison_HardCodedChar_CSharp_Diagnostic() { var testCode = """ + using System.Collections.Generic; // Intentionally unused. + class C { void M(string a) @@ -573,6 +575,7 @@ void M(string a) var fixedCode = """ using System; + using System.Collections.Generic; // Intentionally unused. class C { From d0a198df55388593aaae2cd1d5a98180d48e5c1d Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 14:19:28 +0200 Subject: [PATCH 23/26] Run pack --- src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md | 2 +- src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif | 2 +- src/NetAnalyzers/RulesMissingDocumentation.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index 34d8cd80b2..7a51e0bf08 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1656,7 +1656,7 @@ The parameter expects a constant for optimal performance. |CodeFix|False| --- -## [CA1858](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858): Use 'StartsWith' +## [CA1858](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858): Use 'StartsWith' instead of 'IndexOf' It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero. diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif index 0a3002f289..b137230d32 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -3077,7 +3077,7 @@ }, "CA1858": { "id": "CA1858", - "shortDescription": "Use 'StartsWith'", + "shortDescription": "Use 'StartsWith' instead of 'IndexOf'", "fullDescription": "It is both clearer and faster to use 'StartsWith' instead of comparing the result of 'IndexOf' to zero.", "defaultLevel": "note", "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858", diff --git a/src/NetAnalyzers/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md index 748ff872ca..6d742e2829 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -10,4 +10,4 @@ CA1512 | | Use ObjectDisposedException throw helper | CA1856 | | Incorrect usage of ConstantExpected attribute | CA1857 | | A constant is expected for the parameter | -CA1858 | | Use 'StartsWith' | +CA1858 | | Use 'StartsWith' instead of 'IndexOf' | From b1ad36e979f5b45f088e822aa90a122cbf5f3e34 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 15:59:54 +0200 Subject: [PATCH 24/26] Revert "Revert "Try with elastic marker"" This reverts commit 167b826f3a9f34275e6fd120e4942474487a576b. --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index 836cffdb03..e9b9047c57 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -51,7 +51,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) // VB: Use a.StartsWith(c.ToString(), stringComparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate)))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate))))); // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' // For negation, we use '.Length == 0 || [0] != ch' From c358bfb175fd7dd16f4bcb77ae96d2f05dcc53c4 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sat, 17 Dec 2022 16:20:52 +0200 Subject: [PATCH 25/26] revert --- .../UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs | 2 +- .../UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs index e9b9047c57..836cffdb03 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -51,7 +51,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison) // VB: Use a.StartsWith(c.ToString(), stringComparison) case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison: - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate))))); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate)))); // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch' // For negation, we use '.Length == 0 || [0] != ch' diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs index fdfae33580..41baab4d29 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -562,8 +562,6 @@ End Class public async Task CharStringComparison_HardCodedChar_CSharp_Diagnostic() { var testCode = """ - using System.Collections.Generic; // Intentionally unused. - class C { void M(string a) @@ -575,7 +573,6 @@ void M(string a) var fixedCode = """ using System; - using System.Collections.Generic; // Intentionally unused. class C { From d22ec17501bbe6a15550d834ead039a87eddfc2c Mon Sep 17 00:00:00 2001 From: Buyaa Namnan Date: Mon, 19 Dec 2022 14:33:18 -0800 Subject: [PATCH 26/26] Apply suggestions from code review --- ...sWithInsteadOfIndexOfComparisonWithZero.cs | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs index 6a7060ce1a..86badd347f 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -49,16 +49,13 @@ public override void Initialize(AnalysisContext context) var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); var hasAnyStartsWith = false; var hasStartsWithCharOverload = false; - foreach (var startsWith in stringType.GetMembers("StartsWith")) + foreach (var startsWithMethod in stringType.GetMembers("StartsWith").OfType()) { - if (startsWith is IMethodSymbol startsWithMethod) + hasAnyStartsWith = true; + if (startsWithMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]) { - hasAnyStartsWith = true; - if (startsWithMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]) - { - hasStartsWithCharOverload = true; - break; - } + hasStartsWithCharOverload = true; + break; } } @@ -68,26 +65,23 @@ public override void Initialize(AnalysisContext context) } var indexOfMethodsBuilder = ImmutableArray.CreateBuilder<(IMethodSymbol IndexOfSymbol, string OverloadPropertyValue)>(); - foreach (var indexOf in stringType.GetMembers("IndexOf")) + foreach (var indexOfMethod in stringType.GetMembers("IndexOf").OfType()) { - if (indexOf is IMethodSymbol indexOfMethod) + if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_String }]) { - if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_String }]) - { - indexOfMethodsBuilder.Add((indexOfMethod, OverloadString)); - } - else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]) - { - indexOfMethodsBuilder.Add((indexOfMethod, OverloadChar)); - } - else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }]) - { - indexOfMethodsBuilder.Add((indexOfMethod, OverloadString_StringComparison)); - } - else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }, { Name: "comparisonType" }]) - { - indexOfMethodsBuilder.Add((indexOfMethod, OverloadChar_StringComparison)); - } + indexOfMethodsBuilder.Add((indexOfMethod, OverloadString)); + } + else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]) + { + indexOfMethodsBuilder.Add((indexOfMethod, OverloadChar)); + } + else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_String }, { Name: "comparisonType" }]) + { + indexOfMethodsBuilder.Add((indexOfMethod, OverloadString_StringComparison)); + } + else if (indexOfMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }, { Name: "comparisonType" }]) + { + indexOfMethodsBuilder.Add((indexOfMethod, OverloadChar_StringComparison)); } }