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

Issue 33798: Prefer Dictionary<K, V>.TryGetValue() over guarded indexer access #4851

Merged
merged 21 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.NetCore.Analyzers.Runtime;

namespace Microsoft.NetCore.CSharp.Analyzers.Runtime
{
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class CSharpPreferDictionaryTryGetValueFixer : PreferDictionaryTryGetValueFixer
{
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.FirstOrDefault();
var dictionaryAccessLocation = diagnostic?.AdditionalLocations[0];
if (dictionaryAccessLocation is null)
{
return;
}

Document document = context.Document;
SyntaxNode root = await document.GetSyntaxRootAsync().ConfigureAwait(false);

var dictionaryAccess = root.FindNode(dictionaryAccessLocation.SourceSpan, getInnermostNodeForTie: true);
if (dictionaryAccess is not ElementAccessExpressionSyntax
|| root.FindNode(context.Span) is not InvocationExpressionSyntax containsKeyInvocation
Copy link
Member

Choose a reason for hiding this comment

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

Same as above, do we need getInnermostNodeForTie here? If so, needs a test that demonstrates that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do not, as I still need to access the InvocationExpressionSyntax later on.

Copy link
Contributor

Choose a reason for hiding this comment

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

if we do not need InvocationExpressionSyntax set, should we remove this?

|| containsKeyInvocation.Expression is not MemberAccessExpressionSyntax containsKeyAccess)
{
return;
}

var action = CodeAction.Create(PreferDictionaryTryGetValueCodeFixTitle, async ct =>
{
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
var generator = editor.Generator;

var tryGetValueAccess = generator.MemberAccessExpression(containsKeyAccess.Expression, "TryGetValue");
var keyArgument = containsKeyInvocation.ArgumentList.Arguments.FirstOrDefault();

var outArgument = generator.Argument(RefKind.Out,
SyntaxFactory.DeclarationExpression(SyntaxFactory.IdentifierName("var"), SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier("value"))));
var tryGetValueInvocation = generator.InvocationExpression(tryGetValueAccess, keyArgument, outArgument);
editor.ReplaceNode(containsKeyInvocation, tryGetValueInvocation);

editor.ReplaceNode(dictionaryAccess, generator.IdentifierName("value"));

return editor.GetChangedDocument();
}, PreferDictionaryTryGetValueCodeFixTitle);

context.RegisterCodeFix(action, context.Diagnostics);
}
}
}
5 changes: 5 additions & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
; Please do not edit this file manually, it should only be updated through code fix application.

### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
CA1839 | Performance | Warning | PreferDictionaryTryGetValueAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1839)

### Removed Rules

Rule ID | Category | Severity | Notes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1510,4 +1510,16 @@
<value>and all other platforms</value>
<comment>This call site is reachable on: 'windows' 10.0.2000 and later, and all other platforms</comment>
</data>
<data name="PreferDictionaryTryGetValueCodeFixTitle" xml:space="preserve">
<value>Use 'TryGetValue(TKey, out TValue)'</value>
</data>
<data name="PreferDictionaryTryGetValueTitle" xml:space="preserve">
<value>Prefer the Dictionary's TryGetValue method</value>
</data>
<data name="PreferDictionaryTryGetValueMessage" xml:space="preserve">
<value>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</value>
</data>
<data name="PreferDictionaryTryGetValueDescription" xml:space="preserve">
<value>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class PreferDictionaryTryGetValueAnalyzer : DiagnosticAnalyzer
{
public const string RuleId = "CA1839";

private const string ContainsKeyMethodName = nameof(IDictionary<dynamic, dynamic>.ContainsKey);
private const string IndexerName = "this[]";
private const string IndexerNameVb = "Item";

private static readonly LocalizableString s_localizableTitle = CreateResource(nameof(MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryGetValueTitle));
private static readonly LocalizableString s_localizableTryGetValueMessage = CreateResource(nameof(MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryGetValueMessage));
private static readonly LocalizableString s_localizableTryGetValueDescription = CreateResource(nameof(MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryGetValueDescription));

internal static readonly DiagnosticDescriptor ContainsKeyRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
s_localizableTryGetValueMessage,
DiagnosticCategory.Performance,
RuleLevel.BuildWarning,
s_localizableTryGetValueDescription,
isPortedFxCopRule: false,
isDataflowRule: false);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(ContainsKeyRule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(OnCompilationStart);
}

private void OnCompilationStart(CompilationStartAnalysisContext compilationContext)
{
var compilation = compilationContext.Compilation;

if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericICollection1, out _))
return;
if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIDictionary2, out var dictionaryType))
return;
if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEnumerable1, out _))
return;

