diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpDoNotGuardSetAddOrRemoveByContains.Fixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpDoNotGuardSetAddOrRemoveByContains.Fixer.cs new file mode 100644 index 0000000000..9a7e7a3bd1 --- /dev/null +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpDoNotGuardSetAddOrRemoveByContains.Fixer.cs @@ -0,0 +1,71 @@ +// 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 System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.NetCore.Analyzers.Performance; + +namespace Microsoft.NetCore.CSharp.Analyzers.Performance +{ + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] + public sealed class CSharpDoNotGuardSetAddOrRemoveByContainsFixer : DoNotGuardSetAddOrRemoveByContainsFixer + { + protected override bool SyntaxSupportedByFixer(SyntaxNode conditionalSyntax, SyntaxNode childStatementSyntax) + { + if (childStatementSyntax is not ExpressionStatementSyntax) + { + return false; + } + + if (conditionalSyntax is IfStatementSyntax ifStatementSyntax) + { + var addOrRemoveInElse = childStatementSyntax.Parent is ElseClauseSyntax || childStatementSyntax.Parent?.Parent is ElseClauseSyntax; + + return addOrRemoveInElse + ? ifStatementSyntax.Else?.Statement.ChildNodes().Count() == 1 + : ifStatementSyntax.Statement.ChildNodes().Count() == 1; + } + + return false; + } + + protected override Document ReplaceConditionWithChild(Document document, SyntaxNode root, SyntaxNode conditionalOperationNode, SyntaxNode childOperationNode) + { + SyntaxNode newRoot; + + if (conditionalOperationNode is IfStatementSyntax { Else: not null } ifStatementSyntax) + { + var expression = GetNegatedExpression(document, childOperationNode); + var addOrRemoveInElse = childOperationNode.Parent is ElseClauseSyntax || childOperationNode.Parent?.Parent is ElseClauseSyntax; + + SyntaxNode newConditionalOperationNode = ifStatementSyntax + .WithCondition((ExpressionSyntax)expression) + .WithStatement(addOrRemoveInElse ? ifStatementSyntax.Statement : ifStatementSyntax.Else.Statement) + .WithElse(null) + .WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(conditionalOperationNode); + + newRoot = root.ReplaceNode(conditionalOperationNode, newConditionalOperationNode); + } + else + { + SyntaxNode newConditionNode = childOperationNode + .WithAdditionalAnnotations(Formatter.Annotation) + .WithTriviaFrom(conditionalOperationNode); + + newRoot = root.ReplaceNode(conditionalOperationNode, newConditionNode); + } + + return document.WithSyntaxRoot(newRoot); + } + + private static SyntaxNode GetNegatedExpression(Document document, SyntaxNode newConditionNode) + { + var generator = SyntaxGenerator.GetGenerator(document); + return generator.LogicalNotExpression(((ExpressionStatementSyntax)newConditionNode).Expression.WithoutTrivia()); + } + } +} diff --git a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md index 9aa5aac34a..ecd6860b49 100644 --- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md +++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md @@ -7,6 +7,7 @@ Rule ID | Category | Severity | Notes CA1865 | Performance | Info | UseStringMethodCharOverloadWithSingleCharacters, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1865) CA1866 | Performance | Info | UseStringMethodCharOverloadWithSingleCharacters, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1866) CA1867 | Performance | Disabled | UseStringMethodCharOverloadWithSingleCharacters, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1867) +CA1868 | Performance | Info | DoNotGuardSetAddOrRemoveByContains, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1865) CA2261 | Usage | Warning | DoNotUseConfigureAwaitWithSuppressThrowing, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250) CA1510 | Maintainability | Info | UseExceptionThrowHelpers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1510) CA1511 | Maintainability | Info | UseExceptionThrowHelpers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1511) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index 3217de6122..b4746a5b3e 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -1509,6 +1509,15 @@ Unnecessary call to 'Dictionary.ContainsKey(key)' + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + Do not guard '{0}' with '{1}' + + + Unnecessary call to 'Contains(item)' + Remove unnecessary call diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.Fixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.Fixer.cs new file mode 100644 index 0000000000..27e6dbd01d --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.Fixer.cs @@ -0,0 +1,60 @@ +// 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.Threading.Tasks; +using Analyzer.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + public abstract class DoNotGuardSetAddOrRemoveByContainsFixer : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DoNotGuardSetAddOrRemoveByContains.RuleId); + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var node = root.FindNode(context.Span, getInnermostNodeForTie: true); + + if (node is null) + { + return; + } + + var diagnostic = context.Diagnostics[0]; + var conditionalLocation = diagnostic.AdditionalLocations[0]; + var childLocation = diagnostic.AdditionalLocations[1]; + + if (root.FindNode(conditionalLocation.SourceSpan) is not SyntaxNode conditionalSyntax || + root.FindNode(childLocation.SourceSpan) is not SyntaxNode childStatementSyntax) + { + return; + } + + if (!SyntaxSupportedByFixer(conditionalSyntax, childStatementSyntax)) + { + return; + } + + var codeAction = CodeAction.Create(MicrosoftNetCoreAnalyzersResources.RemoveRedundantGuardCallCodeFixTitle, + ct => Task.FromResult(ReplaceConditionWithChild(context.Document, root, conditionalSyntax, childStatementSyntax)), + nameof(MicrosoftNetCoreAnalyzersResources.DoNotGuardSetAddOrRemoveByContainsTitle)); + + context.RegisterCodeFix(codeAction, diagnostic); + } + + protected abstract bool SyntaxSupportedByFixer(SyntaxNode conditionalSyntax, SyntaxNode childStatementSyntax); + + protected abstract Document ReplaceConditionWithChild(Document document, SyntaxNode root, + SyntaxNode conditionalOperationNode, + SyntaxNode childOperationNode); + } +} diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.cs new file mode 100644 index 0000000000..c958faf5c3 --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.cs @@ -0,0 +1,357 @@ +// 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 Analyzer.Utilities.PooledObjects; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.NetCore.Analyzers.Performance +{ + using static MicrosoftNetCoreAnalyzersResources; + + /// + /// CA1868: + /// + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class DoNotGuardSetAddOrRemoveByContains : DiagnosticAnalyzer + { + internal const string RuleId = "CA1868"; + + private const string Contains = nameof(Contains); + private const string Add = nameof(Add); + private const string Remove = nameof(Remove); + + // Build custom format instead of CSharpShortErrorMessageFormat/VisualBasicShortErrorMessageFormat to prevent unhelpful messages for VB. + private static readonly SymbolDisplayFormat s_symbolDisplayFormat = SymbolDisplayFormat.MinimallyQualifiedFormat + .WithParameterOptions(SymbolDisplayParameterOptions.IncludeType) + .WithGenericsOptions(SymbolDisplayGenericsOptions.None) + .WithMemberOptions(SymbolDisplayMemberOptions.IncludeParameters | SymbolDisplayMemberOptions.IncludeContainingType) + .WithKindOptions(SymbolDisplayKindOptions.None); + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + RuleId, + CreateLocalizableResourceString(nameof(DoNotGuardSetAddOrRemoveByContainsTitle)), + CreateLocalizableResourceString(nameof(DoNotGuardSetAddOrRemoveByContainsMessage)), + DiagnosticCategory.Performance, + RuleLevel.IdeSuggestion, + CreateLocalizableResourceString(nameof(DoNotGuardSetAddOrRemoveByContainsDescription)), + isPortedFxCopRule: false, + isDataflowRule: false); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private void OnCompilationStart(CompilationStartAnalysisContext context) + { + if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols)) + { + return; + } + + context.RegisterOperationAction(OnConditional, OperationKind.Conditional); + + void OnConditional(OperationAnalysisContext context) + { + var conditional = (IConditionalOperation)context.Operation; + + if (!symbols.HasApplicableContainsMethod(conditional.Condition, out var containsInvocation, out bool containsNegated) || + !symbols.HasApplicableAddOrRemoveMethod(conditional, containsNegated, out var addOrRemoveInvocation) || + !AreInvocationsOnSameInstance(containsInvocation, addOrRemoveInvocation) || + !AreInvocationArgumentsEqual(containsInvocation, addOrRemoveInvocation)) + { + return; + } + + using var locations = ArrayBuilder.GetInstance(2); + locations.Add(conditional.Syntax.GetLocation()); + locations.Add(addOrRemoveInvocation.Syntax.Parent!.GetLocation()); + + context.ReportDiagnostic(containsInvocation.CreateDiagnostic( + Rule, + additionalLocations: locations.ToImmutable(), + properties: null, + addOrRemoveInvocation.TargetMethod.ToDisplayString(s_symbolDisplayFormat), + containsInvocation.TargetMethod.ToDisplayString(s_symbolDisplayFormat))); + } + } + + private static bool AreInvocationsOnSameInstance(IInvocationOperation invocation1, IInvocationOperation invocation2) + { + return (invocation1.Instance, invocation2.Instance) switch + { + (IFieldReferenceOperation fieldRef1, IFieldReferenceOperation fieldRef2) => fieldRef1.Member == fieldRef2.Member, + (IPropertyReferenceOperation propRef1, IPropertyReferenceOperation propRef2) => propRef1.Member == propRef2.Member, + (IParameterReferenceOperation paramRef1, IParameterReferenceOperation paramRef2) => paramRef1.Parameter == paramRef2.Parameter, + (ILocalReferenceOperation localRef1, ILocalReferenceOperation localRef2) => localRef1.Local == localRef2.Local, + _ => false, + }; + } + + // Checks if invocation argument values are equal + // 1. Not equal: Contains(item) != Add(otherItem), Contains("const") != Add("other const") + // 2. Identical: Contains(item) == Add(item), Contains("const") == Add("const") + private static bool AreInvocationArgumentsEqual(IInvocationOperation invocation1, IInvocationOperation invocation2) + { + return IsArgumentValueEqual(invocation1.Arguments[0].Value, invocation2.Arguments[0].Value); + } + + private static bool IsArgumentValueEqual(IOperation targetArg, IOperation valueArg) + { + // Check if arguments are identical constant/local/parameter/field reference operations. + if (targetArg.Kind != valueArg.Kind) + { + return false; + } + + if (targetArg.ConstantValue.HasValue != valueArg.ConstantValue.HasValue) + { + return false; + } + + if (targetArg.ConstantValue.HasValue) + { + return Equals(targetArg.ConstantValue.Value, valueArg.ConstantValue.Value); + } + + return targetArg switch + { + ILocalReferenceOperation targetLocalReference => + SymbolEqualityComparer.Default.Equals(targetLocalReference.Local, ((ILocalReferenceOperation)valueArg).Local), + IParameterReferenceOperation targetParameterReference => + SymbolEqualityComparer.Default.Equals(targetParameterReference.Parameter, ((IParameterReferenceOperation)valueArg).Parameter), + IFieldReferenceOperation fieldParameterReference => + SymbolEqualityComparer.Default.Equals(fieldParameterReference.Member, ((IFieldReferenceOperation)valueArg).Member), + _ => false, + }; + } + + private static bool DoesImplementInterfaceMethod(IMethodSymbol? method, IMethodSymbol? interfaceMethod) + { + if (method is null || interfaceMethod is null || method.Parameters.Length != 1) + { + return false; + } + + var typedInterface = interfaceMethod.ContainingType.Construct(method.Parameters[0].Type); + var typedInterfaceMethod = typedInterface.GetMembers(interfaceMethod.Name).FirstOrDefault(); + + // Also check against all original definitions to also cover external interface implementations + return SymbolEqualityComparer.Default.Equals(method, typedInterfaceMethod) || + method.GetOriginalDefinitions().Any(definition => SymbolEqualityComparer.Default.Equals(definition, typedInterfaceMethod)); + } + + internal sealed class RequiredSymbols + { + private RequiredSymbols(IMethodSymbol addMethod, IMethodSymbol removeMethod, IMethodSymbol containsMethod, IMethodSymbol? addMethodImmutableSet, IMethodSymbol? removeMethodImmutableSet, IMethodSymbol? containsMethodImmutableSet) + { + AddMethod = addMethod; + RemoveMethod = removeMethod; + ContainsMethod = containsMethod; + AddMethodImmutableSet = addMethodImmutableSet; + RemoveMethodImmutableSet = removeMethodImmutableSet; + ContainsMethodImmutableSet = containsMethodImmutableSet; + } + + public static bool TryGetSymbols(Compilation compilation, [NotNullWhen(true)] out RequiredSymbols? symbols) + { + symbols = default; + + var typeProvider = WellKnownTypeProvider.GetOrCreate(compilation); + var iSetType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericISet1); + var iCollectionType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericICollection1); + + if (iSetType is null || iCollectionType is null) + { + return false; + } + + IMethodSymbol? addMethod = iSetType.GetMembers(Add).OfType().FirstOrDefault(); + IMethodSymbol? removeMethod = null; + IMethodSymbol? containsMethod = null; + + foreach (var method in iCollectionType.GetMembers().OfType()) + { + switch (method.Name) + { + case Remove: removeMethod = method; break; + case Contains: containsMethod = method; break; + } + } + + if (addMethod is null || removeMethod is null || containsMethod is null) + { + return false; + } + + IMethodSymbol? addMethodImmutableSet = null; + IMethodSymbol? removeMethodImmutableSet = null; + IMethodSymbol? containsMethodImmutableSet = null; + + // The methods from IImmutableSet are optional and will not lead to a code fix. + var iImmutableSetType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsImmutableIImmutableSet1); + + if (iImmutableSetType is not null) + { + foreach (var method in iImmutableSetType.GetMembers().OfType()) + { + switch (method.Name) + { + case Add: addMethodImmutableSet = method; break; + case Remove: removeMethodImmutableSet = method; break; + case Contains: containsMethodImmutableSet = method; break; + } + } + } + + symbols = new RequiredSymbols( + addMethod, removeMethod, containsMethod, + addMethodImmutableSet, removeMethodImmutableSet, containsMethodImmutableSet); + + return true; + } + + // A condition contains an applicable 'Contains' method in the following cases: + // 1. The condition contains only the 'Contains' invocation. + // 2. The condition contains a unary not operation where the operand is a 'Contains' invocation. + // + // In all cases, the invocation must implement either 'ICollection.Contains' or 'IImmutableSet.Contains'. + public bool HasApplicableContainsMethod( + IOperation condition, + [NotNullWhen(true)] out IInvocationOperation? containsInvocation, + out bool containsNegated) + { + containsNegated = false; + containsInvocation = null; + + switch (condition.WalkDownParentheses()) + { + case IInvocationOperation invocation: + containsInvocation = invocation; + break; + case IUnaryOperation unaryOperation when unaryOperation.OperatorKind == UnaryOperatorKind.Not && unaryOperation.Operand is IInvocationOperation operand: + containsNegated = true; + containsInvocation = operand; + break; + default: + return false; + } + + return DoesImplementInterfaceMethod(containsInvocation.TargetMethod, ContainsMethod) || + DoesImplementInterfaceMethod(containsInvocation.TargetMethod, ContainsMethodImmutableSet); + } + + // A conditional contains an applicable 'Add' or 'Remove' method if the first operation of WhenTrue or WhenFalse satisfies one of the following: + // 1. The operation is an invocation of 'Add' or 'Remove'. + // 2. The operation is either a simple assignment or an expression statement. + // In this case the child statements are checked if they contain an invocation of 'Add' or 'Remove'. + // 3. The operation is a variable group declaration. + // In this case the descendants are checked if they contain an invocation of 'Add' or 'Remove'. + // OR when the WhenTrue or WhenFalse is a InvocationOperation (in the case of a ternary operator). + // + // In all cases, the invocation must implement either + // 1. 'ISet.Add' or 'IImmutableSet.Add' + // 2. 'ICollection.Remove' or 'IImmutableSet.Remove' + public bool HasApplicableAddOrRemoveMethod( + IConditionalOperation conditional, + bool containsNegated, + [NotNullWhen(true)] out IInvocationOperation? addOrRemoveInvocation) + { + addOrRemoveInvocation = GetApplicableAddOrRemove(conditional.WhenTrue, extractAdd: containsNegated); + + if (addOrRemoveInvocation is null) + { + addOrRemoveInvocation = GetApplicableAddOrRemove(conditional.WhenFalse, extractAdd: !containsNegated); + } + + return addOrRemoveInvocation is not null; + } + + private IInvocationOperation? GetApplicableAddOrRemove(IOperation? operation, bool extractAdd) + { + if (operation is IInvocationOperation ternaryInvocation) + { + if ((extractAdd && IsAnyAddMethod(ternaryInvocation.TargetMethod)) || + (!extractAdd && IsAnyRemoveMethod(ternaryInvocation.TargetMethod))) + { + return ternaryInvocation; + } + } + + var firstChildOperation = operation?.Children.FirstOrDefault(); + + switch (firstChildOperation) + { + case IInvocationOperation invocation: + if ((extractAdd && IsAnyAddMethod(invocation.TargetMethod)) || + (!extractAdd && IsAnyRemoveMethod(invocation.TargetMethod))) + { + return invocation; + } + + break; + + case ISimpleAssignmentOperation: + case IExpressionStatementOperation: + var firstChildAddOrRemove = firstChildOperation.Children + .OfType() + .FirstOrDefault(i => extractAdd ? + IsAnyAddMethod(i.TargetMethod) : + IsAnyRemoveMethod(i.TargetMethod)); + + if (firstChildAddOrRemove != null) + { + return firstChildAddOrRemove; + } + + break; + + case IVariableDeclarationGroupOperation variableDeclarationGroup: + var firstDescendantAddOrRemove = firstChildOperation.Descendants() + .OfType() + .FirstOrDefault(i => extractAdd ? + IsAnyAddMethod(i.TargetMethod) : + IsAnyRemoveMethod(i.TargetMethod)); + + if (firstDescendantAddOrRemove != null) + { + return firstDescendantAddOrRemove; + } + + break; + } + + return null; + } + + private bool IsAnyAddMethod(IMethodSymbol method) + { + return DoesImplementInterfaceMethod(method, AddMethod) || + DoesImplementInterfaceMethod(method, AddMethodImmutableSet); + } + + private bool IsAnyRemoveMethod(IMethodSymbol method) + { + return DoesImplementInterfaceMethod(method, RemoveMethod) || + DoesImplementInterfaceMethod(method, RemoveMethodImmutableSet); + } + + public IMethodSymbol AddMethod { get; } + public IMethodSymbol RemoveMethod { get; } + public IMethodSymbol ContainsMethod { get; } + public IMethodSymbol? AddMethodImmutableSet { get; } + public IMethodSymbol? RemoveMethodImmutableSet { get; } + public IMethodSymbol? ContainsMethodImmutableSet { get; } + } + } +} 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 8890ddc750..0282f4efba 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -763,6 +763,21 @@ Obecné přetypování (IL unbox.any) používané sekvencí vrácenou metodou E Nepotřebné volání Dictionary.ContainsKey(key) + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Nepoužívejte pevně zakódovaný certifikát 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 afe8bec262..b6ad1bae0f 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -763,6 +763,21 @@ Erweiterungen und benutzerdefinierte Konvertierungen werden bei generischen Type Nicht erforderlicher Aufruf von “Dictionary.ContainsKey(key)” + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Keine Hartcodierung von Zertifikaten 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 f235b2cf96..343c33fdeb 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -763,6 +763,21 @@ La ampliación y las conversiones definidas por el usuario no se admiten con tip Llamada innecesaria a 'Dictionary.ContainsKey(key)' + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate No codificar el certificado de forma rígida 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 ef696cdd2b..33b0cfe1db 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -763,6 +763,21 @@ Les conversions étendues et définies par l’utilisateur ne sont pas prises en Appel inutile à 'Dictionary.ContainsKey(key)' + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Ne pas coder en dur le certificat 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 557e0a3b44..2d3eaeb9e9 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -763,6 +763,21 @@ L'ampliamento e le conversioni definite dall'utente non sono supportate con tipi Chiamata non necessaria a 'Dictionary.ContainsKey(key)' + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Non impostare il certificato come hardcoded 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 91d0ca3659..020ea3cb98 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -763,6 +763,21 @@ Enumerable.OfType<T> で使用されるジェネリック型チェック ( 'Dictionary.ContainsKey(key)' への不要な呼び出し + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate 証明書をハードコーディングしない 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 a3526ffbd8..377a3f5a07 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -763,6 +763,21 @@ Enumerable.OfType<T>에서 사용하는 제네릭 형식 검사(C# 'is' 'Dictionary.ContainsKey(key)'에 대한 불필요한 호출 + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate 인증서 하드 코딩 안 함 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 35db6874ac..97b2f11b69 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -763,6 +763,21 @@ Konwersje poszerzane i zdefiniowane przez użytkownika nie są obsługiwane w pr Niepotrzebne wywołanie metody \"Dictionary.ContainsKey(key)\" + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Nie zapisuj certyfikatu na stałe w kodzie 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 50b3119e38..7b3824dae4 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 @@ -763,6 +763,21 @@ Ampliação e conversões definidas pelo usuário não são compatíveis com tip Chamada desnecessária para 'Dictionary.ContainsKey(key)' + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Não embutir o certificado em código 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 bacbf7bb90..1f97d58c74 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -763,6 +763,21 @@ Widening and user defined conversions are not supported with generic types.Ненужный вызов \"Dictionary.ContainsKey(key)\" + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Не используйте жестко заданный сертификат. 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 7bf4a1e4f6..1feeb939a9 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -763,6 +763,21 @@ Genel türlerde genişletme ve kullanıcı tanımlı dönüştürmeler desteklen Gereksiz 'Dictionary.ContainsKey(key)' çağrısı + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate Sertifikayı sabit olarak kodlama 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 778d6b6c9e..8b339dacbc 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 @@ -763,6 +763,21 @@ Enumerable.OfType<T> 使用的泛型类型检查 (C# 'is' operator/IL 'isi 对 “Dictionary.ContainsKey(key)” 的不必要调用 + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate 请勿硬编码证书 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 e90ad43e93..527ec2d164 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 @@ -763,6 +763,21 @@ Enumerable.OfType<T> 使用的一般型別檢查 (C# 'is' operator/IL 'isi 不需要呼叫 'Dictionary.ContainsKey(key)' + + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + + + + Do not guard '{0}' with '{1}' + Do not guard '{0}' with '{1}' + + + + Unnecessary call to 'Contains(item)' + Unnecessary call to 'Contains(item)' + + Do not hard-code certificate 不要硬式編碼憑證 diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index 3d043817d0..5b4eae4792 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1776,6 +1776,18 @@ The char overload is a better performing overload than a string with a single ch |CodeFix|False| --- +## [CA1868](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1868): Unnecessary call to 'Contains(item)' + +Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed. + +|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 8ca31c00fb..62f937484d 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -3273,6 +3273,26 @@ ] } }, + "CA1868": { + "id": "CA1868", + "shortDescription": "Unnecessary call to 'Contains(item)'", + "fullDescription": "Do not guard 'Add(item)' or 'Remove(item)' with 'Contains(item)' for the set. The former two already check whether the item exists and will return if it was added or removed.", + "defaultLevel": "note", + "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1868", + "properties": { + "category": "Performance", + "isEnabledByDefault": true, + "typeName": "DoNotGuardSetAddOrRemoveByContains", + "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 42df726b08..50fa5fb52f 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -13,5 +13,6 @@ CA1863 | | Use char overload | CA1866 | | Use char overload | CA1867 | | Use char overload | +CA1868 | | Unnecessary call to 'Contains(item)' | CA2021 | | Do not call Enumerable.Cast\ or Enumerable.OfType\ with incompatible types | CA2261 | | Do not use ConfigureAwaitOptions.SuppressThrowing with Task\ | diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContainsTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContainsTests.cs new file mode 100644 index 0000000000..51f28d13e0 --- /dev/null +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContainsTests.cs @@ -0,0 +1,2367 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Test.Utilities; +using Xunit; + +using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< + Microsoft.NetCore.Analyzers.Performance.DoNotGuardSetAddOrRemoveByContains, + Microsoft.NetCore.CSharp.Analyzers.Performance.CSharpDoNotGuardSetAddOrRemoveByContainsFixer>; +using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier< + Microsoft.NetCore.Analyzers.Performance.DoNotGuardSetAddOrRemoveByContains, + Microsoft.NetCore.VisualBasic.Analyzers.Performance.BasicDoNotGuardSetAddOrRemoveByContainsFixer>; + +namespace Microsoft.NetCore.Analyzers.Performance.UnitTests +{ + public class DoNotGuardSetAddOrRemoveByContainsTests + { + #region Tests + [Fact] + public async Task NonInvocationConditionDoesNotThrow_CS() + { + string source = """ + class C + { + void M() + { + if (!true) { } + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task AddIsTheOnlyStatement_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + MySet.Add("Item"); + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + MySet.Add("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveIsTheOnlyStatement_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + MySet.Remove("Item"); + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddIsTheOnlyStatementInBlock_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + { + MySet.Add("Item"); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + MySet.Add("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveIsTheOnlyStatementInBlock_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + { + MySet.Remove("Item"); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddHasElseStatement_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + MySet.Add("Item"); + else + throw new System.Exception("Item already exists"); + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Add("Item")) + throw new System.Exception("Item already exists"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveHasElseStatement_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + MySet.Remove("Item"); + else + throw new System.Exception("Item doesn't exist"); + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Remove("Item")) + throw new System.Exception("Item doesn't exist"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddWhenFalseHasElseStatement_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + throw new System.Exception("Item already exists"); + else + MySet.Add("Item"); + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Add("Item")) + throw new System.Exception("Item already exists"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveWhenFalseHasElseStatement_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + throw new System.Exception("Item doesn't exist"); + else + MySet.Remove("Item"); + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Remove("Item")) + throw new System.Exception("Item doesn't exist"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddHasElseBlock_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + { + MySet.Add("Item"); + } + else + { + throw new System.Exception("Item already exists"); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Add("Item")) + { + throw new System.Exception("Item already exists"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveHasElseBlock_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + { + MySet.Remove("Item"); + } + else + { + throw new System.Exception("Item doesn't exist"); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Remove("Item")) + { + throw new System.Exception("Item doesn't exist"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddWhenFalseHasElseBlock_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + { + throw new System.Exception("Item already exists"); + } + else + { + MySet.Add("Item"); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Add("Item")) + { + throw new System.Exception("Item already exists"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveWhenFalseHasElseBlock_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + { + throw new System.Exception("Item doesn't exist"); + } + else + { + MySet.Remove("Item"); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Remove("Item")) + { + throw new System.Exception("Item doesn't exist"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddWithAdditionalStatements_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + { + MySet.Add("Item"); + System.Console.WriteLine(); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWithAdditionalStatements_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + { + MySet.Remove("Item"); + System.Console.WriteLine(); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddWhenFalseWithAdditionalStatements_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + { + throw new System.Exception("Item already exists"); + } + else + { + MySet.Add("Item"); + System.Console.WriteLine(); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWhenFalseWithAdditionalStatements_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + { + throw new System.Exception("Item doesn't exist"); + } + else + { + MySet.Remove("Item"); + System.Console.WriteLine(); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddWithVariableAssignment_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + { + bool result = MySet.Add("Item"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWithVariableAssignment_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + { + bool result = MySet.Remove("Item"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddWhenFalseWithVariableAssignment_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if ([|MySet.Contains("Item")|]) + { + throw new System.Exception("Item already exists"); + } + else + { + bool result = MySet.Add("Item"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWhenFalseWithVariableAssignment_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (![|MySet.Contains("Item")|]) + { + throw new System.Exception("Item doesn't exist"); + } + else + { + bool result = MySet.Remove("Item"); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddWithNonNegatedContains_NoDiagnostics_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (MySet.Contains("Item")) + MySet.Add("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task RemoveWithNegatedContains_NoDiagnostics_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Contains("Item")) + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task AddWhenFalseWithNegatedContains_NoDiagnostics_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (!MySet.Contains("Item")) + throw new System.Exception("Item doesn't exist"); + else + MySet.Add("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task RemoveWhenFalseWithNonNegatedContains_NoDiagnostics_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (MySet.Contains("Item")) + throw new System.Exception("Item already exists"); + else + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task AdditionalCondition_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (MySet.Contains("Item") && MySet.Count > 2) + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task ConditionInVariable_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + var result = MySet.Contains("Item"); + if (result) + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task RemoveInSeparateLine_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + if (MySet.Contains("Item")) + _ = MySet.Count; + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NotSetRemove_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + private bool Remove(string item) => false; + + void M() + { + if (MySet.Contains("Item")) + Remove("Item"); + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NestedConditional_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + private readonly HashSet OtherSet = new HashSet(); + + void M() + { + if (MySet.Contains("Item")) + { + if (OtherSet.Contains("Item")) + { + MySet.Remove("Item"); + } + } + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task AddInTernaryWhenTrue_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool added = ![|MySet.Contains("Item")|] ? MySet.Add("Item") : false; + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenFalse_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool added = [|MySet.Contains("Item")|] ? false : MySet.Add("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenTrue_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool removed = [|MySet.Contains("Item")|] ? MySet.Remove("Item") : false; + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenFalse_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool removed = ![|MySet.Contains("Item")|] ? false : MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenFalseNested_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool nestedAdded = [|MySet.Contains("Item")|] + ? false + : MySet.Add("Item") ? true : false; + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenTrueNested_ReportsDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool nestedRemoved = [|MySet.Contains("Item")|] + ? MySet.Remove("Item") ? true : false + : false; + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenTrueWithNonNegatedContains_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool added = MySet.Contains("Item") ? MySet.Add("Item") : false; + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenFalseWithNegatedContains_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool added = !MySet.Contains("Item") ? false : MySet.Add("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenTrueWithNegatedContains_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool removed = !MySet.Contains("Item") ? MySet.Remove("Item") : false; + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenFalseWithNonNegatedContains_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + bool removed = MySet.Contains("Item") ? false : MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task TriviaIsPreserved_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + // reticulates the splines + if ([|MySet.Contains("Item")|]) + { + MySet.Remove("Item"); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + // reticulates the splines + MySet.Remove("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddIsTheOnlyStatement_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then MySet.Add("Item") + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + MySet.Add("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveIsTheOnlyStatement_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If ([|MySet.Contains("Item")|]) Then MySet.Remove("Item") + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + MySet.Remove("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddIsTheOnlyStatementInBlock_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then + MySet.Add("Item") + End If + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + MySet.Add("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveIsTheOnlyStatementInBlock_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If ([|MySet.Contains("Item")|]) Then + MySet.Remove("Item") + End If + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + MySet.Remove("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddHasElseStatement_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then MySet.Add("Item") Else Throw new System.Exception("Item already exists") + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Add("Item") Then Throw new System.Exception("Item already exists") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveHasElseStatement_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then MySet.Remove("Item") Else Throw new System.Exception("Item doesn't exist") + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Remove("Item") Then Throw new System.Exception("Item doesn't exist") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddWhenFalseHasElseStatement_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then Throw new System.Exception("Item already exists") Else MySet.Add("Item") + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Add("Item") Then Throw new System.Exception("Item already exists") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveWhenFalseHasElseStatement_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then Throw new System.Exception("Item doesn't exist") Else MySet.Remove("Item") + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Remove("Item") Then Throw new System.Exception("Item doesn't exist") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddHasElseBlock_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then + MySet.Add("Item") + Else + Throw new System.Exception("Item already exists") + End If + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Add("Item") Then + Throw new System.Exception("Item already exists") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveHasElseBlock_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then + MySet.Remove("Item") + Else + Throw new System.Exception("Item doesn't exist") + End If + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Remove("Item") Then + Throw new System.Exception("Item doesn't exist") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddWhenFalseHasElseBlock_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then + Throw new System.Exception("Item already exists") + Else + MySet.Add("Item") + End If + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Add("Item") Then + Throw new System.Exception("Item already exists") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task RemoveWhenFalseHasElseBlock_OffersFixer_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then + Throw new System.Exception("Item doesn't exist") + Else + MySet.Remove("Item") + End If + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Remove("Item") Then + Throw new System.Exception("Item doesn't exist") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task AddWithNonNegatedContains_NoDiagnostics_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If MySet.Contains("Item") Then MySet.Add("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task RemoveWithNegatedContains_NoDiagnostics_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Contains("Item") Then MySet.Remove("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task AddWhenFalseWithNegatedContains_NoDiagnostics_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not MySet.Contains("Item") Then Throw new System.Exception("Item doesn't exist") Else MySet.Add("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task RemoveWhenFalseWithNonNegatedContains_NoDiagnostics_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If MySet.Contains("Item") Then Throw new System.Exception("Item already exists") Else MySet.Remove("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task AddWithVariableAssignment_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then + Dim result = MySet.Add("Item") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWithVariableAssignment_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then + Dim result = MySet.Remove("Item") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddWhenFalseWithVariableAssignment_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then + Throw new System.Exception("Item already exists") + Else + Dim result = MySet.Add("Item") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWhenFalseWithVariableAssignment_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then + Throw new System.Exception("Item doesn't exist") + Else + Dim result = MySet.Remove("Item") + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddWithAdditionalStatements_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then + MySet.Add("Item") + System.Console.WriteLine() + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWithAdditionalStatements_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then + MySet.Remove("Item") + System.Console.WriteLine() + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddWhenFalseWithAdditionalStatements_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If [|MySet.Contains("Item")|] Then + Throw new System.Exception("Item already exists") + Else + MySet.Add("Item") + System.Console.WriteLine() + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveWhenFalseWithAdditionalStatements_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + If Not [|MySet.Contains("Item")|] Then + Throw new System.Exception("Item doesn't exist") + Else + MySet.Remove("Item") + System.Console.WriteLine() + End If + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenTrue_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If(Not [|MySet.Contains("Item")|], MySet.Add("Item"), false) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenFalse_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If([|MySet.Contains("Item")|], false, MySet.Add("Item")) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenTrue_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim removed = If([|MySet.Contains("Item")|], MySet.Remove("Item"), false) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenFalse_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If(Not [|MySet.Contains("Item")|], false, MySet.Remove("Item")) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenFalseNested_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If([|MySet.Contains("Item")|], false, If(MySet.Add("Item"), true, false)) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenTrueNested_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If([|MySet.Contains("Item")|], If(MySet.Remove("Item"), true, false), false) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenTrueWithNonNegatedContains_NoDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If(MySet.Contains("Item"), MySet.Add("Item"), false) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task AddInTernaryWhenFalseWithNegatedContains_NoDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If(Not MySet.Contains("Item"), false, MySet.Add("Item")) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenTrueWithNegatedContains_NoDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If(Not MySet.Contains("Item"), MySet.Remove("Item"), false) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task RemoveInTernaryWhenFalseWithNonNegatedContains_ReportsDiagnostic_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + Dim added = If(MySet.Contains("Item"), false, MySet.Remove("Item")) + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task TriviaIsPreserved_VB() + { + string source = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + ' reticulates the splines + If ([|MySet.Contains("Item")|]) Then + MySet.Remove("Item") + End If + End Sub + End Class + """; + + string fixedSource = """ + Imports System.Collections.Generic + + Public Class C + Private ReadOnly MySet As New HashSet(Of String)() + + Public Sub M() + ' reticulates the splines + MySet.Remove("Item") + End Sub + End Class + """; + + await VerifyVB.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + [WorkItem(6377, "https://github.com/dotnet/roslyn-analyzers/issues/6377")] + public async Task ContainsAndRemoveCalledOnDifferentInstances_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet SetField1 = new HashSet(); + private readonly HashSet SetField2 = new HashSet(); + + public HashSet SetProperty1 { get; } = new HashSet(); + + void M() + { + if (SetField2.Contains("Item")) + SetField1.Remove("Item"); + + if (!SetField1.Contains("Item")) + { + SetField2.Remove("Item"); + } + + if (SetProperty1.Contains("Item")) + SetField1.Remove("Item"); + + if (!SetField1.Contains("Item")) + { + SetProperty1.Remove("Item"); + } + + var mySetLocal4 = new HashSet(); + if (mySetLocal4.Contains("Item")) + SetField1.Remove("Item"); + + if (!SetField1.Contains("Item")) + { + mySetLocal4.Remove("Item"); + } + } + + private void RemoveItem(HashSet setParam) + { + if (setParam.Contains("Item")) + SetField1.Remove("Item"); + + if (!SetField1.Contains("Item")) + { + setParam.Remove("Item"); + } + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task ContainsAndAddCalledWithDifferentArguments_NoDiagnostic_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + private const string OtherItemField = "Other Item"; + + public string OtherItemProperty { get; } = "Other Item"; + + void M(string otherItemParameter) + { + if (!MySet.Contains("Item")) + MySet.Add("Other Item"); + + if (!MySet.Contains("Item")) + MySet.Add(otherItemParameter); + + if (!MySet.Contains("Item")) + MySet.Add(OtherItemField); + + if (!MySet.Contains("Item")) + MySet.Add(OtherItemProperty); + + string otherItemLocal = "Other Item"; + if (!MySet.Contains("Item")) + MySet.Add(otherItemLocal); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task ContainsAndAddCalledWithSameArgumentsFields_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + private const string FieldItem = "Item"; + + void M() + { + if (![|MySet.Contains(FieldItem)|]) + { + MySet.Add(FieldItem); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + private const string FieldItem = "Item"; + + void M() + { + MySet.Add(FieldItem); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task ContainsAndAddCalledWithSameArgumentsLocals_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + const string LocalItem = "Item"; + + if (![|MySet.Contains(LocalItem)|]) + { + MySet.Add(LocalItem); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M() + { + const string LocalItem = "Item"; + + MySet.Add(LocalItem); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task ContainsAndAddCalledWithSameArgumentsParameters_OffersFixer_CS() + { + string source = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M(string parameterItem) + { + if (![|MySet.Contains(parameterItem)|]) + { + MySet.Add(parameterItem); + } + } + } + """; + + string fixedSource = """ + using System.Collections.Generic; + + class C + { + private readonly HashSet MySet = new HashSet(); + + void M(string parameterItem) + { + MySet.Add(parameterItem); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("SortedSet", "Add")] + [InlineData("SortedSet", "Remove")] + [InlineData("HashSet", "Add")] + [InlineData("HashSet", "Remove")] + [InlineData("ImmutableHashSet.Builder", "Add")] + [InlineData("ImmutableHashSet.Builder", "Remove")] + [InlineData("ImmutableSortedSet.Builder", "Add")] + [InlineData("ImmutableSortedSet.Builder", "Remove")] + public async Task SupportsSetsWithAddOrRemoveReturningBool_OffersFixer_CS(string setType, string method) + { + string source = $$""" + using System.Collections.Generic; + using System.Collections.Immutable; + + class C + { + private readonly {{setType}} MySet = {{SetCreationExpression(setType)}} + + void M() + { + if ({{(method == "Add" ? "!" : string.Empty)}}[|MySet.Contains("Item")|]) + MySet.{{method}}("Item"); + } + } + """; + + string fixedSource = $$""" + using System.Collections.Generic; + using System.Collections.Immutable; + + class C + { + private readonly {{setType}} MySet = {{SetCreationExpression(setType)}} + + void M() + { + MySet.{{method}}("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("ISet", "SortedSet", "Add")] + [InlineData("ISet", "SortedSet", "Remove")] + [InlineData("ISet", "HashSet", "Add")] + [InlineData("ISet", "HashSet", "Remove")] + [InlineData("ISet", "ImmutableHashSet.Builder", "Add")] + [InlineData("ISet", "ImmutableHashSet.Builder", "Remove")] + [InlineData("ISet", "ImmutableSortedSet.Builder", "Add")] + [InlineData("ISet", "ImmutableSortedSet.Builder", "Remove")] + public async Task SupportsSetsWithAddOrRemoveReturningBoolWithInterfaceType_OffersFixer_CS(string interfaceType, string concreteType, string method) + { + string source = $$""" + using System.Collections.Generic; + using System.Collections.Immutable; + + class C + { + private readonly {{interfaceType}} MySet = {{SetCreationExpression(concreteType)}} + + void M() + { + if ({{(method == "Add" ? "!" : string.Empty)}}[|MySet.Contains("Item")|]) + MySet.{{method}}("Item"); + } + } + """; + + string fixedSource = $$""" + using System.Collections.Generic; + using System.Collections.Immutable; + + class C + { + private readonly {{interfaceType}} MySet = {{SetCreationExpression(concreteType)}} + + void M() + { + MySet.{{method}}("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("ImmutableHashSet", "Add")] + [InlineData("ImmutableHashSet", "Remove")] + [InlineData("ImmutableSortedSet", "Add")] + [InlineData("ImmutableSortedSet", "Remove")] + public async Task SupportsSetWithAddOrRemoveReturningGenericType_ReportsDiagnostic_CS(string setType, string method) + { + string source = $$""" + using System.Collections.Generic; + using System.Collections.Immutable; + + class C + { + private {{setType}} MySet = {{setType[..setType.IndexOf('<', StringComparison.Ordinal)]}}.Create(); + + void M() + { + if ({{(method == "Add" ? "!" : string.Empty)}}[|MySet.Contains("Item")|]) + MySet = MySet.{{method}}("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Theory] + [InlineData("IImmutableSet", "ImmutableHashSet", "Add")] + [InlineData("IImmutableSet", "ImmutableHashSet", "Remove")] + [InlineData("IImmutableSet", "ImmutableSortedSet", "Add")] + [InlineData("IImmutableSet", "ImmutableSortedSet", "Remove")] + public async Task SupportsSetWithAddOrRemoveReturningGenericTypeWithInterfaceType_ReportsDiagnostic_CS(string interfaceType, string concreteType, string method) + { + string source = $$""" + using System.Collections.Generic; + using System.Collections.Immutable; + + class C + { + private {{interfaceType}} MySet = {{concreteType[..concreteType.IndexOf('<', StringComparison.Ordinal)]}}.Create(); + + void M() + { + if ({{(method == "Add" ? "!" : string.Empty)}}[|MySet.Contains("Item")|]) + MySet = MySet.{{method}}("Item"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(source, source); + } + #endregion + + #region Helpers + private string SetCreationExpression(string setType) + { + return setType.Contains("Builder", StringComparison.Ordinal) + ? $"{setType[..setType.IndexOf('<', StringComparison.Ordinal)]}.CreateBuilder();" + : $"new {setType}();"; + } + #endregion + } +} diff --git a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicDoNotGuardSetAddOrRemoveByContains.Fixer.vb b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicDoNotGuardSetAddOrRemoveByContains.Fixer.vb new file mode 100644 index 0000000000..bdfbcbdae0 --- /dev/null +++ b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicDoNotGuardSetAddOrRemoveByContains.Fixer.vb @@ -0,0 +1,70 @@ +' 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.Formatting +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.NetCore.Analyzers.Performance + +Namespace Microsoft.NetCore.VisualBasic.Analyzers.Performance + + Public NotInheritable Class BasicDoNotGuardSetAddOrRemoveByContainsFixer + Inherits DoNotGuardSetAddOrRemoveByContainsFixer + + Protected Overrides Function SyntaxSupportedByFixer(conditionalSyntax As SyntaxNode, childStatementSyntax As SyntaxNode) As Boolean + If TypeOf childStatementSyntax IsNot ExpressionStatementSyntax Then + Return False + End If + + If TypeOf conditionalSyntax Is MultiLineIfBlockSyntax Then + Dim addOrRemoveInElse = TypeOf childStatementSyntax.Parent Is ElseBlockSyntax + + If addOrRemoveInElse Then + Return CType(conditionalSyntax, MultiLineIfBlockSyntax).ElseBlock.Statements.Count() = 1 + Else + Return CType(conditionalSyntax, MultiLineIfBlockSyntax).Statements.Count() = 1 + End If + End If + + Return TypeOf conditionalSyntax Is SingleLineIfStatementSyntax + End Function + + Protected Overrides Function ReplaceConditionWithChild(document As Document, root As SyntaxNode, conditionalOperationNode As SyntaxNode, childOperationNode As SyntaxNode) As Document + Dim newConditionNode As SyntaxNode = childOperationNode + + ' If there's an else block, negate the condition and replace the single true statement with it + Dim multiLineIfBlockSyntax = TryCast(conditionalOperationNode, MultiLineIfBlockSyntax) + If multiLineIfBlockSyntax?.ElseBlock?.ChildNodes().Any() Then + Dim generator = SyntaxGenerator.GetGenerator(document) + Dim negatedExpression = generator.LogicalNotExpression(CType(childOperationNode, ExpressionStatementSyntax).Expression.WithoutTrivia()) + Dim addOrRemoveInElse = TypeOf childOperationNode.Parent Is ElseBlockSyntax + + newConditionNode = multiLineIfBlockSyntax.WithIfStatement(multiLineIfBlockSyntax.IfStatement.WithCondition(CType(negatedExpression, ExpressionSyntax))) _ + .WithStatements(If(addOrRemoveInElse, multiLineIfBlockSyntax.Statements, multiLineIfBlockSyntax.ElseBlock.Statements)) _ + .WithElseBlock(Nothing) _ + .WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(conditionalOperationNode) + Else + ' if there's an else statement, negate the condition and replace the single true statement with it + Dim singleLineIfBlockSyntax = TryCast(conditionalOperationNode, SingleLineIfStatementSyntax) + If singleLineIfBlockSyntax?.ElseClause?.ChildNodes().Any() Then + Dim generator = SyntaxGenerator.GetGenerator(document) + Dim negatedExpression = generator.LogicalNotExpression(CType(childOperationNode, ExpressionStatementSyntax).Expression.WithoutTrivia()) + Dim addOrRemoveInElse = TypeOf childOperationNode.Parent Is SingleLineElseClauseSyntax + + newConditionNode = singleLineIfBlockSyntax.WithCondition(CType(negatedExpression, ExpressionSyntax)) _ + .WithStatements(If(addOrRemoveInElse, singleLineIfBlockSyntax.Statements, singleLineIfBlockSyntax.ElseClause.Statements)) _ + .WithElseClause(Nothing) _ + .WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(conditionalOperationNode) + Else + newConditionNode = newConditionNode.WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(conditionalOperationNode) + End If + End If + + Dim newRoot = root.ReplaceNode(conditionalOperationNode, newConditionNode) + + Return document.WithSyntaxRoot(newRoot) + End Function + End Class +End Namespace diff --git a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index 231fe32714..1da70dbbb5 100644 --- a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -1,4 +1,4 @@ -# This file contains the allowed analyzer rule "Category" and corresponding "Diagnostic ID range" +# This file contains the allowed analyzer rule "Category" and corresponding "Diagnostic ID range" # FORMAT: # 'Category': Comma separate list of 'StartId-EndId' or 'Id' or 'Prefix' @@ -12,7 +12,7 @@ Design: CA2210, CA1000-CA1070 Globalization: CA2101, CA1300-CA1311 Mobility: CA1600-CA1601 -Performance: HA, CA1800-CA1867 +Performance: HA, CA1800-CA1868 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5405 Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2261 Naming: CA1700-CA1727