Skip to content
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

Merged
merged 46 commits into from Jul 28, 2023

Conversation

mpidash
Copy link
Contributor

@mpidash mpidash commented Jul 13, 2023

This analyzer detects when either Set.Add or Set.Remove is guarded by Set.Contains.
As recommended in dotnet/runtime#85490, a fix is only suggested when the conditional body only has a single Set.Add or Set.Remove.

I tried out the analyzer in dotnet/roslyn, dotnet/roslyn-analyzers and dotnet/runtime and found cases of guarded calls:

  1. roslyn-analyzers/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/InteropServices/PlatformCompatibilityAnalyzer.cs#1376
  2. runtime/src/tasks/MobileBuildTasks/Android/AndroidProject.cs#L125
  3. runtime/src/tools/illink/src/ILLink.Shared/Annotations.cs#L82
  4. runtime/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ModuleInitializerListNode.cs#L116
  5. runtime/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MetricsEventSource.cs#L475
  6. runtime/src/coreclr/tools/aot/ILCompiler.DependencyAnalysisFramework/DgmlWriter.cs#L161
  7. roslyn/src/Compilers/Core/CodeAnalysisTest/Collections/HashSet/ISet_Generic_Tests`1.cs#L595
  8. roslyn/src/Compilers/Core/Portable/MetadataReader/MetadataDecoder.cs#L2073
  9. roslyn/src/Compilers/Core/Portable/MetadataReader/MetadataDecoder.cs#L2094
  10. roslyn/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceMemberContainerTypeSymbol.vb#L1805
  11. roslyn/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceNamedTypeSymbol.vb#L958 No longer flagged as it uses a property which could cause side effects.
  12. roslyn/src/EditorFeatures/Core/CodeDefinitionWindow/DefinitionContextTracker.cs#L80
  13. roslyn/src/VisualStudio/Core/Def/ChangeSignature/ChangeSignatureDialogViewModel.cs#L244
  14. roslyn/src/VisualStudio/Core/Def/Interop/CleanableWeakComHandleTable.cs#L173
  15. roslyn/src/VisualStudio/VisualBasic/Impl/ProjectSystemShim/VisualBasicProject.vb#L341

Fixes dotnet/runtime#85490

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
@mpidash mpidash requested a review from a team as a code owner July 13, 2023 20:54
@codecov
Copy link

codecov bot commented Jul 13, 2023

Codecov Report

Merging #6767 (953a052) into main (7fcfa19) will increase coverage by 0.01%.
The diff coverage is 98.40%.

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     

@mpidash
Copy link
Contributor Author

mpidash commented Jul 14, 2023

I've added additional tests and corrected the fixer analogous to #6770, PTAL.

@buyaa-n
Copy link
Member

buyaa-n commented Jul 18, 2023

I tried out the analyzer in dotnet/roslyn, dotnet/roslyn-analyzers and dotnet/runtime and found cases of guarded calls:

roslyn-analyzers/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/InteropServices/PlatformCompatibilityAnalyzer.cs#1376
runtime/src/coreclr/tools/aot/ILCompiler.DependencyAnalysisFramework/DgmlWriter.cs#L161
...

Thank you @mpidash I found more hits in runtime, FYI you could build with -warnaserror 0 option to continue building with analyzers warnings, to see all the diagnostics caused by the analyzer.

Found one false positive with the testing, it warns when the elements in the Contains and Add area different, for example:

    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}");
            }
        }
    }

@mpidash
Copy link
Contributor Author

mpidash commented Jul 25, 2023

Great thank you @mpidash feel free to put up a PR with the fix for valid diagnostics to those repos

I'll prepare one for dotnet/runtime, already submitted for the other two: dotnet/roslyn#69179 and #6793.

Looks great, thank you! Left a few more comment, overall the PR shape looks good.

Thank you so much for your guidance and patience with me! 🎉

SyntaxNode newConditionalOperationNode = conditionalExpressionSyntax
.WithCondition((ExpressionSyntax)expression)
.WithWhenTrue(conditionalExpressionSyntax.WhenFalse)
.WithWhenFalse(null!)
Copy link
Member

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

Copy link
Member

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?

Copy link
Contributor Author

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):

SyntaxNode newConditionalOperationNode = conditionalExpressionSyntax
.WithCondition((ExpressionSyntax)negatedExpression)
.WithWhenTrue(conditionalExpressionSyntax.WhenFalse)
.WithWhenFalse(null!)
.WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(conditionalOperationNode);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explanation @mpidash, removing this part sounds good to me, what you think @sharwell?

Copy link
Member

@sharwell sharwell Jul 26, 2023

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?

Copy link
Contributor Author

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:

  1. One with a ternary operator as in your example. In this case, no diagnostic is reported.
  2. Another test with a nested ternary operator. No diagnostic is reported for the Add case, but one is reported for the Remove 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"");
}

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ad82982 adds support for Add and Remove in else blocks
  • f723ac5 adds support for ternary diagnostics (no fixer)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the fix @mpidash!

@sharwell requested change is addressed PTAL

@buyaa-n
Copy link
Member

buyaa-n commented Jul 25, 2023

@mpidash seems need to run msbuild -pack once more

mpidash and others added 2 commits July 25, 2023 23:00
Co-authored-by: Sam Harwell <sam@tunnelvisionlabs.com>
Comment on lines 1132 to 1149
#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";
Copy link
Member

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

Copy link
Contributor Author

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);
}

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 6f69703.

@mpidash
Copy link
Contributor Author

mpidash commented Jul 26, 2023

I've re-ran the analyzer against dotnet/roslyn, dotnet/roslyn-analyzers and dotnet/runtime and found two new guarded calls, both in else blocks:

  1. roslyn/src/Compilers/CSharp/Portable/Parser/DocumentationCommentParser.cs#L333-L340
  2. runtime/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/ReflectionModel/ReflectionComposablePartDefinition.cs#L188-L195

Also ran it against dotnet/aspnetcore and found one guarded call: aspnetcore/src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs#L103-L110

I found no cases of ternary operators.

@mpidash
Copy link
Contributor Author

mpidash commented Jul 26, 2023

Looks like CA1865 was used by another analyzer that was already merged -- I will switch to the next one that is free.

@mpidash mpidash changed the title Add CA1865: Unnecessary call to Set.Contains(item) Add CA1868: Unnecessary call to Set.Contains(item) Jul 27, 2023
Copy link
Member

@buyaa-n buyaa-n left a 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
@buyaa-n buyaa-n dismissed sharwell’s stale review July 28, 2023 00:09

The requested change addressed

@buyaa-n buyaa-n merged commit 1479680 into dotnet:main Jul 28, 2023
11 checks passed
@mpidash mpidash deleted the issue-85490-analyzer-unnecessary-set-call branch July 28, 2023 20:14
kiminuo added a commit to kiminuo/WalletWasabi that referenced this pull request Jul 31, 2023
lontivero pushed a commit to zkSNACKs/WalletWasabi that referenced this pull request Jul 31, 2023
* See dotnet/roslyn-analyzers#6767

* IWalletModel: Unify

* Fix prison tests

* Redundant `public` keywords

* Remove unnecessary cast (error)

* `PrisonTests`: Add timeout for safety

* ?
buyaa-n pushed a commit that referenced this pull request Sep 20, 2023
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Analyzer: Report inefficient use of sets.
6 participants