compilationContext.RegisterOperationAction(context => OnOperationAction(context, dictionaryType), OperationKind.PropertyReference);
}

private static void OnOperationAction(OperationAnalysisContext context, INamedTypeSymbol dictionaryType)
{
var propertyReference = (IPropertyReferenceOperation)context.Operation;

if (propertyReference.Parent is IAssignmentOperation
|| !IsDictionaryAccess(propertyReference, dictionaryType)
|| !TryGetParentConditionalOperation(propertyReference, out var conditionalOperation)
|| !TryGetContainsKeyGuard(conditionalOperation, out var containsKeyInvocation))
{
return;
}

if (conditionalOperation.WhenTrue is IBlockOperation blockOperation && DictionaryEntryIsModified(propertyReference, blockOperation))
{
return;
}

var additionalLocations = ImmutableArray.Create(propertyReference.Syntax.GetLocation());
context.ReportDiagnostic(Diagnostic.Create(ContainsKeyRule, containsKeyInvocation.Syntax.GetLocation(), additionalLocations));
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why? What advantages do I gain?

}

private static bool TryGetContainsKeyGuard(IConditionalOperation conditionalOperation, [NotNullWhen(true)] out IInvocationOperation? containsKeyInvocation)
{
containsKeyInvocation = conditionalOperation.Condition as IInvocationOperation ?? FindContainsKeyInvocation(conditionalOperation.Condition);
if (containsKeyInvocation is not null && containsKeyInvocation.TargetMethod.Name == ContainsKeyMethodName)
{
return true;
}

return false;
}

private static IInvocationOperation? FindContainsKeyInvocation(IOperation baseOperation)
{
return baseOperation switch
{
IInvocationOperation i when i.TargetMethod.Name == ContainsKeyMethodName => i,
IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalAnd or BinaryOperatorKind.ConditionalOr } b =>
FindContainsKeyInvocation(b.LeftOperand) ?? FindContainsKeyInvocation(b.RightOperand),
_ => null
};
}

private static bool DictionaryEntryIsModified(IPropertyReferenceOperation dictionaryAccess, IBlockOperation blockOperation)
{
return blockOperation.Operations.OfType<IExpressionStatementOperation>().Any(o =>
o.Operation is IAssignmentOperation { Target: IPropertyReferenceOperation reference } && reference.Property.Equals(dictionaryAccess.Property, SymbolEqualityComparer.Default));
}

private static bool IsDictionaryAccess(IPropertyReferenceOperation propertyReference, INamedTypeSymbol dictionaryType)
{
return propertyReference.Property.IsIndexer && IsDictionaryType(propertyReference.Property.ContainingType, dictionaryType) &&
(propertyReference.Property.OriginalDefinition.Name == IndexerName || propertyReference.Language == "Visual Basic" && propertyReference.Property.OriginalDefinition.Name == IndexerNameVb);
}

private static bool TryGetParentConditionalOperation(IOperation derivedOperation, [NotNullWhen(true)] out IConditionalOperation? conditionalOperation)
{
conditionalOperation = null;
do
{
if (derivedOperation.Parent is IConditionalOperation c)
{
conditionalOperation = c;

return true;
}

derivedOperation = derivedOperation.Parent;
} while (derivedOperation.Parent != null);

return false;
}

private static bool IsDictionaryType(INamedTypeSymbol derived, ISymbol dictionaryType)
{
var constructedDictionaryType = derived.GetBaseTypesAndThis()
Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, I think we only want to warn for the Dictionary provided by .Net, no need to warn for any other derived implementations

Correct me if I am wrong @stephentoub

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 remember something about the analyzer needing to find derived types as well, but of course can't find any evidence of that now 😅
Would it be a bad thing to catch derived types as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think because they could have different implementation than .Net and might not cause the issue we trying to fix with the analyzer. Though in this case I doubt that the implementation of ContainsKey+indexer could avoid double lookup.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. So should I remove it, just to be safe?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I think custom implementation of ContainsKey + indexer could avoid double lookup. So lets not check derived types to avoid possible false positives

Copy link
Contributor Author

@CollinAlpert CollinAlpert May 21, 2022

Choose a reason for hiding this comment

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

Okay, I'll get on that. This also means that we won't flag a ContainsKey on an IDictionary, right? Most likely the implementation is a .NET Dictionary, however this is not guaranteed. This is unfortunate, since most people (myself included) use IDictionary when declaring a Dictionary.

Also, what about ConcurrentDictionary? Do we want to flag that?

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point, most cases the derived types implementation would have same issue, so lets keep checking derived types

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. I made some changes to increase symbol comparisons, however I am still running into issues when comparing i.e. the ConcurrentDictionary.ContainsKey method and the IDictionary.ContainsKey methods. Even their generic arguments are symbolically different, so the only way I found was to compare signature elements (return type, name and parameters) via string comparison. Is there any way to compare an implemented method to it's interface version more reliably?

Copy link
Contributor

@buyaa-n buyaa-n May 27, 2022

Choose a reason for hiding this comment

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

I would use:

public static bool IsOverrideOrImplementationOfInterfaceMember(this ISymbol symbol, [NotNullWhen(returnValue: true)] ISymbol? interfaceMember)
but seems somehow it is not working for this symbols cc @mavasani

.WhereAsArray(x => x.OriginalDefinition.Equals(dictionaryType, SymbolEqualityComparer.Default))
.SingleOrDefault() ?? derived.AllInterfaces
.WhereAsArray(x => x.OriginalDefinition.Equals(dictionaryType, SymbolEqualityComparer.Default))
.SingleOrDefault();

return constructedDictionaryType is not null;
}

