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(); + } +}