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

Don't guard Dictionary<K, V>.Remove(key) by ContainsKey(key) #4836

Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7245b4e
Early stab at #33797 Don't guard Dictionary<K, V>.Remove(key) by Cont…
Feb 8, 2021
17f1053
Fix VB tests
Feb 11, 2021
06c0391
Implement and test a single-statement block
Feb 11, 2021
6fe0b87
Assign the correct rule ID, fix whitespace, add a comment
Feb 11, 2021
30bf3d7
Added a CodeFixProvider/CodeAction
Feb 11, 2021
b344987
Apply suggestions from code review
chucker Feb 16, 2021
6a2806b
Apply more suggestions from code review
Feb 16, 2021
bb61880
We don't need {|#0: |} notation if we only have one rule
Feb 16, 2021
cd81756
The fixed source doesn't have diagnostics
Feb 16, 2021
ede496c
Use DocumentChangeAction and replaces nodes instead of editing text
Feb 16, 2021
d78addf
formatting; re-ran 'pack' target
Feb 16, 2021
9041727
removed obsolete code
Feb 16, 2021
7edc5df
Fixer should have a title distinct from analyzer
Feb 17, 2021
5f97bf3
Apply suggestions from code review
chucker Feb 17, 2021
46621be
Additional tests
Feb 18, 2021
6055450
Update src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/…
chucker Feb 17, 2021
accc0bd
Inline calls to CreateResource
Feb 18, 2021
916f0e3
Mark the rule as unnecessary, to fade it out in the IDE
Feb 18, 2021
513a6af
Simplify/generalize name of fixer action
Feb 18, 2021
704f32e
Fixed a dumb regression (all strings were empty) from f01cb8b
Feb 18, 2021
2a8fc92
Simplified the description
Feb 18, 2021
e4f50eb
Added HasElseBlock_NoDiagnostic_CS test
Feb 20, 2021
bca7332
Register the conditional instead of the invocation inside the condition
Mar 5, 2021
26f6f3e
Test Remove() with out param
Mar 5, 2021
cce22c2
For additional statements, warn (but don't offer a fix)
Mar 6, 2021
7d97c6a
NegatedCondition_NoDiagnostic_CS now reports a diagnostic; also porte…
Mar 6, 2021
eb84634
Preserve trivia (e.g., code comments)
Mar 9, 2021
47bfc85
fixed a unit test regression due to incorrect whitespace
Mar 9, 2021
e82a37d
if there's an else block, but the if block _only_ contains Remove, ne…
Mar 10, 2021
f15f370
Bumped rule ID to 1849
Sep 1, 2021
b49cbf6
Newer file headers
Sep 2, 2021
03136aa
Re-ran /t:pack
Sep 2, 2021
7a6af81
invocationOperation might be null
Sep 2, 2021
1e75911
Fix IDE0073
Sep 2, 2021
7b6917a
AnalyzerReleases.Unshipped.md is apparently being generated incorrectly?
Sep 2, 2021
a952e90
Add a test for 5c8c8fe
Sep 2, 2021
2c9d717
Merge conflicts
buyaa-n Feb 1, 2022
67e3398
Apply feedbacks, restrict to Dictionary type methods
buyaa-n Feb 2, 2022
7fc1933
Merge conflicts
buyaa-n Feb 2, 2022
7bb8646
Fix build errors
buyaa-n Feb 2, 2022
45fea08
Apply feedback
buyaa-n Feb 4, 2022
0d93a8d
Merge conflicts
buyaa-n Mar 30, 2022
3ddac4c
Typo
buyaa-n Mar 30, 2022
191c334
Apply feedbacks
buyaa-n Apr 27, 2022
63fc959
Merge conflicts
buyaa-n Apr 27, 2022
d91f724
Update with new ID and regenerate files
buyaa-n Apr 27, 2022
1078915
Apply feedback, make remove2Param optional
buyaa-n Apr 28, 2022
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,81 @@
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.NetCore.Analyzers.Performance;

namespace Microsoft.NetCore.CSharp.Analyzers.Performance
{
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class CSharpDoNotGuardDictionaryRemoveByContainsKeyFixer : DoNotGuardDictionaryRemoveByContainsKeyFixer
{
protected override bool SyntaxSupportedByFixer(SyntaxNode conditionalSyntax)
{
if (conditionalSyntax is ConditionalExpressionSyntax conditionalExpressionSyntax)
return conditionalExpressionSyntax.WhenTrue.ChildNodes().Count() == 1;

if (conditionalSyntax is IfStatementSyntax)
return true;

return false;
}

protected override Document ReplaceConditionWithChild(Document document, SyntaxNode root,
SyntaxNode conditionalOperationNode,
SyntaxNode childOperationNode)
{
SyntaxNode newRoot;

// if there's a false (else) block, negate the condition and replace the single true statement with it

if (conditionalOperationNode is ConditionalExpressionSyntax conditionalExpressionSyntax &&
conditionalExpressionSyntax.WhenFalse.ChildNodes().Any())
{
var negatedExpression = GetNegatedExpression(document, childOperationNode);

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

newRoot = root.ReplaceNode(conditionalOperationNode, newConditionalOperationNode);
}
else if (conditionalOperationNode is IfStatementSyntax ifStatementSyntax && ifStatementSyntax.Else != null)
{
var negatedExpression = GetNegatedExpression(document, childOperationNode);

SyntaxNode newConditionalOperationNode = ifStatementSyntax
.WithCondition((ExpressionSyntax)negatedExpression)
.WithStatement(ifStatementSyntax.Else.Statement)
.WithElse(null)
.WithAdditionalAnnotations(Formatter.Annotation).WithTriviaFrom(conditionalOperationNode);

newRoot = root.ReplaceNode(conditionalOperationNode, newConditionalOperationNode);
}
else
{
// preserve formatting and trivia

SyntaxNode newConditionNode = childOperationNode
.WithAdditionalAnnotations(Formatter.Annotation)
.WithTriviaFrom(conditionalOperationNode);

newRoot = root.ReplaceNode(conditionalOperationNode, newConditionNode);
}

return document.WithSyntaxRoot(newRoot);
}

private static SyntaxNode GetNegatedExpression(Document document, SyntaxNode newConditionNode)
{
var generator = SyntaxGenerator.GetGenerator(document);
return generator.LogicalNotExpression(((ExpressionStatementSyntax)newConditionNode).Expression.WithoutTrivia());
}
}
}
1 change: 1 addition & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ CA1849 | Performance | Disabled | UseAsyncMethodInAsyncContext, [Documentation](
CA1850 | Performance | Info | PreferHashDataOverComputeHashAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1850)
CA5404 | Security | Disabled | DoNotDisableTokenValidationChecks, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca5404)
CA5405 | Security | Disabled | DoNotAlwaysSkipTokenValidationInDelegates, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca5405)
CA1851 | Performance | Info | DoNotGuardDictionaryRemoveByContainsKey, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849)
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,18 @@
<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="DoNotGuardDictionaryRemoveByContainsKeyDescription" xml:space="preserve">
<value>Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'. The former already checks whether the key exists, and will not throw if it does not.</value>
</data>
<data name="DoNotGuardDictionaryRemoveByContainsKeyMessage" xml:space="preserve">
<value>Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'</value>
</data>
<data name="DoNotGuardDictionaryRemoveByContainsKeyTitle" xml:space="preserve">
<value>Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'</value>
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="RemoveRedundantGuardCallCodeFixTitle" xml:space="preserve">
<value>Remove redundant guard call</value>
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="BufferBlockCopyLengthMessage" xml:space="preserve">
<value>'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument. Using 'Array.Length' may not match the number of bytes that needs to be copied.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.NetCore.Analyzers.Performance
{
public abstract class DoNotGuardDictionaryRemoveByContainsKeyFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(DoNotGuardDictionaryRemoveByContainsKey.RuleId);

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);

