-
Notifications
You must be signed in to change notification settings - Fork 480
Add CA1868: Unnecessary call to Set.Contains(item)
#6767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
d5e2607
4d10f23
5c6bcdb
9547162
52e7f91
fea7a0a
02fc236
68c210f
5a0113f
5e841a6
07943c4
9b43e00
a8e4875
29a4630
468274e
326481c
533cd78
cf49578
d063408
d7c7c54
4c71d4c
ed09398
4f62beb
315e2c5
a0f8d26
f430939
f22e2dc
efe2993
7c68257
860f217
8b7a007
1facbcf
ba59f85
9513d9d
8a3f6ef
be9d96e
5fef8d0
1d220c6
c5376f3
7a9fa60
6f69703
ad82982
f723ac5
915a4ad
8866f5d
953a052
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||
| // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. | ||||||||||||
|
|
||||||||||||
| using System.Linq; | ||||||||||||
| using Microsoft.CodeAnalysis; | ||||||||||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||||||||||
| using Microsoft.CodeAnalysis.Editing; | ||||||||||||
| using Microsoft.CodeAnalysis.Formatting; | ||||||||||||
| using Microsoft.NetCore.Analyzers.Performance; | ||||||||||||
|
|
||||||||||||
| namespace Microsoft.NetCore.CSharp.Analyzers.Performance | ||||||||||||
| { | ||||||||||||
| internal class CSharpDoNotGuardSetAddOrRemoveByContainsFixer : DoNotGuardSetAddOrRemoveByContainsFixer | ||||||||||||
| { | ||||||||||||
| protected override bool SyntaxSupportedByFixer(SyntaxNode conditionalSyntax) | ||||||||||||
| { | ||||||||||||
| if (conditionalSyntax is ConditionalExpressionSyntax conditionalExpressionSyntax) | ||||||||||||
| return conditionalExpressionSyntax.WhenTrue.ChildNodes().Count() == 1; | ||||||||||||
|
|
||||||||||||
| if (conditionalSyntax is IfStatementSyntax) | ||||||||||||
| return true; | ||||||||||||
|
|
||||||||||||
| return false; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| protected override Document ReplaceConditionWithChild(Document document, SyntaxNode root, SyntaxNode conditionalOperationNode, SyntaxNode childOperationNode) | ||||||||||||
| { | ||||||||||||
| SyntaxNode newRoot; | ||||||||||||
|
|
||||||||||||
| if (conditionalOperationNode is ConditionalExpressionSyntax conditionalExpressionSyntax && | ||||||||||||
| conditionalExpressionSyntax.WhenFalse.ChildNodes().Any()) | ||||||||||||
| { | ||||||||||||
| var expression = GetNegatedExpression(document, childOperationNode); | ||||||||||||
|
|
||||||||||||
| SyntaxNode newConditionalOperationNode = conditionalExpressionSyntax | ||||||||||||
| .WithCondition((ExpressionSyntax)expression) | ||||||||||||
| .WithWhenTrue(conditionalExpressionSyntax.WhenFalse) | ||||||||||||
| .WithWhenFalse(null!) | ||||||||||||
|
||||||||||||
| SyntaxNode newConditionalOperationNode = conditionalExpressionSyntax | |
| .WithCondition((ExpressionSyntax)negatedExpression) | |
| .WithWhenTrue(conditionalExpressionSyntax.WhenFalse) | |
| .WithWhenFalse(null!) | |
| .WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(conditionalOperationNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe if this is reachable, it would would be hit by:
bool added = set.Contains(value) ? false : set.Add(value);
Maybe start by adding the test and seeing what kind of result it gives?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added two tests for ternary operators:
- One with a ternary operator as in your example. In this case, no diagnostic is reported.
- Another test with a nested ternary operator. No diagnostic is reported for the
Addcase, but one is reported for theRemovecase.
This is because we only check the true part of a conditional for an applicable Add or Remove. So the Add case gets filtered out immediately because it contains no child operations:
var firstOperation = operations.FirstOrDefault();
if (firstOperation is null)
{
return false;
}For Remove it depends on the content of the true part. In the simple case, it contains a FieldReferenceOperation, which is not covered by the switch in HasApplicableAddOrRemoveMethod so it produces no diagnostic.
The nested case for Remove produces a diagnostic because the first child operation is a IInvocationOperation.
A ternary operator gets filtered out by the fixer in SyntaxSupportedByFixer, because the childStatementSyntax is not a ExpressionStatementSyntax (it is ConditionalExpressionSyntax). So the part where the ArgumentNullException would be thrown is not reachable.
I think we should filter out ternary operators for now. I am not sure how likely it is that someone would write code like in the test cases, as they already use the return value of Add and Remove.
DoNotGuardDictionaryRemoveByContainsKey should be adapted to treat ternary operators in the same way as this analyzer.
The above has shown a case that is not covered at the moment:
if (MySet.Contains(""Item""))
{
throw new Exception(""Item already exists"");
}
else
{
MySet.Add(""Item"");
}which could be refactored to:
if (!MySet.Add(""Item""))
{
throw new Exception(""Item already exists"");
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should filter out ternary operators for now. I am not sure how likely it is that someone would write code like in the test cases, as they already use the return value of Add and Remove.
DoNotGuardDictionaryRemoveByContainsKey should be adapted to treat ternary operators in the same way as this analyzer.
Agree that it would be rare, though it could happen and it fixable. I am OK with filtering this out for now and create an issue for future fix.
The above has shown a case that is not covered at the moment:
Did you find why it was not covered and could you fix this with the PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you find why it was not covered and could you fix this with the PR?
Its because atm only the true branch of a condition is checked for Add and Remove.
I will add a fix which also checks the false branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mpidash marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // 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 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<string> 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.First(); | ||
mpidash marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
|
|
||
| // We only offer a fixer if the conditonal true branch has a single statement, either 'Add' or 'Delete' | ||
| if (!SyntaxSupportedByFixer(conditionalSyntax)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var title = MicrosoftNetCoreAnalyzersResources.DoNotGuardSetAddOrRemoveByContainsTitle; | ||
| var codeAction = CodeAction.Create(title, | ||
| ct => Task.FromResult(ReplaceConditionWithChild(context.Document, root, conditionalSyntax, childStatementSyntax)), | ||
| title); | ||
mpidash marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| context.RegisterCodeFix(codeAction, diagnostic); | ||
| } | ||
|
|
||
| protected abstract bool SyntaxSupportedByFixer(SyntaxNode conditionalSyntax); | ||
|
|
||
| protected abstract Document ReplaceConditionWithChild(Document document, SyntaxNode root, | ||
| SyntaxNode conditionalOperationNode, | ||
| SyntaxNode childOperationNode); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
|
||
| using System.Collections.Generic; | ||
| 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; | ||
|
|
||
| /// <summary> | ||
| /// CA1865: <inheritdoc cref="@DoNotGuardSetAddOrRemoveByContainsTitle"/> | ||
| /// </summary> | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] | ||
| public sealed class DoNotGuardSetAddOrRemoveByContains : DiagnosticAnalyzer | ||
| { | ||
| internal const string RuleId = "CA1865"; | ||
|
|
||
| private const string Contains = nameof(Contains); | ||
| private const string Add = nameof(Add); | ||
| private const string Remove = nameof(Remove); | ||
|
|
||
| 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<DiagnosticDescriptor> 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 (!TryGetRequiredMethods(context.Compilation, out var containsMethod, out var addMethod, out var removeMethod)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| context.RegisterOperationAction(context => OnConditional(context, containsMethod, addMethod, removeMethod), OperationKind.Conditional); | ||
| } | ||
|
|
||
| private static void OnConditional(OperationAnalysisContext context, IMethodSymbol containsMethod, IMethodSymbol addMethod, IMethodSymbol removeMethod) | ||
| { | ||
| var conditional = (IConditionalOperation)context.Operation; | ||
|
|
||
| if (!TryExtractContainsInvocation(conditional.Condition, containsMethod, out var containsInvocation, out var containsNegated)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (!TryExtractAddOrRemoveInvocation(conditional.WhenTrue.Children, addMethod, removeMethod, containsNegated, out var addOrRemoveInvocation)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (!AreInvocationsOnSameInstance(containsInvocation, addOrRemoveInvocation)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| using var locations = ArrayBuilder<Location>.GetInstance(2); | ||
| locations.Add(conditional.Syntax.GetLocation()); | ||
| locations.Add(addOrRemoveInvocation.Syntax.Parent!.GetLocation()); | ||
|
|
||
| context.ReportDiagnostic(containsInvocation.CreateDiagnostic(Rule, additionalLocations: locations.ToImmutable(), null)); | ||
| } | ||
|
|
||
| private static bool TryGetRequiredMethods( | ||
| Compilation compilation, | ||
| [NotNullWhen(true)] out IMethodSymbol? containsMethod, | ||
| [NotNullWhen(true)] out IMethodSymbol? addMethod, | ||
| [NotNullWhen(true)] out IMethodSymbol? removeMethod) | ||
| { | ||
| var iSetType = WellKnownTypeProvider.GetOrCreate(compilation).GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericISet1); | ||
| var iCollectionType = WellKnownTypeProvider.GetOrCreate(compilation).GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericICollection1); | ||
buyaa-n marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (iSetType is null || iCollectionType is null) | ||
| { | ||
| containsMethod = null; | ||
| addMethod = null; | ||
| removeMethod = null; | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| addMethod = iSetType.GetMembers(Add).OfType<IMethodSymbol>().FirstOrDefault(); | ||
| containsMethod = iCollectionType.GetMembers(Contains).OfType<IMethodSymbol>().FirstOrDefault(); | ||
| removeMethod = iCollectionType.GetMembers(Remove).OfType<IMethodSymbol>().FirstOrDefault(); | ||
buyaa-n marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return containsMethod is not null && addMethod is not null && removeMethod is not null; | ||
| } | ||
|
|
||
| private static bool TryExtractContainsInvocation( | ||
| IOperation condition, | ||
| IMethodSymbol containsMethod, | ||
| [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 DoesSignatureMatch(containsInvocation.TargetMethod, containsMethod); | ||
buyaa-n marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| private static bool TryExtractAddOrRemoveInvocation( | ||
| IEnumerable<IOperation> operations, | ||
| IMethodSymbol addMethod, | ||
| IMethodSymbol removeMethod, | ||
| bool containsNegated, | ||
| [NotNullWhen(true)] out IInvocationOperation? addOrRemoveInvocation) | ||
| { | ||
| addOrRemoveInvocation = null; | ||
| var firstOperation = operations.FirstOrDefault(); | ||
|
|
||
| if (firstOperation is null) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| switch (firstOperation) | ||
| { | ||
| case IInvocationOperation invocation: | ||
| if ((containsNegated && DoesSignatureMatch(invocation.TargetMethod, addMethod)) || | ||
| (!containsNegated && DoesSignatureMatch(invocation.TargetMethod, removeMethod))) | ||
| { | ||
| addOrRemoveInvocation = invocation; | ||
| return true; | ||
| } | ||
|
|
||
| break; | ||
| case IExpressionStatementOperation expressionStatement: | ||
| var nestedAddOrRemove = expressionStatement.Children | ||
| .OfType<IInvocationOperation>() | ||
| .FirstOrDefault(i => containsNegated ? | ||
| DoesSignatureMatch(i.TargetMethod, addMethod) : | ||
| DoesSignatureMatch(i.TargetMethod, removeMethod)); | ||
|
|
||
| if (nestedAddOrRemove != null) | ||
| { | ||
| addOrRemoveInvocation = nestedAddOrRemove; | ||
| return true; | ||
| } | ||
|
|
||
| break; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| 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, | ||
| }; | ||
| } | ||
|
|
||
| private static bool DoesSignatureMatch(IMethodSymbol suspected, IMethodSymbol comparator) | ||
| { | ||
| return suspected.OriginalDefinition.ReturnType.Name == comparator.ReturnType.Name | ||
| && suspected.Name == comparator.Name | ||
| && suspected.Parameters.Length == comparator.Parameters.Length | ||
| && suspected.Parameters.Zip(comparator.Parameters, (p1, p2) => p1.OriginalDefinition.Type.Name == p2.Type.Name).All(isParameterEqual => isParameterEqual); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.