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..e9cc0be020 --- /dev/null +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -0,0 +1,45 @@ +// 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 +{ + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] + public sealed class CSharpUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix + { + 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/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md index cc57137b64..535ba85922 100644 --- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md +++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md @@ -10,6 +10,7 @@ CA1512 | Maintainability | Info | UseExceptionThrowHelpers, [Documentation](http CA1513 | Maintainability | Info | UseExceptionThrowHelpers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1513) CA1856 | Performance | Error | ConstantExpectedAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1856) CA1857 | Performance | Warning | ConstantExpectedAnalyzer, [Documentation](https://learn.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) ### Removed Rules diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 31a1371ce1..71925b2dc4 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -2031,6 +2031,18 @@ 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 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 'IndexOf' + Use ArgumentNullException throw helper @@ -2052,5 +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 new file mode 100644 index 0000000000..836cffdb03 --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.cs @@ -0,0 +1,98 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + public abstract class UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : CodeFixProvider + { + 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; + var diagnostic = context.Diagnostics[0]; + var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var node = root.FindNode(context.Span, getInnermostNodeForTie: true); + + context.RegisterCodeFix( + CodeAction.Create(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle, + 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 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 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 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: + if (compilationHasStartsWithCharOverload) + { + return Task.FromResult(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 Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(replacement)))); + + default: + Debug.Fail("This should never happen."); + return Task.FromResult(document); + } + }, + equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle)), + context.Diagnostics); + } + + protected abstract SyntaxNode HandleCharStringComparisonOverload(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate); + protected abstract SyntaxNode AppendElasticMarker(SyntaxNode replacement); + + 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/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs new file mode 100644 index 0000000000..86badd347f --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZero.cs @@ -0,0 +1,152 @@ +// 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.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 const string CompilationHasStartsWithCharOverloadKey = "CompilationHasStartsWithCharOverload"; + + 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: RuleId, + 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); + var hasAnyStartsWith = false; + var hasStartsWithCharOverload = false; + foreach (var startsWithMethod in stringType.GetMembers("StartsWith").OfType()) + { + hasAnyStartsWith = true; + if (startsWithMethod.Parameters is [{ Type.SpecialType: SpecialType.System_Char }]) + { + hasStartsWithCharOverload = true; + break; + } + } + + if (!hasAnyStartsWith) + { + return; + } + + var indexOfMethodsBuilder = ImmutableArray.CreateBuilder<(IMethodSymbol IndexOfSymbol, string OverloadPropertyValue)>(); + foreach (var indexOfMethod in stringType.GetMembers("IndexOf").OfType()) + { + 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; + } + + var indexOfMethods = indexOfMethodsBuilder.ToImmutable(); + + 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, indexOfMethods, out var additionalLocations, out var properties) || + IsIndexOfComparedWithZero(binaryOperation.RightOperand, binaryOperation.LeftOperand, indexOfMethods, out additionalLocations, out properties)) + { + if (binaryOperation.OperatorKind == BinaryOperatorKind.NotEquals) + { + properties = properties.Add(ShouldNegateKey, ""); + } + + if (hasStartsWithCharOverload) + { + properties = properties.Add(CompilationHasStartsWithCharOverloadKey, ""); + } + + context.ReportDiagnostic(binaryOperation.CreateDiagnostic(Rule, additionalLocations, properties)); + } + }, OperationKind.Binary); + }); + } + + 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, overloadPropertyValue) in indexOfMethods) + { + if (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(); + + properties = properties.Add(ExistingOverloadKey, overloadPropertyValue); + return true; + } + } + } + + additionalLocations = ImmutableArray.Empty; + return false; + } + } +} 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 374f4f46cf..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,6 +3007,26 @@ Preferovat možnost Vymazat před možností Fill + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 e2cb9eef18..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,6 +3007,26 @@ „Clear“ vor „Fill“ bevorzugen + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 dedfb8505e..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,6 +3007,26 @@ Preferir "Clear" en lugar de "Fill" + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 1a2935cb80..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,6 +3007,26 @@ Préférer 'Effacer' à 'Remplissage' + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 4f6e20d388..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,6 +3007,26 @@ Preferisci 'Cancella' a 'Riempimento' + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 de4a2c8be7..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,6 +3007,26 @@ '塗りつぶし' よりも 'クリア' を優先する + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 541498c383..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,6 +3007,26 @@ '채우기'보다 '지우기' 선호 + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 f2a4ffacb9..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,6 +3007,26 @@ Preferuj opcję „Wyczyść” niż „Wypełnij” + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 eacc76591f..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,6 +3007,26 @@ Preferir 'Clear' ao invés de 'Fill' + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 4edf160973..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,6 +3007,26 @@ Предпочитать "Clear" вместо "Fill" + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 497936e666..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,6 +3007,26 @@ 'Fill' yerine 'Clear' tercih et + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 f71adae4d4..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,6 +3007,26 @@ 首选“清除”而不是“填充” + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + 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 a6b9aa3d42..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,6 +3007,26 @@ 優先使用 'Clear' 而不是 'Fill' + + Use 'StartsWith' + Use 'StartsWith' + + + + 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. + + + + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + Use 'StartsWith' instead of comparing the result of 'IndexOf' to 0 + + + + Use 'StartsWith' instead of 'IndexOf' + Use 'StartsWith' instead of 'IndexOf' + + Use 'string.Equals' 請使用 'string.Equals' diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index f39d1a13e9..7a51e0bf08 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1656,6 +1656,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' instead of 'IndexOf' + +It is both clearer and 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 0a572bfd80..b137230d32 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -3075,6 +3075,26 @@ ] } }, + "CA1858": { + "id": "CA1858", + "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", + "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/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md index 259e0c77c6..6d742e2829 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -10,3 +10,4 @@ CA1512 | | Use ObjectDisposedException throw helper | CA1856 | | Incorrect usage of ConstantExpected attribute | CA1857 | | A constant is expected for the parameter | +CA1858 | | 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 new file mode 100644 index 0000000000..41baab4d29 --- /dev/null +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseStartsWithInsteadOfIndexOfComparisonWithZeroTests.cs @@ -0,0 +1,825 @@ +// 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.CSharp.Analyzers.Performance.CSharpUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; + +using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier< + Microsoft.NetCore.Analyzers.Performance.UseStartsWithInsteadOfIndexOfComparisonWithZero, + Microsoft.NetCore.VisualBasic.Analyzers.Performance.BasicUseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix>; + +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, + LanguageVersion = CodeAnalysis.CSharp.LanguageVersion.CSharp8, + }.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() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf("") == 0|]; + } + } + """; + + var fixedCode = """ + class C + { + void M(string a) + { + _ = a.StartsWith(""); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 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 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_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 AndAlso a(0) = "a"c + End Sub + End Class + """; + + var fixedCode21 = """ + Class C + Sub M(a As String) + Dim unused = a.StartsWith("a"c) + End Sub + End Class + """; + + 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] + public async Task CharStringComparison_HardCodedChar_CSharp_Diagnostic() + { + var testCode = """ + class C + { + void M(string a) + { + _ = [|a.IndexOf('a', System.StringComparison.Ordinal) == 0|]; + } + } + """; + + var fixedCode = """ + using System; + + class C + { + void M(string a) + { + _ = a.AsSpan().StartsWith(stackalloc char[1] { + 'a' + }, System.StringComparison.Ordinal); + } + } + """; + + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [Fact] + public async Task CharStringComparison_HardCodedChar_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", 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 + """; + + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixCSAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + + [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 VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard20); + await VerifyCodeFixVBAsync(testCode, fixedCode, ReferenceAssemblies.NetStandard.NetStandard21); + } + } +} 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..4f19b783d3 --- /dev/null +++ b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicUseStartsWithInsteadOfIndexOfComparisonWithZero.Fixer.vb @@ -0,0 +1,35 @@ +' 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.Editing +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +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 + + 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 diff --git a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index 57d0e801ad..44ee358950 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