if (node is null)
{
return;
}

var diagnostic = context.Diagnostics.FirstOrDefault();

if (!TryParseLocationInfo(diagnostic, DoNotGuardDictionaryRemoveByContainsKey.ConditionalOperation, out var conditionalOperationSpan) ||
!TryParseLocationInfo(diagnostic, DoNotGuardDictionaryRemoveByContainsKey.ChildStatementOperation, out var childStatementOperationSpan) ||
root.FindNode(conditionalOperationSpan) is not SyntaxNode conditionalSyntax ||
root.FindNode(childStatementOperationSpan) is not SyntaxNode childStatementSyntax)
{
return;
}

// we only offer a fixer if 'Remove' is the _only_ statement
if (!SyntaxSupportedByFixer(conditionalSyntax))
return;

context.RegisterCodeFix(new DoNotGuardDictionaryRemoveByContainsKeyCodeAction(_ =>
Task.FromResult(ReplaceConditionWithChild(context.Document, root, conditionalSyntax, childStatementSyntax))),
diagnostic);
}

protected abstract bool SyntaxSupportedByFixer(SyntaxNode conditionalSyntax);

protected abstract Document ReplaceConditionWithChild(Document document, SyntaxNode root,
SyntaxNode conditionalOperationNode,
SyntaxNode childOperationNode);