private static LocalizableString CreateResource(string resourceName)
{
return new LocalizableResourceString(resourceName, MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeFixes;

namespace Microsoft.NetCore.Analyzers.Runtime
{
public abstract class PreferDictionaryTryGetValueFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(PreferDictionaryTryGetValueAnalyzer.RuleId);

protected static string PreferDictionaryTryGetValueCodeFixTitle => MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryGetValueCodeFixTitle;

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,26 @@
<target state="translated">Toto přetížení metody {0}.{1} může být nebezpečné. Může povolit specifikaci DTD, která může být ohrožená útoky DoS (Denial of Service) nebo může používat XmlResolver, který představuje riziko odhalení informací. Použijte místo toho přetížení, které přijímá instanci XmlReader, má zakázané zpracování DTD a nepoužívá XmlResolver.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueCodeFixTitle">
<source>Use 'TryGetValue(TKey, out TValue)'</source>
<target state="new">Use 'TryGetValue(TKey, out TValue)'</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueTitle">
<source>Prefer the Dictionary's TryGetValue method</source>
<target state="new">Prefer the Dictionary's TryGetValue method</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueMessage">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueDescription">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,26 @@
<target state="translated">Diese Überladung der {0}.{1}-Methode ist potenziell unsicher. Sie kann die Dokumenttypdefinition (DTD) aktivieren, die für DoS-Angriff (Denial of Service) anfällig ist, oder einen XmlResolver verwenden, der anfällig für eine Offenlegung von Informationen sein kann. Verwenden Sie eine Überladung, die stattdessen eine XmlReader-Instanz nutzt, und bei der die DTD-Verarbeitung deaktiviert ist und kein XmlResolver-Vorgang ausgeführt wird.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueCodeFixTitle">
<source>Use 'TryGetValue(TKey, out TValue)'</source>
<target state="new">Use 'TryGetValue(TKey, out TValue)'</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueTitle">
<source>Prefer the Dictionary's TryGetValue method</source>
<target state="new">Prefer the Dictionary's TryGetValue method</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueMessage">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueDescription">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,26 @@
<target state="translated">Es posible que la sobrecarga del método "{0}.{1}" no sea segura. Puede habilitar la definición de tipo de documento (DTD), que puede ser vulnerable a los ataques por denegación de servicio, o bien usar un elemento XmlResolver que puede ser vulnerable a la divulgación de información. Use una sobrecarga que tome una instancia de XmlReader, con el procesamiento de DTD deshabilitado y sin XmlResolver.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueCodeFixTitle">
<source>Use 'TryGetValue(TKey, out TValue)'</source>
<target state="new">Use 'TryGetValue(TKey, out TValue)'</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueTitle">
<source>Prefer the Dictionary's TryGetValue method</source>
<target state="new">Prefer the Dictionary's TryGetValue method</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueMessage">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueDescription">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,26 @@
<target state="translated">Cette surcharge de la méthode '{0}.{1}' est potentiellement non sécurisée. Elle peut activer le traitement DTD (définition de type de document), qui est vulnérable aux attaques par déni de service, ou utiliser une opération XmlResolver, qui est vulnérable à la divulgation d'informations. Employez une surcharge qui accepte plutôt une instance de XmlReader, avec désactivation du traitement DTD et de XmlResolver.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueCodeFixTitle">
<source>Use 'TryGetValue(TKey, out TValue)'</source>
<target state="new">Use 'TryGetValue(TKey, out TValue)'</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueTitle">
<source>Prefer the Dictionary's TryGetValue method</source>
<target state="new">Prefer the Dictionary's TryGetValue method</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueMessage">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
<trans-unit id="PreferDictionaryTryGetValueDescription">
<source>Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</source>
<target state="new">Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check.</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
Loading