diff --git a/Directory.Build.props b/Directory.Build.props
index 19cafe4b..136129b3 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -38,7 +38,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/README.md b/README.md
index 059ac756..b84c2381 100755
--- a/README.md
+++ b/README.md
@@ -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\|⚠️|❌|❌|
|[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|ℹ️|✔️|❌|
diff --git a/docs/README.md b/docs/README.md
index bed03975..23b4a1da 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -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\|⚠️|❌|❌|
|[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|ℹ️|✔️|❌|
|Id|Suppressed rule|Justification|
|--|---------------|-------------|
@@ -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
@@ -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
```
diff --git a/docs/Rules/MA0160.md b/docs/Rules/MA0160.md
new file mode 100644
index 00000000..1e53353f
--- /dev/null
+++ b/docs/Rules/MA0160.md
@@ -0,0 +1,9 @@
+# MA0160 - Use ContainsKey instead of TryGetValue
+
+````c#
+Dictionary dict;
+dict.TryGetValue("dummy", out _); // non-compliant
+
+dict.TryGetValue("dummy", out var a); // ok
+dict.ContainsKey("dummy"); // ok
+````
diff --git a/global.json b/global.json
index 34b2b9f0..91a2345c 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.302",
+ "version": "8.0.303",
"rollForward": "latestMajor"
}
}
\ No newline at end of file
diff --git a/src/ListDotNetTypes/ListDotNetTypes.csproj b/src/ListDotNetTypes/ListDotNetTypes.csproj
index 8ed799a6..387f0dc3 100644
--- a/src/ListDotNetTypes/ListDotNetTypes.csproj
+++ b/src/ListDotNetTypes/ListDotNetTypes.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index c45170b6..2a70c6e5 100755
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -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)
{
diff --git a/src/Meziantou.Analyzer/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzer.cs
new file mode 100644
index 00000000..2b3606d1
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzer.cs
@@ -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 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)[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;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj b/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj
index 75638da6..8b305430 100644
--- a/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj
+++ b/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj
@@ -12,15 +12,15 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzerTests.cs
new file mode 100644
index 00000000..e243ea97
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzerTests.cs
@@ -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();
+ }
+
+ [Fact]
+ public async Task IDictionary_TryGetValue_Value()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class ClassTest
+ {
+ void Test(System.Collections.Generic.IDictionary 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 dict)
+ {
+ [||]dict.TryGetValue("", out _);
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task IReadOnlyDictionary_TryGetValue_Discard()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class ClassTest
+ {
+ void Test(System.Collections.Generic.IReadOnlyDictionary dict)
+ {
+ [||]dict.TryGetValue("", out _);
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Dictionary_TryGetValue_Discard()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class ClassTest
+ {
+ void Test(System.Collections.Generic.Dictionary 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
+ {
+ }
+ """)
+ .ValidateAsync();
+ }
+}