diff --git a/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferDictionaryTryGetValueFixer.cs b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferDictionaryTryGetValueFixer.cs new file mode 100644 index 0000000000..3c4c370c05 --- /dev/null +++ b/src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Runtime/CSharpPreferDictionaryTryGetValueFixer.cs @@ -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.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.NetCore.Analyzers.Runtime; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +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(context.CancellationToken).ConfigureAwait(false); + + var dictionaryAccess = root.FindNode(dictionaryAccessLocation.SourceSpan, getInnermostNodeForTie: true); + if (dictionaryAccess is not ElementAccessExpressionSyntax + || root.FindNode(context.Span) is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax containsKeyAccess } containsKeyInvocation) + { + 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, + DeclarationExpression( + IdentifierName(Var), + SingleVariableDesignation(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); + } + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md index aafd3e9b87..d929849d5f 100644 --- a/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md +++ b/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md @@ -12,6 +12,7 @@ CA1850 | Performance | Info | PreferHashDataOverComputeHashAnalyzer, [Documentat CA1851 | Performance | Disabled | AvoidMultipleEnumerations, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851) CA1852 | Performance | Hidden | SealInternalTypes, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852) CA1853 | Performance | Info | DoNotGuardDictionaryRemoveByContainsKey, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1853) +CA1854 | Performance | Info | PreferDictionaryTryGetValueAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1854) CA2019 | Reliability | Info | UseThreadStaticCorrectly, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2019) CA2259 | Usage | Warning | UseThreadStaticCorrectly, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2259) CA5404 | Security | Disabled | DoNotDisableTokenValidationChecks, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca5404) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx index a3c4114dda..4fee9352a2 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx @@ -1933,4 +1933,16 @@ Delegates with managed types as parameters or the return type require runtime marshalling to be enabled in the assembly where the delegate is defined. + + Use 'TryGetValue(TKey, out TValue)' + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueAnalyzer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueAnalyzer.cs new file mode 100644 index 0000000000..fd5c257793 --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueAnalyzer.cs @@ -0,0 +1,179 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +using static Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources; + +namespace Microsoft.NetCore.Analyzers.Runtime +{ + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class PreferDictionaryTryGetValueAnalyzer : DiagnosticAnalyzer + { + public const string RuleId = "CA1854"; + + private const string IndexerName = "this[]"; + private const string IndexerNameVb = "Item"; + private const string ContainsKey = nameof(IDictionary.ContainsKey); + + private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(PreferDictionaryTryGetValueTitle)); + private static readonly LocalizableString s_localizableTryGetValueMessage = CreateLocalizableResourceString(nameof(PreferDictionaryTryGetValueMessage)); + private static readonly LocalizableString s_localizableTryGetValueDescription = CreateLocalizableResourceString(nameof(PreferDictionaryTryGetValueDescription)); + + internal static readonly DiagnosticDescriptor ContainsKeyRule = DiagnosticDescriptorHelper.Create( + RuleId, + s_localizableTitle, + s_localizableTryGetValueMessage, + DiagnosticCategory.Performance, + RuleLevel.IdeSuggestion, + s_localizableTryGetValueDescription, + isPortedFxCopRule: false, + isDataflowRule: false); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(ContainsKeyRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext compilationContext) + { + var compilation = compilationContext.Compilation; + if (!TryGetDictionaryTypeAndMembers(compilation, out var iDictionaryType, out var containsKeySymbol, out var indexerSymbol)) + { + return; + } + + compilationContext.RegisterOperationAction(context => OnOperationAction(context, iDictionaryType, containsKeySymbol, indexerSymbol), OperationKind.PropertyReference); + } + + private static void OnOperationAction(OperationAnalysisContext context, INamedTypeSymbol dictionaryType, IMethodSymbol containsKeySymbol, IPropertySymbol indexerSymbol) + { + var propertyReference = (IPropertyReferenceOperation)context.Operation; + + if (propertyReference.Parent is IAssignmentOperation + || !IsDictionaryAccess(propertyReference, dictionaryType, indexerSymbol) + || !TryGetParentConditionalOperation(propertyReference, out var conditionalOperation) + || !TryGetContainsKeyGuard(conditionalOperation, containsKeySymbol, 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)); + } + + private static bool TryGetDictionaryTypeAndMembers(Compilation compilation, + [NotNullWhen(true)] out INamedTypeSymbol? iDictionaryType, + [NotNullWhen(true)] out IMethodSymbol? containsKeySymbol, + [NotNullWhen(true)] out IPropertySymbol? indexerSymbol) + { + containsKeySymbol = null; + indexerSymbol = null; + if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIDictionary2, out iDictionaryType)) + { + return false; + } + + containsKeySymbol = iDictionaryType.GetMembers().OfType().FirstOrDefault(m => m.Name == ContainsKey); + indexerSymbol = iDictionaryType.GetMembers().OfType().FirstOrDefault(m => m.Name == IndexerName || m.Language == LanguageNames.VisualBasic && m.Name == IndexerNameVb); + + return containsKeySymbol is not null && indexerSymbol is not null; + } + + private static bool TryGetContainsKeyGuard(IConditionalOperation conditionalOperation, IMethodSymbol containsKeySymbol, [NotNullWhen(true)] out IInvocationOperation? containsKeyInvocation) + { + containsKeyInvocation = FindContainsKeyInvocation(conditionalOperation.Condition, containsKeySymbol); + + return containsKeyInvocation is not null; + } + + private static IInvocationOperation? FindContainsKeyInvocation(IOperation baseOperation, IMethodSymbol containsKeyMethod) + { + return baseOperation switch + { + IInvocationOperation i when IsContainsKeyMethod(i.TargetMethod, containsKeyMethod) => i, + IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalAnd or BinaryOperatorKind.ConditionalOr } b => + FindContainsKeyInvocation(b.LeftOperand, containsKeyMethod) ?? FindContainsKeyInvocation(b.RightOperand, containsKeyMethod), + _ => null + }; + } + + private static bool IsContainsKeyMethod(IMethodSymbol suspectedContainsKeyMethod, IMethodSymbol containsKeyMethod) + { + return suspectedContainsKeyMethod.OriginalDefinition.Equals(containsKeyMethod, SymbolEqualityComparer.Default) + || DoesSignatureMatch(suspectedContainsKeyMethod, containsKeyMethod); + } + + private static bool DictionaryEntryIsModified(IPropertyReferenceOperation dictionaryAccess, IBlockOperation blockOperation) + { + return blockOperation.Operations.OfType().Any(o => + o.Operation is IAssignmentOperation { Target: IPropertyReferenceOperation reference } && reference.Property.Equals(dictionaryAccess.Property, SymbolEqualityComparer.Default)); + } + + private static bool IsDictionaryAccess(IPropertyReferenceOperation propertyReference, INamedTypeSymbol dictionaryType, IPropertySymbol indexer) + { + return propertyReference.Property.IsIndexer + && IsDictionaryType(propertyReference.Property.ContainingType, dictionaryType) + && (propertyReference.Property.OriginalDefinition.Equals(indexer, SymbolEqualityComparer.Default) + || DoesSignatureMatch(propertyReference.Property, indexer)); + } + + private static bool TryGetParentConditionalOperation(IOperation derivedOperation, [NotNullWhen(true)] out IConditionalOperation? conditionalOperation) + { + conditionalOperation = null; + do + { + if (derivedOperation.Parent is IConditionalOperation conditional) + { + conditionalOperation = conditional; + + return true; + } + + derivedOperation = derivedOperation.Parent; + } while (derivedOperation.Parent != null); + + return false; + } + + private static bool IsDictionaryType(INamedTypeSymbol suspectedDictionaryType, ISymbol iDictionaryType) + { + // Either the type is the IDictionary it is a type which (indirectly) implements it. + return suspectedDictionaryType.OriginalDefinition.Equals(iDictionaryType, SymbolEqualityComparer.Default) + || suspectedDictionaryType.AllInterfaces.Any((@interface, dictionary) => @interface.OriginalDefinition.Equals(dictionary, SymbolEqualityComparer.Default), iDictionaryType); + } + + // Unfortunately we can't do symbol comparison, since this won't work for i.e. a method in a ConcurrentDictionary comparing against the same method in the IDictionary. + 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); + } + + private static bool DoesSignatureMatch(IPropertySymbol suspected, IPropertySymbol comparator) + { + return suspected.OriginalDefinition.Type.Name == comparator.Type.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); + } + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueFixer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueFixer.cs new file mode 100644 index 0000000000..c2200be6c2 --- /dev/null +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueFixer.cs @@ -0,0 +1,19 @@ +// 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 Microsoft.CodeAnalysis.CodeFixes; + +namespace Microsoft.NetCore.Analyzers.Runtime +{ + public abstract class PreferDictionaryTryGetValueFixer : CodeFixProvider + { + protected const string Var = "var"; + protected const string Value = "value"; + protected const string TryGetValue = nameof(TryGetValue); + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(PreferDictionaryTryGetValueAnalyzer.RuleId); + + protected static string PreferDictionaryTryGetValueCodeFixTitle => MicrosoftNetCoreAnalyzersResources.PreferDictionaryTryGetValueCodeFixTitle; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf index a35edc2b98..a0163a59c1 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf @@ -3002,6 +3002,26 @@ {3}{0} používá typ preview {1} a vyžaduje vyjádření výslovného souhlasu s funkcemi preview. Další informace najdete v {2}. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf index 47c8bfda86..5e13e4659b 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf @@ -3002,6 +3002,26 @@ {3} „{0}“ verwendet den Vorschautyp „{1}“, daher müssen Vorschaufeatures abonniert werden. Weitere Informationen finden Sie unter {2}. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf index 88af9a3b54..4fb7dff8cd 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf @@ -3002,6 +3002,26 @@ {3} '{0}' usa el tipo de vista previa '{1}' y debe participar en las características en versión preliminar. Consulte {2} para obtener más información. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf index dc29506874..8af984161b 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf @@ -3002,6 +3002,26 @@ '{3}'{0} utilise le type de prévisualisation '{1}' et doit opter pour les fonctionnalités de prévisualisation. Voir {2}pour plus d'informations. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf index caf2a1878b..76d6739cee 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf @@ -3002,6 +3002,26 @@ {3} '{0}' utilizza il tipo di anteprima '{1}' e deve quindi acconsentire esplicitamente alle funzionalità di anteprima. Per altre informazioni, vedere {2}. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf index 7b8042b438..72b2428c5d 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf @@ -3002,6 +3002,26 @@ {3} '{0}' はプレビュー型 '{1}' を使用するため、プレビュー機能を選択する必要があります。詳細については、「{2}」を参照してください。 + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf index 075889720c..10c201827a 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf @@ -3002,6 +3002,26 @@ {3} '{0}'은(는) 미리 보기 유형 '{1}'을(를) 사용하며 미리 보기 기능을 선택해야 합니다. 자세한 내용은 {2}를 참조하세요. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf index c894d6b2a4..cd3a767af5 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf @@ -3002,6 +3002,26 @@ {3} „{0}” używa typu w wersji zapoznawczej „{1}” i wymaga wyrażenia zgody na korzystanie z funkcji w wersji zapoznawczej. Aby uzyskać więcej informacji, zobacz {2}. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf index c4e069b49f..241598f94c 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf @@ -3002,6 +3002,26 @@ {3}“{0}” usa o tipo de visualização “{1}” e precisa aceitar os recursos de visualização. Consulte {2} para obter mais informações. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf index 623d3968dc..7ffd9800a1 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf @@ -3002,6 +3002,26 @@ "{0}" {3} использует предварительную версию типа "{1}", поэтому требуется согласие на использование предварительных версий функций. Дополнительные сведения см. в разделе {2}. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf index e1ecb807ee..6f6b0bedb8 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf @@ -3002,6 +3002,26 @@ {3} '{0}', '{1}' önizleme türünü kullandığından önizleme özelliklerini kabul etmesi gerekir. Daha fazla bilgi için bkz. {2}. + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf index 95cf2bce77..e5a6abc02a 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf @@ -3002,6 +3002,26 @@ {3}.“{0}”使用预览类型“{1}”,并且需要选择加入预览功能。有关详细信息,请参阅 {2}。 + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf index eff4249189..277553a568 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf @@ -3002,6 +3002,26 @@ {3} '{0}' 使用預覽類型 '{1}',因此必須加入預覽功能。如需詳細資訊,請參閱 {2}。 + + Use 'TryGetValue(TKey, out TValue)' + Use 'TryGetValue(TKey, out TValue)' + + + + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + + + + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + + \ No newline at end of file diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md index fa3d699baa..308d72dcec 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md @@ -1548,6 +1548,18 @@ Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'. The fo |CodeFix|True| --- +## [CA1854](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1854): Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method + +Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup. + +|Item|Value| +|-|-| +|Category|Performance| +|Enabled|True| +|Severity|Info| +|CodeFix|True| +--- + ## [CA2000](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead. diff --git a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif index 9978b965cd..cec4eac782 100644 --- a/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif +++ b/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif @@ -2860,6 +2860,26 @@ ] } }, + "CA1854": { + "id": "CA1854", + "shortDescription": "Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method", + "fullDescription": "Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check. 'ContainsKey' and the indexer both would lookup the key under the hood, so using 'TryGetValue' removes the extra lookup.", + "defaultLevel": "note", + "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1854", + "properties": { + "category": "Performance", + "isEnabledByDefault": true, + "typeName": "PreferDictionaryTryGetValueAnalyzer", + "languages": [ + "C#", + "Visual Basic" + ], + "tags": [ + "Telemetry", + "EnabledRuleInAggressiveMode" + ] + } + }, "CA2000": { "id": "CA2000", "shortDescription": "Dispose objects before losing scope", diff --git a/src/NetAnalyzers/RulesMissingDocumentation.md b/src/NetAnalyzers/RulesMissingDocumentation.md index c882b1714b..907430eedc 100644 --- a/src/NetAnalyzers/RulesMissingDocumentation.md +++ b/src/NetAnalyzers/RulesMissingDocumentation.md @@ -7,5 +7,6 @@ CA1420 | | This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied | CA1852 | | Seal internal types | CA1853 | | Unnecessary call to 'Dictionary.ContainsKey(key)' | +CA1854 | | Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method | CA2019 | | Improper 'ThreadStatic' field initialization | CA2259 | | 'ThreadStatic' only affects static fields | diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueMethodsTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueMethodsTests.cs new file mode 100644 index 0000000000..a662ed1bcf --- /dev/null +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/PreferDictionaryTryGetValueMethodsTests.cs @@ -0,0 +1,541 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< + Microsoft.NetCore.Analyzers.Runtime.PreferDictionaryTryGetValueAnalyzer, + Microsoft.NetCore.CSharp.Analyzers.Runtime.CSharpPreferDictionaryTryGetValueFixer>; +using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier< + Microsoft.NetCore.Analyzers.Runtime.PreferDictionaryTryGetValueAnalyzer, + Microsoft.NetCore.VisualBasic.Analyzers.Runtime.BasicPreferDictionaryTryGetValueFixer>; + +namespace Microsoft.NetCore.Analyzers.Runtime.UnitTests +{ + public class PreferDictionaryTryGetValueMethodsTests + { + #region C# Tests + + private const string CSharpTemplate = @" +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Test +{{ + public class TestClass + {{ + public int TestMethod() + {{ + {0} + }} + + private class MyDictionary {{ + public bool ContainsKey(TKey key) {{ + return true; + }} + + public TValue this[TKey key] {{ get => default; set {{}} }} + }} + }} +}}"; + + private const string DictionaryContainsKeyPrintValue = @" + string key = ""key""; + Dictionary data = new Dictionary(); + if ({|#0:data.ContainsKey(key)|}) + { + Console.WriteLine({|#1:data[key]|}); + } + + return 0;"; + + private const string DictionaryContainsKeyPrintValueFixed = @" + string key = ""key""; + Dictionary data = new Dictionary(); + if (data.TryGetValue(key, out var value)) + { + Console.WriteLine(value); + } + + return 0;"; + + private const string DictionaryContainsKeyReturnValue = @" + string key = ""key""; + ConcurrentDictionary data = new ConcurrentDictionary(); + if ({|#0:data.ContainsKey(key)|}) + { + return {|#1:data[key]|}; + } + + return 0;"; + + private const string DictionaryContainsKeyReturnValueFixed = @" + string key = ""key""; + ConcurrentDictionary data = new ConcurrentDictionary(); + if (data.TryGetValue(key, out var value)) + { + return value; + } + + return 0;"; + + private const string DictionaryContainsKeyMultipleStatementsInIf = @" + string key = ""key""; + IDictionary data = new Dictionary(); + if ({|#0:data.ContainsKey(key)|}) + { + Console.WriteLine(2); + var x = 2; + Console.WriteLine({|#1:data[key]|}); + + return x; + } + + return 0;"; + + private const string DictionaryContainsKeyMultipleStatementsInIfFixed = @" + string key = ""key""; + IDictionary data = new Dictionary(); + if (data.TryGetValue(key, out var value)) + { + Console.WriteLine(2); + var x = 2; + Console.WriteLine(value); + + return x; + } + + return 0;"; + + private const string DictionaryContainsKeyMultipleConditions = @" + string key = ""key""; + IDictionary data = new Dictionary(); + if (key == ""key"" && {|#0:data.ContainsKey(key)|}) + { + Console.WriteLine(2); + var x = 2; + Console.WriteLine({|#1:data[key]|}); + + return x; + } + + return 0;"; + + private const string DictionaryContainsKeyMultipleConditionsFixed = @" + string key = ""key""; + IDictionary data = new Dictionary(); + if (key == ""key"" && data.TryGetValue(key, out var value)) + { + Console.WriteLine(2); + var x = 2; + Console.WriteLine(value); + + return x; + } + + return 0;"; + + private const string DictionaryContainsKeyNestedDictionaryAccess = @" + string key = ""key""; + IDictionary data = new Dictionary(); + if (key == ""key"" && {|#0:data.ContainsKey(key)|}) + { + Console.WriteLine(2); + var x = 2; + Console.WriteLine(Wrapper({|#1:data[key]|})); + + return x; + } + + int Wrapper(int i) { + return i; + } + + return 0;"; + + private const string DictionaryContainsKeyNestedDictionaryAccessFixed = @" + string key = ""key""; + IDictionary data = new Dictionary(); + if (key == ""key"" && data.TryGetValue(key, out var value)) + { + Console.WriteLine(2); + var x = 2; + Console.WriteLine(Wrapper(value)); + + return x; + } + + int Wrapper(int i) { + return i; + } + + return 0;"; + + private const string DictionaryContainsKeyTernary = @" + string key = ""key""; + IDictionary data = new Dictionary(); + + return {|#0:data.ContainsKey(key)|} ? {|#1:data[key]|} : 2;"; + + private const string DictionaryContainsKeyTernaryFixed = @" + string key = ""key""; + IDictionary data = new Dictionary(); + + return data.TryGetValue(key, out var value) ? value : 2;"; + + #region NoDiagnostic + + private const string DictionaryContainsKeyModifyDictionary = @" + string key = ""key""; + IDictionary data = new Dictionary(); + if (data.ContainsKey(key)) + { + Console.WriteLine(2); + data[key] = 2; + Console.WriteLine(data[key]); + + return 2; + } + + return 0;"; + + private const string DictionaryContainsKeyNonIDictionary = @" + string key = ""key""; + MyDictionary data = new MyDictionary(); + if (data.ContainsKey(key)) + { + Console.WriteLine(2); + Console.WriteLine(data[key]); + + return 2; + } + + return 0;"; + + private const string DictionaryContainsKeyNotGuardedByContainsKey = @" + string key = ""key""; + int value = 3; + Dictionary data = new Dictionary(); + if (data.ContainsValue(value)) + { + Console.WriteLine(data[key]); + } + + return 0;"; + + #endregion + + #endregion + + #region VB Tests + + private const string VbTemplate = @" +Imports System +Imports System.Collections +Imports System.Collections.Concurrent +Imports System.Collections.Generic +Imports System.Linq + +Namespace Test + Public Class TestClass + Public Function TestMethod() As Integer + {0} + End Function + + Private Class MyDictionary(Of TKey, TValue) + Public Function ContainsKey(ByVal key As TKey) As Boolean + Return True + End Function + + Default Public Property Item(ByVal key As TKey) As TValue + Get + Return Nothing + End Get + Set(ByVal value As TValue) + End Set + End Property + End Class + End Class +End Namespace"; + + private const string VbDictionaryContainsKeyPrintValue = @" + Dim key As String = ""key"" + Dim data As Dictionary(Of String, Guid) = New Dictionary(Of String, Guid)() + + If {|#0:data.ContainsKey(key)|} Then + Console.WriteLine({|#1:data(key)|}) + End If + + Return 0"; + + private const string VbDictionaryContainsKeyPrintValueFixed = @" + Dim key As String = ""key"" + Dim data As Dictionary(Of String, Guid) = New Dictionary(Of String, Guid)() + + Dim value As Guid + If data.TryGetValue(key, value) Then + Console.WriteLine(value) + End If + + Return 0"; + + private const string VbDictionaryContainsKeyReturnValue = @" + Dim key As String = ""key"" + Dim data As ConcurrentDictionary(Of String, Integer) = New ConcurrentDictionary(Of String, Integer)() + + If {|#0:data.ContainsKey(key)|} Then + Return {|#1:data(key)|} + End If + + Return 0"; + + private const string VbDictionaryContainsKeyReturnValueFixed = @" + Dim key As String = ""key"" + Dim data As ConcurrentDictionary(Of String, Integer) = New ConcurrentDictionary(Of String, Integer)() + + Dim value As Integer + If data.TryGetValue(key, value) Then + Return value + End If + + Return 0"; + + private const string VbDictionaryContainsKeyMultipleStatementsInIf = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + If {|#0:data.ContainsKey(key)|} Then + Console.WriteLine(2) + Dim x = 2 + Console.WriteLine({|#1:data(key)|}) + + Return x + End If + + Return 0"; + + private const string VbDictionaryContainsKeyMultipleStatementsInIfFixed = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + Dim value As Integer + If data.TryGetValue(key, value) Then + Console.WriteLine(2) + Dim x = 2 + Console.WriteLine(value) + + Return x + End If + + Return 0"; + + private const string VbDictionaryContainsKeyMultipleConditions = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + If key = ""key"" AndAlso {|#0:data.ContainsKey(key)|} Then + Console.WriteLine(2) + Dim x = 2 + Console.WriteLine({|#1:data(key)|}) + + Return x + End If + + Return 0"; + + private const string VbDictionaryContainsKeyMultipleConditionsFixed = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + Dim value As Integer + If key = ""key"" AndAlso data.TryGetValue(key, value) Then + Console.WriteLine(2) + Dim x = 2 + Console.WriteLine(value) + + Return x + End If + + Return 0"; + + private const string VbDictionaryContainsKeyNestedDictionaryAccess = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + If key = ""key"" AndAlso {|#0:data.ContainsKey(key)|} Then + Console.WriteLine(2) + Dim x = 2 + Dim wrapper = Function(i As Integer) As Integer + Return i + End Function + Console.WriteLine(wrapper({|#1:data(key)|})) + + Return x + End If + + Return 0"; + + private const string VbDictionaryContainsKeyNestedDictionaryAccessFixed = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + Dim value As Integer + If key = ""key"" AndAlso data.TryGetValue(key, value) Then + Console.WriteLine(2) + Dim x = 2 + Dim wrapper = Function(i As Integer) As Integer + Return i + End Function + Console.WriteLine(wrapper(value)) + + Return x + End If + + Return 0"; + + private const string VbDictionaryContainsKeyTernary = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + Return If({|#0:data.ContainsKey(key)|}, {|#1:data(key)|}, 2)"; + + private const string VbDictionaryContainsKeyTernaryFixed = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + Dim value As Integer + Return If(data.TryGetValue(key, value), value, 2)"; + + #region NoDiagnostic + + private const string VbDictionaryContainsKeyModifyDictionary = @" + Dim key As String = ""key"" + Dim data As IDictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + If data.ContainsKey(key) Then + Console.WriteLine(2) + data(key) = 2 + Console.WriteLine(data(key)) + + Return 2 + End If + + Return 0"; + + private const string VbDictionaryContainsKeyNonIDictionary = @" + Dim key As String = ""key"" + Dim data As MyDictionary(Of String, Integer) = New MyDictionary(Of String, Integer)() + + If data.ContainsKey(key) Then + Console.WriteLine(2) + Console.WriteLine(data(key)) + + Return 2 + End If + + Return 0"; + + private const string VbDictionaryContainsKeyNotGuardedByContainsKey = @" + Dim key As String = ""key"" + Dim value As Integer = 3 + Dim data As Dictionary(Of String, Integer) = New Dictionary(Of String, Integer)() + + If data.ContainsValue(value) Then + Console.WriteLine(data(key)) + End If + + Return 0"; + + #endregion + + #endregion + + [Theory] + [InlineData(DictionaryContainsKeyPrintValue, DictionaryContainsKeyPrintValueFixed)] + [InlineData(DictionaryContainsKeyReturnValue, DictionaryContainsKeyReturnValueFixed)] + [InlineData(DictionaryContainsKeyMultipleStatementsInIf, DictionaryContainsKeyMultipleStatementsInIfFixed)] + [InlineData(DictionaryContainsKeyMultipleConditions, DictionaryContainsKeyMultipleConditionsFixed)] + [InlineData(DictionaryContainsKeyNestedDictionaryAccess, DictionaryContainsKeyNestedDictionaryAccessFixed)] + [InlineData(DictionaryContainsKeyTernary, DictionaryContainsKeyTernaryFixed)] + public Task ShouldReportDiagnostic(string codeSnippet, string fixedCodeSnippet) + { + string testCode = CreateCSharpCode(codeSnippet); + string fixedCode = CreateCSharpCode(fixedCodeSnippet); + var diagnostic = VerifyCS.Diagnostic(PreferDictionaryTryGetValueAnalyzer.ContainsKeyRule).WithLocation(0).WithLocation(1); + + return new VerifyCS.Test + { + TestCode = testCode, + FixedCode = fixedCode, + ReferenceAssemblies = ReferenceAssemblies.Net.Net60, + ExpectedDiagnostics = { diagnostic } + }.RunAsync(); + } + + [Theory] + [InlineData(DictionaryContainsKeyModifyDictionary)] + [InlineData(DictionaryContainsKeyNonIDictionary)] + [InlineData(DictionaryContainsKeyNotGuardedByContainsKey)] + public Task ShouldNotReportDiagnostic(string codeSnippet) + { + string testCode = CreateCSharpCode(codeSnippet); + + return new VerifyCS.Test + { + TestCode = testCode, + ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + }.RunAsync(); + } + + [Theory] + [InlineData(VbDictionaryContainsKeyPrintValue, VbDictionaryContainsKeyPrintValueFixed)] + [InlineData(VbDictionaryContainsKeyReturnValue, VbDictionaryContainsKeyReturnValueFixed)] + [InlineData(VbDictionaryContainsKeyMultipleStatementsInIf, VbDictionaryContainsKeyMultipleStatementsInIfFixed)] + [InlineData(VbDictionaryContainsKeyMultipleConditions, VbDictionaryContainsKeyMultipleConditionsFixed)] + [InlineData(VbDictionaryContainsKeyNestedDictionaryAccess, VbDictionaryContainsKeyNestedDictionaryAccessFixed)] + [InlineData(VbDictionaryContainsKeyTernary, VbDictionaryContainsKeyTernaryFixed)] + public Task VbShouldReportDiagnostic(string codeSnippet, string fixedCodeSnippet) + { + string testCode = CreateVbCode(codeSnippet); + string fixedCode = CreateVbCode(fixedCodeSnippet); + var diagnostic = VerifyVB.Diagnostic(PreferDictionaryTryGetValueAnalyzer.ContainsKeyRule).WithLocation(0).WithLocation(1); + + return new VerifyVB.Test + { + TestCode = testCode, + FixedCode = fixedCode, + ReferenceAssemblies = ReferenceAssemblies.Net.Net60, + ExpectedDiagnostics = { diagnostic } + }.RunAsync(); + } + + [Theory] + [InlineData(VbDictionaryContainsKeyModifyDictionary)] + [InlineData(VbDictionaryContainsKeyNonIDictionary)] + [InlineData(VbDictionaryContainsKeyNotGuardedByContainsKey)] + public Task VbShouldNotReportDiagnostic(string codeSnippet) + { + string testCode = CreateVbCode(codeSnippet); + + return new VerifyVB.Test + { + TestCode = testCode, + ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + }.RunAsync(); + } + + private static string CreateCSharpCode(string content) + { + return string.Format(CultureInfo.InvariantCulture, CSharpTemplate, content); + } + + private static string CreateVbCode(string content) + { + return string.Format(CultureInfo.InvariantCulture, VbTemplate, content); + } + } +} \ No newline at end of file diff --git a/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Runtime/BasicPreferDictionaryTryGetValueFixer.vb b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Runtime/BasicPreferDictionaryTryGetValueFixer.vb new file mode 100644 index 0000000000..86f6ea02f2 --- /dev/null +++ b/src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Runtime/BasicPreferDictionaryTryGetValueFixer.vb @@ -0,0 +1,75 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +Imports System.Threading +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.CodeActions +Imports Microsoft.CodeAnalysis.CodeFixes +Imports Microsoft.CodeAnalysis.Editing +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.NetCore.Analyzers.Runtime + +Namespace Microsoft.NetCore.VisualBasic.Analyzers.Runtime + + Public NotInheritable Class BasicPreferDictionaryTryGetValueFixer + Inherits PreferDictionaryTryGetValueFixer + + Public Overrides Async Function RegisterCodeFixesAsync(context As CodeFixContext) As Task + Dim diagnostic = context.Diagnostics.FirstOrDefault() + Dim dictionaryAccessLocation = diagnostic?.AdditionalLocations(0) + If dictionaryAccessLocation Is Nothing + Return + End If + + Dim document = context.Document + Dim root = Await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(False) + + Dim dictionaryAccess = TryCast(root.FindNode(dictionaryAccessLocation.SourceSpan, getInnermostNodeForTie:=True), InvocationExpressionSyntax) + Dim containsKeyInvocation = TryCast(root.FindNode(context.Span), InvocationExpressionSyntax) + Dim containsKeyAccess = TryCast(containsKeyInvocation?.Expression, MemberAccessExpressionSyntax) + If _ + dictionaryAccess Is Nothing Or containsKeyInvocation Is Nothing Or + containsKeyAccess Is Nothing Then + Return + End If + + Dim semanticModel = Await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(False) + Dim dictionaryValueType = GetDictionaryValueType(semanticModel, dictionaryAccess.Expression) + Dim replaceFunction = Async Function(ct as CancellationToken) As Task(Of Document) + Dim editor = Await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(False) + Dim generator = editor.Generator + + Dim tryGetValueAccess = generator.MemberAccessExpression(containsKeyAccess.Expression, TryGetValue) + Dim keyArgument = containsKeyInvocation.ArgumentList.Arguments.FirstOrDefault() + Dim valueAssignment = generator.LocalDeclarationStatement(dictionaryValueType, Value).WithLeadingTrivia(SyntaxFactory.CarriageReturn).WithoutTrailingTrivia() + Dim tryGetValueInvocation = generator.InvocationExpression(tryGetValueAccess, keyArgument, generator.Argument(generator.IdentifierName(Value))) + + Dim ifStatement As SyntaxNode = containsKeyAccess.AncestorsAndSelf().OfType(Of MultiLineIfBlockSyntax).FirstOrDefault() + If ifStatement Is Nothing + ifStatement = containsKeyAccess.AncestorsAndSelf().OfType(Of SingleLineIfStatementSyntax).FirstOrDefault() + End If + + If ifStatement Is Nothing + ' For ternary expressions, we need to add the value assignment before the parent of the expression, since the ternary expression is not an alone-standing expression. + ifStatement = containsKeyAccess.AncestorsAndSelf().OfType(Of TernaryConditionalExpressionSyntax).FirstOrDefault()?.Parent + End If + + editor.InsertBefore(ifStatement, valueAssignment) + editor.ReplaceNode(containsKeyInvocation, tryGetValueInvocation) + editor.ReplaceNode(dictionaryAccess, generator.IdentifierName(Value)) + + Return editor.GetChangedDocument() + End Function + + Dim action = CodeAction.Create(PreferDictionaryTryGetValueCodeFixTitle, replaceFunction, PreferDictionaryTryGetValueCodeFixTitle) + context.RegisterCodeFix(action, context.Diagnostics) + End Function + + Private Shared Function GetDictionaryValueType(semanticModel As SemanticModel, dictionary As SyntaxNode) As ITypeSymbol + Dim symbol = DirectCast(semanticModel.GetSymbolInfo(dictionary).Symbol, ILocalSymbol) + Dim type = DirectCast(symbol.Type, INamedTypeSymbol) + Dim arguments = type.TypeArguments + Return arguments(1) + End Function + End Class +End Namespace \ No newline at end of file diff --git a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt index ecda7d17a7..428f549aad 100644 --- a/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt +++ b/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt @@ -12,7 +12,7 @@ Design: CA2210, CA1000-CA1070 Globalization: CA2101, CA1300-CA1311 Mobility: CA1600-CA1601 -Performance: HA, CA1800-CA1853 +Performance: HA, CA1800-CA1854 Security: CA2100-CA2153, CA2300-CA2330, CA3000-CA3147, CA5300-CA5405 Usage: CA1801, CA1806, CA1816, CA2200-CA2209, CA2211-CA2259 Naming: CA1700-CA1727 @@ -35,4 +35,4 @@ RoslynDiagnosticsDesign: RS0000-RS0999 RoslynDiagnosticsMaintainability: RS0000-RS0999 RoslynDiagnosticsPerformance: RS0000-RS0999 RoslynDiagnosticsReliability: RS0000-RS0999 -RoslynDiagnosticsUsage: RS0000-RS0999 +RoslynDiagnosticsUsage: RS0000-RS0999 \ No newline at end of file