private static bool TryParseLocationInfo(Diagnostic diagnostic, string propertyKey, out TextSpan span)
{
span = default;

if (!diagnostic.Properties.TryGetValue(propertyKey, out var locationInfo))
return false;

var parts = locationInfo.Split(DoNotGuardDictionaryRemoveByContainsKey.AdditionalDocumentLocationInfoSeparatorArray, StringSplitOptions.None);
if (parts.Length != 2 ||
!int.TryParse(parts[0], out var spanStart) ||
!int.TryParse(parts[1], out var spanLength))
{
return false;
}

span = new TextSpan(spanStart, spanLength);
return true;
}

private class DoNotGuardDictionaryRemoveByContainsKeyCodeAction : DocumentChangeAction
{
public DoNotGuardDictionaryRemoveByContainsKeyCodeAction(Func<CancellationToken, Task<Document>> action)
: base(MicrosoftNetCoreAnalyzersResources.RemoveRedundantGuardCallCodeFixTitle, action,
DoNotGuardDictionaryRemoveByContainsKey.RuleId)
{ }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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 System.Diagnostics.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
chucker marked this conversation as resolved.
Show resolved Hide resolved
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Performance
{
using static MicrosoftNetCoreAnalyzersResources;

[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DoNotGuardDictionaryRemoveByContainsKey : DiagnosticAnalyzer
{
internal const string RuleId = "CA1851";

public const string AdditionalDocumentLocationInfoSeparator = ";;";
public static readonly string[] AdditionalDocumentLocationInfoSeparatorArray = new[] { AdditionalDocumentLocationInfoSeparator };
public const string ConditionalOperation = nameof(ConditionalOperation);
public const string ChildStatementOperation = nameof(ChildStatementOperation);

internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
chucker marked this conversation as resolved.
Show resolved Hide resolved
RuleId,
CreateLocalizableResourceString(nameof(DoNotGuardDictionaryRemoveByContainsKeyTitle)),
CreateLocalizableResourceString(nameof(DoNotGuardDictionaryRemoveByContainsKeyMessage)),
DiagnosticCategory.Performance,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(DoNotGuardDictionaryRemoveByContainsKeyDescription)),
isPortedFxCopRule: false,
isDataflowRule: false,
additionalCustomTags: WellKnownDiagnosticTags.Unnecessary);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

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

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
if (!TryGetDictionaryTypeAndContainsKeyeMethod(context.Compilation, out var dictionaryType, out var containsKeyMethod))
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
{
return;
}

context.RegisterOperationAction(context => AnalyzeOperation(context, dictionaryType, containsKeyMethod), OperationKind.Conditional);

static void AnalyzeOperation(OperationAnalysisContext context, INamedTypeSymbol dictionaryType, IMethodSymbol containsKeyMethod)
{
var conditionalOperation = (IConditionalOperation)context.Operation;

IInvocationOperation? invocationOperation = null;

switch (conditionalOperation.Condition)
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
{
case IInvocationOperation iOperation:
invocationOperation = iOperation;
break;
case IUnaryOperation unaryOperation when unaryOperation.OperatorKind == UnaryOperatorKind.Not:
if (unaryOperation.Operand is IInvocationOperation operand)
invocationOperation = operand;
break;
default:
return;
}

if (invocationOperation == null || !invocationOperation.TargetMethod.OriginalDefinition.Equals(containsKeyMethod, SymbolEqualityComparer.Default))
{
return;
}

if (conditionalOperation.WhenTrue.Children.Any())
{
var properties = ImmutableDictionary.CreateBuilder<string, string?>();
properties[ConditionalOperation] = CreateLocationInfo(conditionalOperation.Syntax);

switch (conditionalOperation.WhenTrue.Children.First())
{
case IInvocationOperation childInvocationOperation:
if (childInvocationOperation.TargetMethod.Name == "Remove" &&
childInvocationOperation.TargetMethod.OriginalDefinition.ContainingType.Equals(dictionaryType, SymbolEqualityComparer.Default))
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
{
properties[ChildStatementOperation] = CreateLocationInfo(childInvocationOperation.Syntax.Parent);
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved

context.ReportDiagnostic(invocationOperation.CreateDiagnostic(Rule, properties.ToImmutable()));
}

break;
case IExpressionStatementOperation childStatementOperation:
/*
* If the if statement contains a block, only proceed if one of the methods calls Remove.
* However, a fixer is only offered if there is a single method in the block.
*/

var nestedInvocationOperation = childStatementOperation.Children.OfType<IInvocationOperation>()
.FirstOrDefault(op => op.TargetMethod.Name == "Remove" &&
op.TargetMethod.OriginalDefinition.ContainingType.Equals(dictionaryType, SymbolEqualityComparer.Default));

if (nestedInvocationOperation != null)
{
properties[ChildStatementOperation] = CreateLocationInfo(nestedInvocationOperation.Syntax.Parent);

context.ReportDiagnostic(invocationOperation.CreateDiagnostic(Rule, properties.ToImmutable()));
}

break;
default:
break;
}
}
}
static bool TryGetDictionaryTypeAndContainsKeyeMethod(Compilation compilation, [NotNullWhen(true)] out INamedTypeSymbol? dictionaryType, [NotNullWhen(true)] out IMethodSymbol? containsKeyMethod)
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
{
containsKeyMethod = null;

if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericDictionary2, out dictionaryType))
{
return false;
}

foreach (var m in dictionaryType.GetMembers().OfType<IMethodSymbol>())
{
if (m.ReturnType.SpecialType == SpecialType.System_Boolean &&
m.Parameters.Length == 1 &&
m.Name == "ContainsKey" &&
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
m.Parameters[0].Name == "key")
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
{
containsKeyMethod = m;
return true;
}
}

return false;
}
}

