Skip to content

Commit

Permalink
New rule: MA0160 Use ContainsKey instead of TryGetValue when the seco…
Browse files Browse the repository at this point in the history
…nd arg is a discard (#743)
  • Loading branch information
meziantou authored Jul 16, 2024
1 parent 7eede0e commit 359a6a4
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Polyfill" Version="1.0.38">
<PackageReference Include="Meziantou.Polyfill" Version="1.0.39">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0157](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0157.md)|Design|Do not use 'Async' suffix when a method does not return IAsyncEnumerable\<T\>|⚠️|||
|[MA0158](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0158.md)|Performance|Use System.Threading.Lock|⚠️|✔️||
|[MA0159](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0159.md)|Performance|Use 'Order' instead of 'OrderBy'|ℹ️|✔️|✔️|
|[MA0160](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0160.md)|Performance|Use ContainsKey instead of TryGetValue|ℹ️|✔️||

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
|[MA0157](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0157.md)|Design|Do not use 'Async' suffix when a method does not return IAsyncEnumerable\<T\>|<span title='Warning'>⚠️</span>|||
|[MA0158](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0158.md)|Performance|Use System.Threading.Lock|<span title='Warning'>⚠️</span>|✔️||
|[MA0159](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0159.md)|Performance|Use 'Order' instead of 'OrderBy'|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0160](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0160.md)|Performance|Use ContainsKey instead of TryGetValue|<span title='Info'>ℹ️</span>|✔️||

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -642,6 +643,9 @@ dotnet_diagnostic.MA0158.severity = warning
# MA0159: Use 'Order' instead of 'OrderBy'
dotnet_diagnostic.MA0159.severity = suggestion
# MA0160: Use ContainsKey instead of TryGetValue
dotnet_diagnostic.MA0160.severity = suggestion
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1120,4 +1124,7 @@ dotnet_diagnostic.MA0158.severity = none
# MA0159: Use 'Order' instead of 'OrderBy'
dotnet_diagnostic.MA0159.severity = none
# MA0160: Use ContainsKey instead of TryGetValue
dotnet_diagnostic.MA0160.severity = none
```
9 changes: 9 additions & 0 deletions docs/Rules/MA0160.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# MA0160 - Use ContainsKey instead of TryGetValue

````c#
Dictionary<string, string> dict;
dict.TryGetValue("dummy", out _); // non-compliant
dict.TryGetValue("dummy", out var a); // ok
dict.ContainsKey("dummy"); // ok
````
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.302",
"version": "8.0.303",
"rollForward": "latestMajor"
}
}
2 changes: 1 addition & 1 deletion src/ListDotNetTypes/ListDotNetTypes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NuGet.Protocol" Version="6.10.0" />
<PackageReference Include="NuGet.Protocol" Version="6.10.1" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ internal static class RuleIdentifiers
public const string MethodsNotReturningIAsyncEnumerableMustNotHaveTheAsyncSuffix = "MA0157";
public const string UseSystemThreadingLockInsteadOfObject = "MA0158";
public const string OptimizeEnumerable_UseOrder = "MA0159";
public const string UseContainsKeyInsteadOfTryGetValue = "MA0160";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseContainsKeyInsteadOfTryGetValueAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.UseContainsKeyInsteadOfTryGetValue,
title: "Use ContainsKey instead of TryGetValue",
messageFormat: "Use ContainsKey instead of TryGetValue",
RuleCategories.Performance,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseContainsKeyInsteadOfTryGetValue));

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

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.RegisterCompilationStartAction(ctx =>
{
var analyzerContext = new AnalyzerContext(ctx.Compilation);
ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
});
}

private sealed class AnalyzerContext(Compilation compilation)
{
private INamedTypeSymbol? IReadOnlyDictionary { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IReadOnlyDictionary`2");
private INamedTypeSymbol? IDictionary { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IDictionary`2");

public void AnalyzeInvocation(OperationAnalysisContext context)
{
var operation = (IInvocationOperation)context.Operation;

if (operation is { TargetMethod: { Name: "TryGetValue", Parameters.Length: 2, ContainingType: not null }, Arguments: [_, { Value: IDiscardOperation }] })
{
foreach (var symbol in (ReadOnlySpan<INamedTypeSymbol?>)[IReadOnlyDictionary, IDictionary])
{
if (symbol is not null)
{
var iface = operation.TargetMethod.ContainingType.OriginalDefinition.IsEqualTo(symbol) ? operation.TargetMethod.ContainingType : operation.TargetMethod.ContainingType.AllInterfaces.FirstOrDefault(i => SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, symbol));
if (iface is not null)
{
if (iface.GetMembers("TryGetValue").FirstOrDefault() is IMethodSymbol member)
{
var implementation = operation.TargetMethod.IsEqualTo(member) ? member : operation.TargetMethod.ContainingType.FindImplementationForInterfaceMember(member);
if (SymbolEqualityComparer.Default.Equals(operation.TargetMethod, implementation))
{
context.ReportDiagnostic(Rule, operation);
return;
}
}
}
}
}
}
}
}
}
8 changes: 4 additions & 4 deletions tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3">
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="System.Reflection.Metadata" Version="8.0.0" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Threading.Tasks;
using Meziantou.Analyzer.Rules;
using TestHelper;
using Xunit;

namespace Meziantou.Analyzer.Test.Rules;
public sealed class UseContainsKeyInsteadOfTryGetValueAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<UseContainsKeyInsteadOfTryGetValueAnalyzer>();
}

[Fact]
public async Task IDictionary_TryGetValue_Value()
{
await CreateProjectBuilder()
.WithSourceCode("""
class ClassTest
{
void Test(System.Collections.Generic.IDictionary<string, string> dict)
{
dict.TryGetValue("", out var a);
}
}
""")
.ValidateAsync();
}

[Fact]
public async Task IDictionary_TryGetValue_Discard()
{
await CreateProjectBuilder()
.WithSourceCode("""
class ClassTest
{
void Test(System.Collections.Generic.IDictionary<string, string> dict)
{
[||]dict.TryGetValue("", out _);
}
}
""")
.ValidateAsync();
}

[Fact]
public async Task IReadOnlyDictionary_TryGetValue_Discard()
{
await CreateProjectBuilder()
.WithSourceCode("""
class ClassTest
{
void Test(System.Collections.Generic.IReadOnlyDictionary<string, string> dict)
{
[||]dict.TryGetValue("", out _);
}
}
""")
.ValidateAsync();
}

[Fact]
public async Task Dictionary_TryGetValue_Discard()
{
await CreateProjectBuilder()
.WithSourceCode("""
class ClassTest
{
void Test(System.Collections.Generic.Dictionary<string, string> dict)
{
[||]dict.TryGetValue("", out _);
}
}
""")
.ValidateAsync();
}

[Fact]
public async Task CustomDictionary_TryGetValue_Discard()
{
await CreateProjectBuilder()
.WithSourceCode("""
class ClassTest
{
void Test(SampleDictionary dict)
{
[||]dict.TryGetValue("", out _);
}
}

class SampleDictionary : System.Collections.Generic.Dictionary<string, string>
{
}
""")
.ValidateAsync();
}
}

0 comments on commit 359a6a4

Please sign in to comment.