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

Prefer Dictionary<K, V>.TryAdd(key) over guarded Add(key) #6199

Merged
merged 30 commits into from
Jun 28, 2023

Conversation

CollinAlpert
Copy link
Contributor

@CollinAlpert CollinAlpert commented Oct 9, 2022

This PR adds an Analyzer to detect usages of dictionary.Add() guarded by a dictionary.ContainsKey() check. It also adds a CodeFix which offers to refactor this construct into a single dictionary.TryAdd() call.

Fixes dotnet/runtime#33799, #6589

@CollinAlpert CollinAlpert requested a review from a team as a code owner October 9, 2022 19:50
@CollinAlpert
Copy link
Contributor Author

Ping for review :)

# Conflicts:
#	src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
# Conflicts:
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
#	src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md
#	src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif
#	src/NetAnalyzers/RulesMissingDocumentation.md
#	src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt
Comment on lines 141 to 147
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);
}
Copy link
Member

Choose a reason for hiding this comment

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

Why not use suspected.Equals(comparator, SymbolEqualityComparer.Default)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because symbolically, System.Collections.Generic.Dictionary<TKey, TValue>.ContainsKey(TKey) and System.Collections.Generic.IDictionary<TKey, TValue>.ContainsKey(TKey) are not equal.

Copy link
Member

Choose a reason for hiding this comment

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

@CollinAlpert There is an IsImplementationOfInterfaceMember extension method that I think could work for this scenario.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I tried substituting the body of the method with return suspected.OriginalDefinition.IsImplementationOfInterfaceMember(comparator);, but that also doesn't work.

@CollinAlpert
Copy link
Contributor Author

Thank you @CollinAlpert, I did tested with runtime and somehow the fixer is not producing suggestions in VS. Have you tried the use the fixer in VS? Could you take a look into this issue?

That's weird. I double checked everything, and the IDs registered are all correct and passed to the appropriate properties. Unfortunately I don't use VS, so I can't check there. I consolidated the fixers now, maybe that will help?

@buyaa-n
Copy link
Member

buyaa-n commented May 25, 2023

Thanks for the updates @CollinAlpert, now the fixer is working in VS, so I continued with analyzing all diagnostics and found these false positives:

  1. The analyzers flagging an instance of ImmutableDictionary<TKey, TValue>.Builder and suggesting the code fix when it doesn't have a TryAdd(TKey, TValue) method, this case should not be flagged
  2. The analyzer flagging and suggesting a fixer when the value is only created within the conditional, this should exclude this scenatio. Example code snippet:
        XmlQualifiedName notationName = GetNameQualified(false);
        SchemaNotation? notation = null;
        if (!_schemaInfo.Notations.ContainsKey(notationName.Name))
        {
            _undeclaredNotations?.Remove(notationName.Name);
            notation = new SchemaNotation(notationName);
            _schemaInfo.Notations.Add(notation.Name.Name, notation);
        }

SchemaNotation notation only created with this is not exist in the dictionary

Alos found one false negative case, it is not flagging the below case when it should:

    public void Test(Dictionary<XmlQualifiedName, SchemaElementDecl> _elementDecls )
    {
        foreach (KeyValuePair<XmlQualifiedName, SchemaElementDecl> entry in sinfo._elementDecls)
        {
            if (!_elementDecls.ContainsKey(entry.Key))
            {
                _elementDecls.Add(entry.Key, entry.Value);
            }
        }
    }

Please fix these issues, thank you!

@CollinAlpert
Copy link
Contributor Author

@buyaa-n Thanks for the hints. I have addressed them and added tests.

Regarding the false negative, I don't think it is one. Behind a property reference (in this case entry.Value) could be a computation which the developer only wants to trigger when the key is not present in the dictionary. Currently the analyzer only flags literals, local references, field references and constants.

# Conflicts:
#	src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf
#	src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf
#	src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md
#	src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif
#	src/NetAnalyzers/RulesMissingDocumentation.md
@buyaa-n
Copy link
Member

buyaa-n commented Jun 28, 2023

@buyaa-n Thanks for the hints. I have addressed them and added tests.

Thank you!

Regarding the false negative, I don't think it is one. Behind a property reference (in this case entry.Value) could be a computation which the developer only wants to trigger when the key is not present in the dictionary. Currently the analyzer only flags literals, local references, field references and constants.

Well, the KeyValuePair<TKey,TValue >.Value just returns an internal field which set within constructor.

        public KeyValuePair(TKey key, TValue value)
        {
            this.key = key;
            this.value = value;
        }
        public TKey Key => key;
        public TValue Value => value;

Anyway, I guess you meant about any property not specific to KeyValuePair, so this scenario is not a blocker for the PR. We can discuss this scenario with a separate issue.

@CollinAlpert
Copy link
Contributor Author

Yes, I meant properties in general could have expensive computations in their getter, not specifically KeyValuePair.

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.

Left some NIT, overall LGTM, thank you!

It would be great if you could add a test case for #6589

@buyaa-n buyaa-n enabled auto-merge (squash) June 28, 2023 19:09
@buyaa-n buyaa-n merged commit c6b6e10 into dotnet:main Jun 28, 2023
10 of 11 checks passed
@CollinAlpert CollinAlpert deleted the issue_33799 branch June 29, 2023 07:49
@CollinAlpert CollinAlpert changed the title CA1862: Prefer Dictionary<K, V>.TryAddValue(key) over guarded Add(key) Prefer Dictionary<K, V>.TryAddValue(key) over guarded Add(key) Jul 7, 2023
@CollinAlpert CollinAlpert changed the title Prefer Dictionary<K, V>.TryAddValue(key) over guarded Add(key) Prefer Dictionary<K, V>.TryAdd(key) over guarded Add(key) Jul 7, 2023
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.

Prefer Dictionary<K, V>.TryAddValue(key) over guarded Add(key)
6 participants