private static string CreateLocationInfo(SyntaxNode syntax)
{
// see DiagnosticDescriptorCreationAnalyzer

var location = syntax.GetLocation();
var span = location.SourceSpan;

return $"{span.Start}{AdditionalDocumentLocationInfoSeparator}{span.Length}";
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,21 @@
<target state="translated">Nezakazujte ServicePointManagerSecurityProtocols</target>
<note />
</trans-unit>
<trans-unit id="DoNotGuardDictionaryRemoveByContainsKeyDescription">
<source>Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'. The former already checks whether the key exists, and will not throw if it does not.</source>
<target state="new">Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'. The former already checks whether the key exists, and will not throw if it does not.</target>
<note />
</trans-unit>
<trans-unit id="DoNotGuardDictionaryRemoveByContainsKeyMessage">
<source>Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'</source>
<target state="new">Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'</target>
<note />
</trans-unit>
<trans-unit id="DoNotGuardDictionaryRemoveByContainsKeyTitle">
<source>Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'</source>
<target state="new">Do not guard 'Dictionary.Remove(key)' with 'Dictionary.ContainsKey(key)'</target>
<note />
</trans-unit>
<trans-unit id="DoNotHardCodeCertificate">
<source>Do not hard-code certificate</source>
<target state="translated">Nepoužívejte pevně zakódovaný certifikát</target>
Expand Down Expand Up @@ -2082,6 +2097,11 @@
<target state="translated">Odebrat redundantní volání</target>
<note />
</trans-unit>
<trans-unit id="RemoveRedundantGuardCallCodeFixTitle">
<source>Remove redundant guard call</source>
<target state="new">Remove redundant guard call</target>
<note />
</trans-unit>
<trans-unit id="ReplaceStringLiteralWithCharLiteralCodeActionTitle">
<source>Replace string literal with char literal</source>
<target state="translated">Nahradit řetězcový literál s literálem char</target>
Expand Down
Loading