-
Notifications
You must be signed in to change notification settings - Fork 470
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
Add CA1868: Unnecessary call to Set.Contains(item)
#6767
Add CA1868: Unnecessary call to Set.Contains(item)
#6767
Conversation
This analyzer detects when either `Set.Add` or `Set.Remove` is guarded by `Set.Contains`. A fix is only suggested when the conditional body has a single statement that is either one of the former two methods. Fix #85490
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #6767 +/- ##
==========================================
+ Coverage 96.34% 96.36% +0.01%
==========================================
Files 1394 1399 +5
Lines 327462 329717 +2255
Branches 10788 10841 +53
==========================================
+ Hits 315507 317727 +2220
- Misses 9237 9258 +21
- Partials 2718 2732 +14 |
...rp/Microsoft.NetCore.Analyzers/Performance/CSharpDoNotGuardSetAddOrRemoveByContains.Fixer.cs
Outdated
Show resolved
Hide resolved
...Analyzers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.cs
Outdated
Show resolved
Hide resolved
...ers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.Fixer.cs
Outdated
Show resolved
Hide resolved
...ers/Core/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContains.Fixer.cs
Outdated
Show resolved
Hide resolved
This reverts commit 68c210f.
I've added additional tests and corrected the fixer analogous to #6770, PTAL. |
Thank you @mpidash I found more hits in runtime, FYI you could build with Found one false positive with the testing, it warns when the elements in the public void Test(HashSet<string> runtimeOptions, string[] jiterpreterOptions, bool jiterpreter)
{
foreach (var jiterpreterOption in jiterpreterOptions)
{
if (jiterpreter == true)
{
if (!runtimeOptions.Contains($"--no-{jiterpreterOption}"))
runtimeOptions.Add($"--{jiterpreterOption}");
}
else
{
if (!runtimeOptions.Contains($"--{jiterpreterOption}"))
runtimeOptions.Add($"--no-{jiterpreterOption}");
}
}
} |
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
Outdated
Show resolved
Hide resolved
I'll prepare one for
Thank you so much for your guidance and patience with me! 🎉 |
...rp/Microsoft.NetCore.Analyzers/Performance/CSharpDoNotGuardSetAddOrRemoveByContains.Fixer.cs
Outdated
Show resolved
Hide resolved
SyntaxNode newConditionalOperationNode = conditionalExpressionSyntax | ||
.WithCondition((ExpressionSyntax)expression) | ||
.WithWhenTrue(conditionalExpressionSyntax.WhenFalse) | ||
.WithWhenFalse(null!) |
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.
❗ This is going to throw ArgumentNullException
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.
Good catch, I thought this is for removing the false part, did not notice the '!'. What can we use instead?
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 originally copied this section from the similar fixer for dictionaries (CSharpDoNotGuardDictionaryRemoveByContainsKeyFixer
).
As part of a bugfix for the dictionary analyzer, it was discovered that the case of the ternary operator was never flagged by the analyzer (see #6770 (comment)).
So the code in the fixer that would throw an ArgumentNullException is never executed.
That part should probably be removed from the dictionary fixer as well (I missed that in the PR above):
Lines 44 to 48 in 47edf21
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
Add
case, but one is reported for theRemove
case.
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 seems need to run |
Co-authored-by: Sam Harwell <[email protected]>
#region Helpers | ||
private const string CSUsings = @"using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable;"; | ||
|
||
private const string CSNamespaceAndClassStart = @"namespace Testopolis | ||
{ | ||
public class MyClass | ||
{ | ||
"; | ||
|
||
private const string CSNamespaceAndClassEnd = @" | ||
} | ||
}"; | ||
|
||
private const string VBUsings = @"Imports System | ||
Imports System.Collections.Generic | ||
Imports System.Collections.Immutable"; |
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.
💡 It would be preferable to inline all of these
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.
Just to be sure that I understand correctly:
Change this:
[Fact]
public async Task AddWithVariableAssignment_ReportsDiagnostic_CS()
{
string source = CSUsings + CSNamespaceAndClassStart + @"
private readonly HashSet<string> MySet = new HashSet<string>();
public MyClass()
{
if (![|MySet.Contains(""Item"")|])
{
bool result = MySet.Add(""Item"");
}
}
" + CSNamespaceAndClassEnd;
await VerifyCS.VerifyCodeFixAsync(source, source);
}
To:
[Fact]
public async Task AddWithVariableAssignment_ReportsDiagnostic_CS()
{
string source = """
using System.Collections.Generic;
class C
{
private readonly HashSet<string> MySet = new HashSet<string>();
void M()
{
if (![|MySet.Contains("Item")|])
{
bool result = MySet.Add("Item");
}
}
}
""";
await VerifyCS.VerifyCodeFixAsync(source, source);
}
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.
Just to be sure that I understand correctly:
Correct, it meant to move CSNamespaceAndClassStart
and CSNamespaceAndClassEnd
inline.
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.
Changed in 6f69703.
...UnitTests/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContainsTests.cs
Outdated
Show resolved
Hide resolved
I've re-ran the analyzer against
Also ran it against I found no cases of ternary operators. |
Looks like CA1865 was used by another analyzer that was already merged -- I will switch to the next one that is free. |
Set.Contains(item)
Set.Contains(item)
...UnitTests/Microsoft.NetCore.Analyzers/Performance/DoNotGuardSetAddOrRemoveByContainsTests.cs
Outdated
Show resolved
Hide resolved
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.
Thank you @mpidash LGTM!
…ance/DoNotGuardSetAddOrRemoveByContainsTests.cs
* See dotnet/roslyn-analyzers#6767 * IWalletModel: Unify * Fix prison tests * Redundant `public` keywords * Remove unnecessary cast (error) * `PrisonTests`: Add timeout for safety * ?
* Rename do not guard set analyzer and fixer This is done in a separate commit, so that the actual changes can be seen in the next commit. This is because git would not recognize the rename if the changes were added as is. Note that this commit by itself is broken. * Combine analyzers for guarded calls This combines the analyzers CA1853 and CA1868 to avoid code duplication, make CA1853 support the same range of cases as CA1868 and fix #6781.
This analyzer detects when either
Set.Add
orSet.Remove
is guarded bySet.Contains
.As recommended in dotnet/runtime#85490, a fix is only suggested when the conditional body only has a single
Set.Add
orSet.Remove
.I tried out the analyzer in
dotnet/roslyn
,dotnet/roslyn-analyzers
anddotnet/runtime
and found cases of guarded calls:roslyn/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceNamedTypeSymbol.vb#L958No longer flagged as it uses a property which could cause side effects.Fixes dotnet/runtime#85490