From 9e4611882690548ccd462ff9aeaa5aa5a94905a3 Mon Sep 17 00:00:00 2001 From: David Boike Date: Tue, 12 Mar 2024 14:37:48 -0500 Subject: [PATCH] New analyzers: Dictionary keys & async void + NUnit (#278) * Start of dictionary key analyzer * Tweaks and fixes * C# 12 * Enable nullable * Types test and ability to better customize compilation on tests * ISet is in a different assembly in net4x * Switch to NUnit to match every other repo * And then add NUnit analyzers and update to 4.0.1 * Change title to be more exact about what we're analyzing * Fix duplicated diagnostic id by moving StructuredLoggingWithRepeatedToken to 26 since Core already has a PR with pragmas for PS0025 * Add async void analyzer * Tweaks * Extended the message a bit * Update src/Particular.Analyzers/DiagnosticDescriptors.cs Co-authored-by: Brandon Ording --------- Co-authored-by: Brandon Ording --- .../AsyncVoidAnalyzerTests.cs | 57 +++++ .../CancellationTryCatchAnalyzerTests.cs | 47 ++-- .../ContextMethodParameterAnalyzerTests.cs | 65 +++-- .../DelegateParametersAnalyzerTests.cs | 13 +- .../Cancellation/EmptyTokenAnalyzerTests.cs | 13 +- .../MethodFuncParameterAnalyzerTests.cs | 53 ++-- .../MethodParametersAnalyzerTests.cs | 51 ++-- .../MethodTokenNamesAnalyzerTests.cs | 61 +++-- .../NonPrivateMethodTokenNameAnalyzerTests.cs | 61 +++-- .../TaskReturningMethodAnalyzerTests.cs | 47 ++-- .../TokenAccessibilityAnalyzerTests.cs | 58 ++--- .../DateTimeImplicitCastAnalyzerTests.cs | 23 +- .../DateTimeNowAnalyzerTests.cs | 7 +- .../DictionaryKeyAnalyzerTests.cs | 127 ++++++++++ .../DroppedTaskAnalyzerTests.cs | 13 +- .../Helpers/AnalyzerTestFixture.cs | 59 ++--- .../Helpers/TestCustomizations.cs | 35 +++ .../LoggingAnalyzerTests.cs | 45 ++-- .../Particular.Analyzers.Tests.csproj | 9 +- .../TypeNameAnalyzerTests.cs | 21 +- src/Particular.Analyzers/AsyncVoidAnalyzer.cs | 45 ++++ .../CancellationTryCatchAnalyzer.cs | 34 ++- .../ContextMethodParameterAnalyzer.cs | 9 +- .../DelegateParametersAnalyzer.cs | 7 +- .../Cancellation/EmptyTokenAnalyzer.cs | 2 +- .../MethodFuncParameterAnalyzer.cs | 6 +- .../Cancellation/MethodParametersAnalyzer.cs | 6 +- .../Cancellation/MethodTokenNamesAnalyzer.cs | 8 +- .../NonPrivateMethodTokenNameAnalyzer.cs | 6 +- .../TaskReturningMethodAnalyzer.cs | 6 +- .../TokenAccessibilityAnalyzer.cs | 4 +- .../TokenParamUnusedSuppressor.cs | 11 +- .../DateTimeImplicitCastAnalyzer.cs | 32 +-- .../DateTimeNowAnalyzer.cs | 6 +- .../DiagnosticDescriptors.cs | 80 +++--- src/Particular.Analyzers/DiagnosticIds.cs | 4 +- .../DictionaryKeysAnalyzer.cs | 233 ++++++++++++++++++ .../DroppedTaskAnalyzer.cs | 11 +- .../Extensions/MethodSymbolExtensions.cs | 20 +- .../Extensions/SemanticModelExtensions.cs | 10 +- .../Extensions/SymbolExtensions.cs | 24 +- .../Extensions/TypeSymbolExtensions.cs | 12 +- src/Particular.Analyzers/LoggingAnalyzer.cs | 16 +- .../Particular.Analyzers.csproj | 6 +- src/Particular.Analyzers/TypeNameAnalyzer.cs | 2 +- 45 files changed, 971 insertions(+), 494 deletions(-) create mode 100644 src/Particular.Analyzers.Tests/AsyncVoidAnalyzerTests.cs create mode 100644 src/Particular.Analyzers.Tests/DictionaryKeyAnalyzerTests.cs create mode 100644 src/Particular.Analyzers.Tests/Helpers/TestCustomizations.cs create mode 100644 src/Particular.Analyzers/AsyncVoidAnalyzer.cs create mode 100644 src/Particular.Analyzers/DictionaryKeysAnalyzer.cs diff --git a/src/Particular.Analyzers.Tests/AsyncVoidAnalyzerTests.cs b/src/Particular.Analyzers.Tests/AsyncVoidAnalyzerTests.cs new file mode 100644 index 0000000..1e2b27a --- /dev/null +++ b/src/Particular.Analyzers.Tests/AsyncVoidAnalyzerTests.cs @@ -0,0 +1,57 @@ +namespace Particular.Analyzers.Tests +{ + using System.Threading.Tasks; + using NUnit.Framework; + using Particular.Analyzers.Tests.Helpers; + + public class AsyncVoidAnalyzerTests : AnalyzerTestFixture + { + [Test] + public Task NoAsyncVoid() + { + var code = """ + public class Foo + { + async void [|Bad1|]() { await Task.Delay(1); } + private async void [|Bad2|]() { await Task.Delay(1); } + public async void [|Bad3|]() { await Task.Delay(1); } + protected async void [|Bad4|]() { await Task.Delay(1); } + static async void [|Bad5|]() { await Task.Delay(1); } + static private async void [|Bad6|]() { await Task.Delay(1); } + static public async void [|Bad7|]() { await Task.Delay(1); } + static protected async void [|Bad8|]() { await Task.Delay(1); } + async void [|Bad9|](string a, int b) { await Task.Delay(1); } + + async Task ReturnsTask1() { await Task.Delay(1); } + private async Task ReturnsTask2() { await Task.Delay(1); } + public async Task ReturnsTask3() { await Task.Delay(1); } + protected async Task ReturnsTask4() { await Task.Delay(1); } + static async Task ReturnsTask5() { await Task.Delay(1); } + static private async Task ReturnsTask6() { await Task.Delay(1); } + static public async Task ReturnsTask7() { await Task.Delay(1); } + static protected async Task ReturnsTask8() { await Task.Delay(1); } + async Task ReturnsTask9(string a, int b) { await Task.Delay(1); } + + async Task ReturnsValueTask1() { await Task.Delay(1); } + private async Task ReturnsValueTask2() { await Task.Delay(1); } + public async Task ReturnsValueTask3() { await Task.Delay(1); } + protected async Task ReturnsValueTask4() { await Task.Delay(1); } + static async Task ReturnsValueTask5() { await Task.Delay(1); } + static private async Task ReturnsValueTask6() { await Task.Delay(1); } + static public async Task ReturnsValueTask7() { await Task.Delay(1); } + static protected async Task ReturnsValueTask8() { await Task.Delay(1); } + async Task ReturnsValueTask9(string a, int b) { await Task.Delay(1); } + + public async Task RegularMethod() + { + async void [|Local|]() => await Task.Delay(1); + + await Task.Delay(1); + } + } + """; + + return Assert(code, DiagnosticIds.AsyncVoid); + } + } +} diff --git a/src/Particular.Analyzers.Tests/Cancellation/CancellationTryCatchAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/CancellationTryCatchAnalyzerTests.cs index 1178d28..16fb948 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/CancellationTryCatchAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/CancellationTryCatchAnalyzerTests.cs @@ -2,16 +2,13 @@ { using System.Linq; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class CancellationTryCatchAnalyzerTests : AnalyzerTestFixture { - public CancellationTryCatchAnalyzerTests(ITestOutputHelper output) : base(output) { } - public static readonly Data AllCatchBlocks; public static readonly Data CatchBlocksWithoutIdentifiers; @@ -65,8 +62,8 @@ static CancellationTryCatchAnalyzerTests() CatchBlocksWithoutIdentifiers = catchBlocksWithoutIdentifiers.ToData(); } - [Theory] - [MemberData(nameof(AllCatchBlocks))] + [Test] + [TestCaseSource(nameof(AllCatchBlocks))] public Task PassingSimpleToken(string expectedDiagnostic, string catchBlocks) { const string PassesSimpleTokenTemplate = @@ -88,8 +85,8 @@ public async Task Bar(CancellationToken cancellationToken) return Assert(GetCode(PassesSimpleTokenTemplate, catchBlocks, "cancellationToken"), expectedDiagnostic); } - [Theory] - [MemberData(nameof(AllCatchBlocks))] + [Test] + [TestCaseSource(nameof(AllCatchBlocks))] public Task PassingTokenProperty(string expectedDiagnostic, string catchBlocks) { const string PassesTokenPropertyTemplate = @@ -115,8 +112,8 @@ public class SomeContext return Assert(GetCode(PassesTokenPropertyTemplate, catchBlocks, "context.Token"), expectedDiagnostic); } - [Theory] - [MemberData(nameof(CatchBlocksWithoutIdentifiers))] + [Test] + [TestCaseSource(nameof(CatchBlocksWithoutIdentifiers))] public Task MethodThatGeneratesToken(string expectedDiagnostic, string catchBlocks) { const string PassesTokenPropertyTemplate = @@ -139,8 +136,8 @@ public async Task Bar() return Assert(GetCode(PassesTokenPropertyTemplate, catchBlocks, null), expectedDiagnostic); } - [Theory] - [MemberData(nameof(CatchBlocksWithoutIdentifiers))] + [Test] + [TestCaseSource(nameof(CatchBlocksWithoutIdentifiers))] public Task FuncThatGeneratesToken(string expectedDiagnostic, string catchBlocks) { const string PassesTokenPropertyTemplate = @@ -164,8 +161,8 @@ public async Task Bar() return Assert(GetCode(PassesTokenPropertyTemplate, catchBlocks, null), expectedDiagnostic); } - [Theory] - [MemberData(nameof(AllCatchBlocks))] + [Test] + [TestCaseSource(nameof(AllCatchBlocks))] public Task ThrowIfCancellationRequested(string expectedDiagnostic, string catchBlocks) { const string PassesSimpleTokenTemplate = @@ -187,8 +184,8 @@ public async Task Bar(CancellationToken cancellationToken) return Assert(GetCode(PassesSimpleTokenTemplate, catchBlocks, "cancellationToken"), expectedDiagnostic); } - [Theory] - [MemberData(nameof(AllCatchBlocks))] + [Test] + [TestCaseSource(nameof(AllCatchBlocks))] public Task PassesCancellableContext(string expectedDiagnostic, string catchBlocks) { const string PassesCancellableContextTemplate = @@ -215,8 +212,8 @@ class SomeContext : ICancellableContext { public CancellationToken CancellationT return Assert(GetCode(PassesCancellableContextTemplate, catchBlocks, "context.CancellationToken"), expectedDiagnostic); } - [Theory] - [MemberData(nameof(AllCatchBlocks))] + [Test] + [TestCaseSource(nameof(AllCatchBlocks))] public Task PassesTokenFromTokenSource(string expectedDiagnostic, string catchBlocks) { const string PassesCancellableContextTemplate = @@ -242,8 +239,8 @@ public async Task Bar() return Assert(GetCode(PassesCancellableContextTemplate, catchBlocks, "tokenSource.Token"), expectedDiagnostic); } - [Theory] - [MemberData(nameof(CatchBlocksWithoutIdentifiers))] + [Test] + [TestCaseSource(nameof(CatchBlocksWithoutIdentifiers))] public Task PassesNoTokenOrEmptyToken(string expectedDiagnostic, string catchBlocks) { const string PassesNoTokenOrEmptyTokenTemplate = @@ -274,7 +271,7 @@ async Task Bar(CancellationToken token) return Assert(GetCode(PassesNoTokenOrEmptyTokenTemplate, noDiagnosticCatchBlocks, null), expectedDiagnostic); } - [Fact] + [Test] public Task NoWarnWhenSingleTokenUsedMultipleTimesInTry() { const string UsesSameTokenMultipleTimes = @@ -309,10 +306,10 @@ async Task Bar(CancellationToken token) return Assert(UsesSameTokenMultipleTimes); } - [Theory] - [InlineData("newToken")] - [InlineData("tokenSource.Token")] - [InlineData("context.Token")] + [Test] + [TestCase("newToken")] + [TestCase("tokenSource.Token")] + [TestCase("context.Token")] public Task WarnWhenMultipleTokenUsedInTry(string tokenExpression) { const string UsesMultipleTokenTemplate = diff --git a/src/Particular.Analyzers.Tests/Cancellation/ContextMethodParameterAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/ContextMethodParameterAnalyzerTests.cs index a0cf04f..2838085 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/ContextMethodParameterAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/ContextMethodParameterAnalyzerTests.cs @@ -2,10 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class ContextMethodParameterAnalyzerTests : AnalyzerTestFixture @@ -71,8 +70,6 @@ void IMyInterface.MyMethod({0}) {{ }} static void MyMethod({0}) {{ }} }}"; - public ContextMethodParameterAnalyzerTests(ITestOutputHelper output) : base(output) { } - public static readonly Data SadParams = new List { "CancellationToken [|foo|]", @@ -85,62 +82,62 @@ public ContextMethodParameterAnalyzerTests(ITestOutputHelper output) : base(outp "object foo", }.ToData(); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadClasses(string @params) => Assert(GetCode(@class, @params), DiagnosticIds.CancellableContextMethodCancellationToken); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyClasses(string @params) => Assert(GetCode(@class, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadInterfaces(string @params) => Assert(GetCode(@interface, @params), DiagnosticIds.CancellableContextMethodCancellationToken); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyInterfaces(string @params) => Assert(GetCode(@interface, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadRecords(string @params) => Assert(GetCode(@record, @params), DiagnosticIds.CancellableContextMethodCancellationToken); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyRecords(string @params) => Assert(GetCode(@record, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadStructs(string @params) => Assert(GetCode(@struct, @params), DiagnosticIds.CancellableContextMethodCancellationToken); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyStructs(string @params) => Assert(GetCode(@struct, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadOverrides(string parameters) => Assert(GetCode(@override, parameters), DiagnosticIds.CancellableContextMethodCancellationToken); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyOverrides(string parameters) => Assert(GetCode(@override, parameters)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadExplicits(string parameters) => Assert(GetCode(@explicit, parameters), DiagnosticIds.CancellableContextMethodCancellationToken); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyExplicits(string parameters) => Assert(GetCode(@explicit, parameters)); - [Theory] - [MemberData(nameof(SadParams))] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(SadParams))] + [TestCaseSource(nameof(HappyParams))] public Task HappyConstructors(string @params) => Assert(GetCode(constructor, @params)); - [Theory] - [MemberData(nameof(SadParams))] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(SadParams))] + [TestCaseSource(nameof(HappyParams))] public Task HappyStatics(string @params) => Assert(GetCode(@static, @params)); static string GetCode(string template, string @params) => string.Format(template, @params); diff --git a/src/Particular.Analyzers.Tests/Cancellation/DelegateParametersAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/DelegateParametersAnalyzerTests.cs index 64186f9..66a939d 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/DelegateParametersAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/DelegateParametersAnalyzerTests.cs @@ -2,10 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class DelegateParametersAnalyzerTests : AnalyzerTestFixture @@ -16,8 +15,6 @@ public class DelegateParametersAnalyzerTests : AnalyzerTestFixture { "CancellationToken [|cancellationToken|], object foo", @@ -36,12 +33,12 @@ public DelegateParametersAnalyzerTests(ITestOutputHelper output) : base(output) "CancellationToken cancellationToken1, CancellationToken cancellationToken2, params object[] foos", }.ToData(); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task Sad(string @params) => Assert(GetCode(@params), DiagnosticIds.DelegateCancellationTokenMisplaced); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task Happy(string @params) => Assert(GetCode(@params)); static string GetCode(string @params) => string.Format(@delegate, @params); diff --git a/src/Particular.Analyzers.Tests/Cancellation/EmptyTokenAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/EmptyTokenAnalyzerTests.cs index 8d556dc..898e98e 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/EmptyTokenAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/EmptyTokenAnalyzerTests.cs @@ -2,10 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class EmptyTokenAnalyzerTests : AnalyzerTestFixture @@ -16,8 +15,6 @@ public class EmptyTokenAnalyzerTests : AnalyzerTestFixture void MyMethod(CancellationToken cancellationToken) => Task.Delay(1, [|{0}|]); }}"; - public EmptyTokenAnalyzerTests(ITestOutputHelper output) : base(output) { } - public static readonly Data SadArgs = new List<(string, string)> { ("default", DiagnosticIds.EmptyCancellationTokenDefaultLiteral), @@ -26,12 +23,12 @@ public EmptyTokenAnalyzerTests(ITestOutputHelper output) : base(output) { } public static readonly Data HappyArgs = new List { "cancellationToken", "new CancellationToken()", "CancellationToken.None" }.ToData(); - [Theory] - [MemberData(nameof(SadArgs))] + [Test] + [TestCaseSource(nameof(SadArgs))] public Task Sad(string arg, string diagnosticId) => Assert(GetCode(arg), diagnosticId); - [Theory] - [MemberData(nameof(HappyArgs))] + [Test] + [TestCaseSource(nameof(HappyArgs))] public Task Happy(string arg) => Assert(GetCode(arg)); static string GetCode(string arg) => string.Format(invoke, arg); diff --git a/src/Particular.Analyzers.Tests/Cancellation/MethodFuncParameterAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/MethodFuncParameterAnalyzerTests.cs index bcee594..1500865 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/MethodFuncParameterAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/MethodFuncParameterAnalyzerTests.cs @@ -2,10 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class MethodFuncParameterAnalyzerTests : AnalyzerTestFixture @@ -54,8 +53,6 @@ class MyClass : IMyInterface where T : CancellableContext void IMyInterface.MyMethod({0} foo) {{ }} }}"; - public MethodFuncParameterAnalyzerTests(ITestOutputHelper output) : base(output) { } - public static readonly Data SadNames = new List<(string, string[])> { ("Func", new[] { DiagnosticIds.MethodFuncParameterMixedCancellation }), @@ -86,7 +83,7 @@ public MethodFuncParameterAnalyzerTests(ITestOutputHelper output) : base(output) #endif }.ToData(); - public static readonly Data HappyNames = new List + public static readonly Data HappyNames = new string[] { "object", "Action", @@ -106,39 +103,45 @@ public MethodFuncParameterAnalyzerTests(ITestOutputHelper output) : base(output) "Func", }.ToData(); - [Theory] - [MemberData(nameof(SadNames))] + [Test] + [TestCaseSource(nameof(SadNames))] public Task SadMethods(string name, params string[] diagnosticIds) => Assert(GetCode(method, name), diagnosticIds); - [Theory] - [MemberData(nameof(HappyNames))] + [Test] + [TestCaseSource(nameof(HappyNames))] public Task HappyMethods(string name) => Assert(GetCode(method, name)); - [Theory] - [MemberData(nameof(SadNames))] + [Test] + [TestCaseSource(nameof(SadNames))] public Task SadConstructors(string name, params string[] diagnosticIds) => Assert(GetCode(constructor, name), diagnosticIds); - [Theory] - [MemberData(nameof(HappyNames))] + [Test] + [TestCaseSource(nameof(HappyNames))] public Task HappyConstructors(string name) => Assert(GetCode(constructor, name)); - [Theory] - [MemberData(nameof(SadNames))] + [Test] + [TestCaseSource(nameof(SadNames))] public Task SadDelegates(string name, params string[] diagnosticIds) => Assert(GetCode(@delegate, name), diagnosticIds); - [Theory] - [MemberData(nameof(HappyNames))] + [Test] + [TestCaseSource(nameof(HappyNames))] public Task HappyDelegates(string name) => Assert(GetCode(@delegate, name)); - [Theory] - [MemberData(nameof(SadNames))] - [MemberData(nameof(HappyNames))] - public Task HappyOverrides(string name, params string[] diagnosticIds) => Assert(GetCode(@override, name), diagnosticIds); + [Test] + [TestCaseSource(nameof(HappyNames))] + public Task HappyOverrides(string name) => Assert(GetCode(@override, name)); + + [Test] + [TestCaseSource(nameof(SadNames))] + public Task SadOverrides(string name, params string[] diagnosticIds) => Assert(GetCode(@override, name), diagnosticIds); + + [Test] + [TestCaseSource(nameof(HappyNames))] + public Task HappyExplicits(string name) => Assert(GetCode(@explicit, name)); - [Theory] - [MemberData(nameof(SadNames))] - [MemberData(nameof(HappyNames))] - public Task HappyExplicits(string name, params string[] diagnosticIds) => Assert(GetCode(@explicit, name), diagnosticIds); + [Test] + [TestCaseSource(nameof(SadNames))] + public Task SadExplicits(string name, params string[] diagnosticIds) => Assert(GetCode(@explicit, name), diagnosticIds); static string GetCode(string template, string name) => string.Format(template, name); } diff --git a/src/Particular.Analyzers.Tests/Cancellation/MethodParametersAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/MethodParametersAnalyzerTests.cs index 8e40ced..8dc9819 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/MethodParametersAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/MethodParametersAnalyzerTests.cs @@ -2,10 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class MethodParametersAnalyzerTests : AnalyzerTestFixture @@ -54,8 +53,6 @@ class MyClass : IMyInterface where T : CancellableContext void IMyInterface.MyMethod({0}) {{ }} }}"; - public MethodParametersAnalyzerTests(ITestOutputHelper output) : base(output) { } - public static readonly Data SadParams = new List<(string, string[])> { ("ICancellableContext foo, CancellationToken cancellationToken", new[] { DiagnosticIds.MethodMixedCancellation }), @@ -92,39 +89,45 @@ public MethodParametersAnalyzerTests(ITestOutputHelper output) : base(output) { "T foo, object bar", }.ToData(); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadMethods(string @params, params string[] diagnosticIds) => Assert(GetCode(method, @params), diagnosticIds); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyMethods(string @params) => Assert(GetCode(method, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadConstructors(string @params, params string[] diagnosticIds) => Assert(GetCode(constructor, @params), diagnosticIds); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyConstructors(string @params) => Assert(GetCode(constructor, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadDelegates(string @params, params string[] diagnosticIds) => Assert(GetCode(@delegate, @params), diagnosticIds); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyDelegates(string @params) => Assert(GetCode(@delegate, @params)); - [Theory] - [MemberData(nameof(SadParams))] - [MemberData(nameof(HappyParams))] - public Task HappyOverrides(string @params, params string[] diagnosticIds) => Assert(GetCode(@override, @params), diagnosticIds); + [Test] + [TestCaseSource(nameof(HappyParams))] + public Task HappyOverrides(string @params) => Assert(GetCode(@override, @params)); + + [Test] + [TestCaseSource(nameof(SadParams))] + public Task SadOverrides(string @params, params string[] diagnosticIds) => Assert(GetCode(@override, @params), diagnosticIds); + + [Test] + [TestCaseSource(nameof(HappyParams))] + public Task HappyExplicits(string @params) => Assert(GetCode(@explicit, @params)); - [Theory] - [MemberData(nameof(SadParams))] - [MemberData(nameof(HappyParams))] - public Task HappyExplicits(string @params, params string[] diagnosticIds) => Assert(GetCode(@explicit, @params), diagnosticIds); + [Test] + [TestCaseSource(nameof(SadParams))] + public Task SadExplicits(string @params, params string[] diagnosticIds) => Assert(GetCode(@explicit, @params), diagnosticIds); static string GetCode(string template, string @params) => string.Format(template, @params); } diff --git a/src/Particular.Analyzers.Tests/Cancellation/MethodTokenNamesAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/MethodTokenNamesAnalyzerTests.cs index 186dcf6..389ecfa 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/MethodTokenNamesAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/MethodTokenNamesAnalyzerTests.cs @@ -3,10 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class MethodTokenNamesAnalyzerTests : AnalyzerTestFixture @@ -65,8 +64,6 @@ void MyMethod({0}) {{ }} }}"; #endif - public MethodTokenNamesAnalyzerTests(ITestOutputHelper output) : base(output) { } - public static readonly Data SadPublicAndPrivateParams = new List { "CancellationToken [|foo|], CancellationToken [|bar|]", @@ -93,61 +90,61 @@ public MethodTokenNamesAnalyzerTests(ITestOutputHelper output) : base(output) { "object foo, CancellationToken fooCancellationToken, CancellationToken barCancellationToken", }.ToData(); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadMethods(string @params) => Assert(GetCode(method, @params), DiagnosticIds.MethodCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyMethods(string @params) => Assert(GetCode(method, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadConstructors(string @params) => Assert(GetCode(constructor, @params), DiagnosticIds.MethodCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyConstructors(string @params) => Assert(GetCode(constructor, @params)); - [Theory] - [MemberData(nameof(SadPublicAndPrivateParams))] + [Test] + [TestCaseSource(nameof(SadPublicAndPrivateParams))] public Task SadOverrides(string @params) => Assert(GetCode(@override, @params), DiagnosticIds.MethodCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyOverrides(string @params) => Assert(GetCode(@override, @params)); - [Theory] - [MemberData(nameof(SadPublicAndPrivateParams))] + [Test] + [TestCaseSource(nameof(SadPublicAndPrivateParams))] public Task SadExplicits(string @params) => Assert(GetCode(@explicit, @params), DiagnosticIds.MethodCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyExplicits(string @params) => Assert(GetCode(@explicit, @params)); - [Theory] - [MemberData(nameof(SadParams))] + [Test] + [TestCaseSource(nameof(SadParams))] public Task SadDelegates(string @params) => Assert(GetCode(@delegate, @params), DiagnosticIds.MethodCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyDelegates(string @params) => Assert(GetCode(@delegate, @params)); - [Theory] - [MemberData(nameof(SadPublicAndPrivateParams))] + [Test] + [TestCaseSource(nameof(SadPublicAndPrivateParams))] public Task SadInterfaceMethods(string @params) => Assert(GetCode(interfaceMethods, @params), DiagnosticIds.MethodCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyInterfaceMethods(string @params) => Assert(GetCode(interfaceMethods, @params)); #if NET - [Theory] - [MemberData(nameof(SadPublicAndPrivateParams))] + [Test] + [TestCaseSource(nameof(SadPublicAndPrivateParams))] public Task SadInterfaceDefaultMethods(string @params) => Assert(GetCode(interfaceDefaultMethods, @params), DiagnosticIds.MethodCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyParams))] + [Test] + [TestCaseSource(nameof(HappyParams))] public Task HappyInterfaceDefaultMethods(string @params) => Assert(GetCode(interfaceDefaultMethods, @params)); #endif diff --git a/src/Particular.Analyzers.Tests/Cancellation/NonPrivateMethodTokenNameAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/NonPrivateMethodTokenNameAnalyzerTests.cs index 10b3d56..89e38e6 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/NonPrivateMethodTokenNameAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/NonPrivateMethodTokenNameAnalyzerTests.cs @@ -3,10 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class NonPrivateMethodTokenNameAnalyzerTests : AnalyzerTestFixture @@ -65,8 +64,6 @@ void IMyInterface.MyMethod({1}) {{ }} }}"; #endif - public NonPrivateMethodTokenNameAnalyzerTests(ITestOutputHelper output) : base(output) { } - static readonly List sadParams = [ "CancellationToken [|token|]", @@ -101,61 +98,61 @@ public NonPrivateMethodTokenNameAnalyzerTests(ITestOutputHelper output) : base(o InterfacePrivateModifiers.SelectMany(modifiers => sadParams.Concat(happyParams).Select(param => (modifiers, param))) .Concat(InterfaceNonPrivateModifiers.SelectMany(modifiers => happyParams.Select(param => (modifiers, param)))).ToData(); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadMethods(string modifiers, string @params) => Assert(GetCode(method, modifiers, @params), DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(HappyData))] public Task HappyMethods(string modifiers, string @params) => Assert(GetCode(method, modifiers, @params)); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadConstructors(string modifiers, string @params) => Assert(GetCode(constructor, modifiers, @params), DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(HappyData))] public Task HappyConstructors(string modifiers, string @params) => Assert(GetCode(constructor, modifiers, @params)); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadOverrides(string modifiers, string @params) => Assert(GetCode(@override, modifiers, @params), DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyOverridesData))] + [Test] + [TestCaseSource(nameof(HappyOverridesData))] public Task HappyOverrides(string modifiers, string @params) => Assert(GetCode(@override, modifiers, @params)); - [Theory] - [MemberData(nameof(SadInterfaceData))] + [Test] + [TestCaseSource(nameof(SadInterfaceData))] public Task SadExplicits(string modifiers, string @params) => Assert(GetCode(@explicit, modifiers, @params), DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyInterfaceMethodData))] + [Test] + [TestCaseSource(nameof(HappyInterfaceMethodData))] public Task HappyExplicits(string modifiers, string @params) => Assert(GetCode(@explicit, modifiers, @params)); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadDelegates(string modifiers, string @params) => Assert(GetCode(@delegate, modifiers, @params), DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(HappyData))] public Task HappyDelegates(string modifiers, string @params) => Assert(GetCode(@delegate, modifiers, @params)); - [Theory] - [MemberData(nameof(SadInterfaceData))] + [Test] + [TestCaseSource(nameof(SadInterfaceData))] public Task SadInterfaceMethods(string modifiers, string @params) => Assert(GetCode(interfaceMethods, modifiers, @params), DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyInterfaceMethodData))] + [Test] + [TestCaseSource(nameof(HappyInterfaceMethodData))] public Task HappyInterfaceMethods(string modifiers, string @params) => Assert(GetCode(interfaceMethods, modifiers, @params)); #if NET - [Theory] - [MemberData(nameof(SadInterfaceData))] + [Test] + [TestCaseSource(nameof(SadInterfaceData))] public Task SadInterfaceDefaultMethods(string modifiers, string @params) => Assert(GetCode(interfaceDefaultMethods, modifiers, @params), DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed); - [Theory] - [MemberData(nameof(HappyInterfaceDefaultMethodData))] + [Test] + [TestCaseSource(nameof(HappyInterfaceDefaultMethodData))] public Task HappyInterfaceDefaultMethods(string modifiers, string @params) => Assert(GetCode(interfaceDefaultMethods, modifiers, @params)); #endif diff --git a/src/Particular.Analyzers.Tests/Cancellation/TaskReturningMethodAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/TaskReturningMethodAnalyzerTests.cs index a26a151..5f85b76 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/TaskReturningMethodAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/TaskReturningMethodAnalyzerTests.cs @@ -5,10 +5,9 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class TaskReturningMethodAnalyzerTests : AnalyzerTestFixture @@ -122,8 +121,6 @@ class Program "T foo, object bar", ]; - public TaskReturningMethodAnalyzerTests(ITestOutputHelper output) : base(output) { } - public static readonly Data TestAttributes = new List { "[NUnit.Framework.OneTimeSetUp]", @@ -145,43 +142,43 @@ public TaskReturningMethodAnalyzerTests(ITestOutputHelper output) : base(output) .Concat(notTaskTypes.SelectMany(type => notTaskParams.Concat(taskParams).Select(@params => (type, @params)))) .ToData(); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadMethods(string returnType, string @params) => Assert(GetCode(method, returnType, @params), DiagnosticIds.TaskReturningMethodNoCancellation); - [Theory] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(HappyData))] public Task HappyMethods(string returnType, string @params) => Assert(GetCode(method, returnType, @params)); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadDelegates(string returnType, string @params) => Assert(GetCode(@delegate, returnType, @params), DiagnosticIds.TaskReturningMethodNoCancellation); - [Theory] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(HappyData))] public Task HappyDelegates(string returnType, string @params) => Assert(GetCode(@delegate, returnType, @params)); - [Theory] - [MemberData(nameof(SadData))] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(SadData))] + [TestCaseSource(nameof(HappyData))] public Task HappyOverrides(string returnType, string @params) => Assert(GetCode(@override, returnType, @params)); - [Theory] - [MemberData(nameof(SadData))] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(SadData))] + [TestCaseSource(nameof(HappyData))] public Task HappyExplicits(string returnType, string @params) => Assert(GetCode(@explicit, returnType, @params)); - [Theory] - [MemberData(nameof(SadData))] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(SadData))] + [TestCaseSource(nameof(HappyData))] public Task HappyContexts(string returnType, string @params) => Assert(GetCode(context, returnType, @params)); - [Theory] - [MemberData(nameof(TestAttributes))] + [Test] + [TestCaseSource(nameof(TestAttributes))] public Task HappyTests(string attribute) => Assert(GetTestCode(attribute, taskTypes.First(), notTaskParams.First())); - [Fact] - public Task HappyEntryPoint() => Assert(entryPoint, new CSharpCompilationOptions(OutputKind.ConsoleApplication)); + [Test] + public Task HappyEntryPoint() => Assert(entryPoint, c => c.CompilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication)); static string GetCode(string template, string returnType, string @params) => string.Format(template, returnType, @params); diff --git a/src/Particular.Analyzers.Tests/Cancellation/TokenAccessibilityAnalyzerTests.cs b/src/Particular.Analyzers.Tests/Cancellation/TokenAccessibilityAnalyzerTests.cs index 3393643..3029eff 100644 --- a/src/Particular.Analyzers.Tests/Cancellation/TokenAccessibilityAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/Cancellation/TokenAccessibilityAnalyzerTests.cs @@ -3,10 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class TokenAccessibilityAnalyzerTests : AnalyzerTestFixture @@ -24,7 +23,7 @@ public class TokenAccessibilityAnalyzerTests : AnalyzerTestFixture PrivateModifiers.SelectMany(modifiers => nonPrivateParams.Select(param => (modifiers, param, DiagnosticIds.CancellationTokenPrivateOptional))) .Concat(NonPrivateModifiers.SelectMany(modifiers => privateParams.Select(param => (modifiers, param, DiagnosticIds.CancellationTokenNonPrivateRequired)))).ToData(); @@ -106,50 +103,53 @@ public TokenAccessibilityAnalyzerTests(ITestOutputHelper output) : base(output) InterfacePrivateModifiers.SelectMany(modifiers => privateParams.Select(param => (modifiers, param))) .Concat(InterfaceNonPrivateModifiers.SelectMany(modifiers => nonPrivateParams.Select(param => (modifiers, param)))).ToData(); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadMethods(string modifiers, string @params, string diagnosticId) => Assert(GetCode(method, modifiers, @params), diagnosticId); - [Theory] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(HappyData))] public Task HappyMethods(string modifiers, string @params) => Assert(GetCode(method, modifiers, @params)); - [Theory] - [MemberData(nameof(SadOverridesData))] + [Test] + [TestCaseSource(nameof(SadOverridesData))] public Task SadOverrides(string modifiers, string @params, string diagnosticId) => Assert(GetCode(@override, modifiers, @params), diagnosticId); - [Theory] - [MemberData(nameof(HappyOverridesData))] + [Test] + [TestCaseSource(nameof(HappyOverridesData))] public Task HappyOverrides(string modifiers, string @params) => Assert(GetCode(@override, modifiers, @params)); - [Theory] - [MemberData(nameof(SadInterfaceMethodData))] - [MemberData(nameof(HappyInterfaceMethodData))] - public Task HappyExplicits(string modifiers, string @params, params string[] diagnosticIds) => Assert(RemoveMarkUp(GetCode(@explicit, modifiers, @params)), diagnosticIds); + [Test] + [TestCaseSource(nameof(HappyInterfaceMethodData))] + public Task HappyExplicits(string modifiers, string @params) => Assert(RemoveMarkUp(GetCode(@explicit, modifiers, @params))); + + [Test] + [TestCaseSource(nameof(SadInterfaceMethodData))] + public Task SadExplicits(string modifiers, string @params, string diagnosticId) => Assert(RemoveMarkUp(GetCode(@explicit, modifiers, @params)), diagnosticId); - [Theory] - [MemberData(nameof(SadData))] + [Test] + [TestCaseSource(nameof(SadData))] public Task SadDelegates(string modifiers, string @params, string diagnosticId) => Assert(GetCode(@delegate, modifiers, @params), diagnosticId); - [Theory] - [MemberData(nameof(HappyData))] + [Test] + [TestCaseSource(nameof(HappyData))] public Task HappyDelegates(string modifiers, string @params) => Assert(GetCode(@delegate, modifiers, @params)); - [Theory] - [MemberData(nameof(SadInterfaceMethodData))] + [Test] + [TestCaseSource(nameof(SadInterfaceMethodData))] public Task SadInterfaceMethods(string modifiers, string @params, string diagnosticId) => Assert(GetCode(interfaceMethods, modifiers, @params), diagnosticId); - [Theory] - [MemberData(nameof(HappyInterfaceMethodData))] + [Test] + [TestCaseSource(nameof(HappyInterfaceMethodData))] public Task HappyInterfaceMethods(string modifiers, string @params) => Assert(GetCode(interfaceMethods, modifiers, @params)); #if NET - [Theory] - [MemberData(nameof(SadInterfaceDefaultMethodData))] + [Test] + [TestCaseSource(nameof(SadInterfaceDefaultMethodData))] public Task SadInterfaceDefaultMethods(string modifiers, string @params, string diagnosticId) => Assert(GetCode(interfaceDefaultMethods, modifiers, @params), diagnosticId); - [Theory] - [MemberData(nameof(HappyInterfaceDefaultMethodData))] + [Test] + [TestCaseSource(nameof(HappyInterfaceDefaultMethodData))] public Task HappyInterfaceDefaultMethods(string modifiers, string @params) => Assert(GetCode(interfaceDefaultMethods, modifiers, @params)); #endif diff --git a/src/Particular.Analyzers.Tests/DateTimeImplicitCastAnalyzerTests.cs b/src/Particular.Analyzers.Tests/DateTimeImplicitCastAnalyzerTests.cs index 2341a80..2779618 100644 --- a/src/Particular.Analyzers.Tests/DateTimeImplicitCastAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/DateTimeImplicitCastAnalyzerTests.cs @@ -1,17 +1,14 @@ namespace Particular.Analyzers.Tests { using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; public class DateTimeImplicitCastAnalyzerTests : AnalyzerTestFixture { - public DateTimeImplicitCastAnalyzerTests(ITestOutputHelper output) : base(output) { } - - [Fact] + [Test] public Task SimpleTest() { const string code = @" @@ -46,7 +43,7 @@ public DateTime GetDateTime() return Assert(code, "PS0022"); } - [Fact] + [Test] public Task Tuples() { const string code = @" @@ -63,7 +60,7 @@ public void Bar() return Assert(code, "PS0022"); } - [Fact] + [Test] public Task Indexer() { const string code = @" @@ -89,7 +86,7 @@ public DateTimeOffset this[int i] return Assert(code, "PS0022"); } - [Fact] + [Test] public Task MethodReturnValue() { const string code = @" @@ -119,7 +116,7 @@ public DateTimeOffset MultipleReturns(bool utc) return Assert(code, "PS0022"); } - [Fact] + [Test] public Task MethodReturnValueAsync() { const string code = @" @@ -156,7 +153,7 @@ public async Task MultipleReturns(bool utc) return Assert(code, "PS0022"); } - [Fact] + [Test] public Task MethodParameter() { const string code = @" @@ -190,7 +187,7 @@ public void Method2(int a, DateTime dt, int b) {} return Assert(code, "PS0022"); } - [Fact] + [Test] public Task DelegateInvocation() { const string code = @" @@ -230,7 +227,7 @@ public void Bar(Func func) return Assert(code, "PS0022"); } - [Fact] + [Test] public Task DontScrewUpOnParamsArrays() { const string code = @" @@ -245,7 +242,7 @@ void Method(params object[] args) {} return Assert(code, "PS0022"); } - [Fact] + [Test] public Task DontFailOnRefAssignments() { // Because FastExpressionCompiler 4.0.0 caused a null-reference exception diff --git a/src/Particular.Analyzers.Tests/DateTimeNowAnalyzerTests.cs b/src/Particular.Analyzers.Tests/DateTimeNowAnalyzerTests.cs index 180ea09..c701ccf 100644 --- a/src/Particular.Analyzers.Tests/DateTimeNowAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/DateTimeNowAnalyzerTests.cs @@ -1,16 +1,13 @@ namespace Particular.Analyzers.Tests { using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Cancellation; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; public class DateTimeNowAnalyzerTests : AnalyzerTestFixture { - public DateTimeNowAnalyzerTests(ITestOutputHelper output) : base(output) { } - - [Fact] + [Test] public Task SimpleTest() { const string code = @" diff --git a/src/Particular.Analyzers.Tests/DictionaryKeyAnalyzerTests.cs b/src/Particular.Analyzers.Tests/DictionaryKeyAnalyzerTests.cs new file mode 100644 index 0000000..c1870c4 --- /dev/null +++ b/src/Particular.Analyzers.Tests/DictionaryKeyAnalyzerTests.cs @@ -0,0 +1,127 @@ +namespace Particular.Analyzers.Tests +{ + using System.Collections.Concurrent; + using System.Collections.Immutable; + using System.Threading.Tasks; + using NUnit.Framework; + using Particular.Analyzers.Tests.Helpers; + + public class DictionaryKeysAnalyzerTests : AnalyzerTestFixture + { + static readonly string template = """ + using System.Collections.Generic; + + public class Foo + { + public DICTIONARY_TYPE Field; + public DICTIONARY_TYPE Property { get; set; } + public DICTIONARY_TYPE[] FieldArray; + public DICTIONARY_TYPE[] PropertyArray { get; set; } + + public DICTIONARY_TYPE ParamAndReturn(DICTIONARY_TYPE source) + { + DICTIONARY_TYPE both = new DICTIONARY_TYPE(); + var asVar = new DICTIONARY_TYPE(); + DICTIONARY_TYPE asType = new(); + DICTIONARY_TYPE justVar; + + return asType; + } + } + + public class BadKey { } + public struct OkStruct { } + public interface IOkBecauseEquatable : IEquatable { } + + public class ClassWithMembers + { + public override bool Equals(object obj) => false; + public override int GetHashCode() => 42; + + // Operators aren't sufficient, but are a "weird" method type that analyzer can't trip over + public static bool operator ==(ClassWithMembers a, ClassWithMembers b) => false; + public static bool operator !=(ClassWithMembers a, ClassWithMembers b) => true; + } + + public class InheritFromDictionary : DICTIONARY_TYPE { } + """; + + [Test] + [TestCase("string")] + [TestCase("int")] + [TestCase("OkStruct")] + [TestCase("ClassWithMembers")] + [TestCase("IOkBecauseEquatable")] + public Task Good(string keyType) + { + var dictionaryType = $"Dictionary<{keyType}, int>"; + var code = template.Replace("DICTIONARY_TYPE", dictionaryType); + return Assert(code, DiagnosticIds.DictionaryHasUnsupportedKeyType); + } + + [Test] + [TestCase("BadKey")] + public Task Bad(string keyType) + { + var dictionaryType = $"Dictionary<{keyType}, int>"; + var code = template.Replace("DICTIONARY_TYPE[]", $"[|{dictionaryType}[]|]") + .Replace("DICTIONARY_TYPE", $"[|{dictionaryType}|]"); + return Assert(code, DiagnosticIds.DictionaryHasUnsupportedKeyType); + } + + [Test] + [TestCase("IDictionary")] + [TestCase("Dictionary")] + [TestCase("ConcurrentDictionary")] + [TestCase("HashSet")] + [TestCase("ISet")] + [TestCase("ImmutableHashSet")] + [TestCase("ImmutableDictionary")] + public Task CheckTypes(string type) + { + var code = $$""" + using System.Collections.Generic; + using System.Collections.Concurrent; + using System.Collections.Immutable; + + public class Foo + { + public [|{{type}}|] Field; + public [|{{type}}|] Property { get; set; } + } + + public class BadKey { } + """; + + return Assert(code, DiagnosticIds.DictionaryHasUnsupportedKeyType, config => + { + config +#if NETFRAMEWORK + .AddMetadataReferenceUsing>() +#endif + .AddMetadataReferenceUsing>() + .AddMetadataReferenceUsing>(); + }); + } + +#if NET + [Test] + public Task RecordTypesOk() + { + var code = """ + using System.Collections.Generic; + + public class Foo + { + public Dictionary Field; + public Dictionary Property { get; set; } + } + + public record class GoodRecord(string A, int B); + """; + + return Assert(code, DiagnosticIds.DictionaryHasUnsupportedKeyType); + } +#endif + } +} diff --git a/src/Particular.Analyzers.Tests/DroppedTaskAnalyzerTests.cs b/src/Particular.Analyzers.Tests/DroppedTaskAnalyzerTests.cs index 845f0c6..a03f3ef 100644 --- a/src/Particular.Analyzers.Tests/DroppedTaskAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/DroppedTaskAnalyzerTests.cs @@ -2,9 +2,8 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class DroppedTaskAnalyzerTests : AnalyzerTestFixture @@ -98,14 +97,12 @@ void MyContainingMethod() "void MyMethod() { var task = Task.Delay(0).ConfigureAwait(false); }", }.ToData(); - public DroppedTaskAnalyzerTests(ITestOutputHelper output) : base(output) { } - - [Theory] - [MemberData(nameof(SadMethodData))] + [Test] + [TestCaseSource(nameof(SadMethodData))] public Task SadMethods(string method) => Assert(GetCode(method), DiagnosticIds.DroppedTask); - [Theory] - [MemberData(nameof(HappyMethodData))] + [Test] + [TestCaseSource(nameof(HappyMethodData))] public Task HappyMethods(string method) => Assert(GetCode(method)); public static string GetCode(string method) => string.Format(code, method); diff --git a/src/Particular.Analyzers.Tests/Helpers/AnalyzerTestFixture.cs b/src/Particular.Analyzers.Tests/Helpers/AnalyzerTestFixture.cs index 3e6dcee..f300e37 100644 --- a/src/Particular.Analyzers.Tests/Helpers/AnalyzerTestFixture.cs +++ b/src/Particular.Analyzers.Tests/Helpers/AnalyzerTestFixture.cs @@ -2,9 +2,7 @@ { using System; using System.Collections.Generic; - using System.Collections.Immutable; using System.Linq; - using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -12,14 +10,10 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; - using Xunit.Abstractions; + using NUnit.Framework; public class AnalyzerTestFixture where TAnalyzer : DiagnosticAnalyzer, new() { - public AnalyzerTestFixture(ITestOutputHelper output) => Output = output; - - AnalyzerTestFixture() { } - protected static readonly List PrivateModifiers = ["", "private"]; protected static readonly List NonPrivateModifiers = ["public", "protected", "internal", "protected internal", "private protected"]; @@ -43,16 +37,17 @@ #endif ]; - protected ITestOutputHelper Output { get; } - - protected Task Assert(string markupCode, CompilationOptions compilationOptions = null, CancellationToken cancellationToken = default) => - Assert(markupCode, Array.Empty(), compilationOptions, cancellationToken); + protected Task Assert(string markupCode, Action customize = null, CancellationToken cancellationToken = default) => + Assert(markupCode, Array.Empty(), customize, cancellationToken); - protected Task Assert(string markupCode, string expectedDiagnosticId, CompilationOptions compilationOptions = null, CancellationToken cancellationToken = default) => - Assert(markupCode, new[] { expectedDiagnosticId }, compilationOptions, cancellationToken); + protected Task Assert(string markupCode, string expectedDiagnosticId, Action customize = null, CancellationToken cancellationToken = default) => + Assert(markupCode, new[] { expectedDiagnosticId }, customize, cancellationToken); - protected async Task Assert(string markupCode, string[] expectedDiagnosticIds, CompilationOptions compilationOptions = null, CancellationToken cancellationToken = default) + protected async Task Assert(string markupCode, string[] expectedDiagnosticIds, Action customize = null, CancellationToken cancellationToken = default) { + var testCustomizations = new TestCustomizations(); + customize?.Invoke(testCustomizations); + var externalTypes = @"namespace NServiceBus { @@ -73,18 +68,18 @@ interface IMessage { } markupCode; var (code, markupSpans) = Parse(markupCode); - WriteCode(Output, code); + WriteCode(code); - var document = CreateDocument(code, externalTypes, compilationOptions); + var document = CreateDocument(code, externalTypes, testCustomizations); var compilerDiagnostics = await document.GetCompilerDiagnostics(cancellationToken); - WriteCompilerDiagnostics(Output, compilerDiagnostics); + WriteCompilerDiagnostics(compilerDiagnostics); var compilation = await document.Project.GetCompilationAsync(cancellationToken); compilation.Compile(); var analyzerDiagnostics = (await compilation.GetAnalyzerDiagnostics(new TAnalyzer(), cancellationToken)).ToList(); - WriteAnalyzerDiagnostics(Output, analyzerDiagnostics); + WriteAnalyzerDiagnostics(analyzerDiagnostics); var expectedSpansAndIds = expectedDiagnosticIds .SelectMany(id => markupSpans.Select(span => (span, id))) @@ -96,50 +91,46 @@ interface IMessage { } .Select(diagnostic => (diagnostic.Location.SourceSpan, diagnostic.Id)) .ToList(); - Xunit.Assert.Equal(expectedSpansAndIds, actualSpansAndIds); + NUnit.Framework.Assert.That(actualSpansAndIds, Is.EqualTo(expectedSpansAndIds)); } - protected static void WriteCode(ITestOutputHelper Output, string code) + protected static void WriteCode(string code) { foreach (var (line, index) in code.Replace("\r\n", "\n").Split('\n') .Select((line, index) => (line, index))) { - Output.WriteLine($" {index + 1,3}: {line}"); + TestContext.Out.WriteLine($" {index + 1,3}: {line}"); } } - protected static Document CreateDocument(string code, string externalTypes, CompilationOptions compilationOptions) + protected static Document CreateDocument(string code, string externalTypes, TestCustomizations customizations) { - var references = ImmutableList.Create( - MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location)); - return new AdhocWorkspace() .AddProject("TestProject", LanguageNames.CSharp) - .WithCompilationOptions(compilationOptions ?? new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) - .AddMetadataReferences(references) + .WithCompilationOptions(customizations.CompilationOptions ?? new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddMetadataReferences(customizations.GetMetadataReferences()) .AddDocument("Externaltypes", externalTypes) .Project .AddDocument("TestDocument", code); } - protected static void WriteCompilerDiagnostics(ITestOutputHelper Output, IEnumerable diagnostics) + protected static void WriteCompilerDiagnostics(IEnumerable diagnostics) { - Output.WriteLine("Compiler diagnostics:"); + TestContext.Out.WriteLine("Compiler diagnostics:"); foreach (var diagnostic in diagnostics) { - Output.WriteLine($" {diagnostic}"); + TestContext.Out.WriteLine($" {diagnostic}"); } } - protected static void WriteAnalyzerDiagnostics(ITestOutputHelper Output, IEnumerable diagnostics) + protected static void WriteAnalyzerDiagnostics(IEnumerable diagnostics) { - Output.WriteLine("Analyzer diagnostics:"); + TestContext.Out.WriteLine("Analyzer diagnostics:"); foreach (var diagnostic in diagnostics) { - Output.WriteLine($" {diagnostic}"); + TestContext.Out.WriteLine($" {diagnostic}"); } } diff --git a/src/Particular.Analyzers.Tests/Helpers/TestCustomizations.cs b/src/Particular.Analyzers.Tests/Helpers/TestCustomizations.cs new file mode 100644 index 0000000..4d85339 --- /dev/null +++ b/src/Particular.Analyzers.Tests/Helpers/TestCustomizations.cs @@ -0,0 +1,35 @@ +namespace Particular.Analyzers.Tests.Helpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Microsoft.CodeAnalysis; + + public class TestCustomizations + { + HashSet typesForMetadataReferences = [ + typeof(object), + typeof(Enumerable) + ]; + + public CompilationOptions CompilationOptions { get; set; } + + public TestCustomizations AddMetadataReferenceUsing() + { + typesForMetadataReferences.Add(typeof(T)); + return this; + } + + public IEnumerable GetMetadataReferences() + { + var arr = typesForMetadataReferences + .Select(type => type.GetTypeInfo().Assembly) + .Distinct() + .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) + .ToArray(); + + return arr; + } + } +} diff --git a/src/Particular.Analyzers.Tests/LoggingAnalyzerTests.cs b/src/Particular.Analyzers.Tests/LoggingAnalyzerTests.cs index fb0db82..b83c432 100644 --- a/src/Particular.Analyzers.Tests/LoggingAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/LoggingAnalyzerTests.cs @@ -2,20 +2,17 @@ { using System; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; public class LoggingAnalyzerTests : AnalyzerTestFixture { - public LoggingAnalyzerTests(ITestOutputHelper output) : base(output) { } - - [Theory] - [InlineData("DebugFormat")] - [InlineData("InfoFormat")] - [InlineData("WarnFormat")] - [InlineData("ErrorFormat")] - [InlineData("FatalFormat")] + [Test] + [TestCase("DebugFormat")] + [TestCase("InfoFormat")] + [TestCase("WarnFormat")] + [TestCase("ErrorFormat")] + [TestCase("FatalFormat")] public Task NServiceBusLogging(string methodName) { var code = $$$"""" @@ -69,13 +66,13 @@ public interface ILog return Assert(code, DiagnosticIds.StructuredLoggingWithRepeatedToken); } - [Theory] - [InlineData("LogDebug", "Debug")] - [InlineData("LogTrace", "Trace")] - [InlineData("LogInformation", "Information")] - [InlineData("LogWarning", "Warning")] - [InlineData("LogError", "Error")] - [InlineData("LogCritical", "Critical")] + [Test] + [TestCase("LogDebug", "Debug")] + [TestCase("LogTrace", "Trace")] + [TestCase("LogInformation", "Information")] + [TestCase("LogWarning", "Warning")] + [TestCase("LogError", "Error")] + [TestCase("LogCritical", "Critical")] public Task MicrosoftExtensionsLoggingAllOk(string methodName, string logLevel) { var code = $$""" @@ -155,13 +152,13 @@ public void Bar(ILogger log) return Assert(code, DiagnosticIds.StructuredLoggingWithRepeatedToken); } - [Theory] - [InlineData("LogDebug", "Debug")] - [InlineData("LogTrace", "Trace")] - [InlineData("LogInformation", "Information")] - [InlineData("LogWarning", "Warning")] - [InlineData("LogError", "Error")] - [InlineData("LogCritical", "Critical")] + [Test] + [TestCase("LogDebug", "Debug")] + [TestCase("LogTrace", "Trace")] + [TestCase("LogInformation", "Information")] + [TestCase("LogWarning", "Warning")] + [TestCase("LogError", "Error")] + [TestCase("LogCritical", "Critical")] public Task MicrosoftExtensionsLoggingNotOk(string methodName, string logLevel) { var code = $$""" diff --git a/src/Particular.Analyzers.Tests/Particular.Analyzers.Tests.csproj b/src/Particular.Analyzers.Tests/Particular.Analyzers.Tests.csproj index 6697d00..3af433c 100644 --- a/src/Particular.Analyzers.Tests/Particular.Analyzers.Tests.csproj +++ b/src/Particular.Analyzers.Tests/Particular.Analyzers.Tests.csproj @@ -1,4 +1,4 @@ - + net472;net6.0;net8.0 @@ -11,10 +11,11 @@ - + - - + + + diff --git a/src/Particular.Analyzers.Tests/TypeNameAnalyzerTests.cs b/src/Particular.Analyzers.Tests/TypeNameAnalyzerTests.cs index e8d0409..5e2c74c 100644 --- a/src/Particular.Analyzers.Tests/TypeNameAnalyzerTests.cs +++ b/src/Particular.Analyzers.Tests/TypeNameAnalyzerTests.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using NUnit.Framework; using Particular.Analyzers.Tests.Helpers; - using Xunit; - using Xunit.Abstractions; using Data = System.Collections.Generic.IEnumerable; public class TypeNameAnalyzerTests : AnalyzerTestFixture @@ -14,8 +13,6 @@ public class TypeNameAnalyzerTests : AnalyzerTestFixture static readonly string @delegate = "delegate void [|{0}|]();"; - public TypeNameAnalyzerTests(ITestOutputHelper output) : base(output) { } - static readonly List interfaceKeywords = [ "interface", @@ -53,20 +50,20 @@ public TypeNameAnalyzerTests(ITestOutputHelper output) : base(output) { } public static Data HappyDelegatesData => nonInterfaceNames.ToData(); - [Theory] - [MemberData(nameof(SadTypesData))] + [Test] + [TestCaseSource(nameof(SadTypesData))] public Task SadTypes(string keyword, string name) => Assert(GetTypeCode(@type, keyword, name), DiagnosticIds.NonInterfaceTypePrefixedWithI); - [Theory] - [MemberData(nameof(HappyTypesData))] + [Test] + [TestCaseSource(nameof(HappyTypesData))] public Task HappyTypes(string keyword, string name) => Assert(GetTypeCode(@type, keyword, name)); - [Theory] - [MemberData(nameof(SadDelegatesData))] + [Test] + [TestCaseSource(nameof(SadDelegatesData))] public Task SadDelegates(string name) => Assert(GetDelegateCode(@delegate, name), DiagnosticIds.NonInterfaceTypePrefixedWithI); - [Theory] - [MemberData(nameof(HappyDelegatesData))] + [Test] + [TestCaseSource(nameof(HappyDelegatesData))] public Task HappyDelegates(string name) => Assert(GetDelegateCode(@delegate, name)); static string GetTypeCode(string template, string keyword, string name) => string.Format(template, keyword, name); diff --git a/src/Particular.Analyzers/AsyncVoidAnalyzer.cs b/src/Particular.Analyzers/AsyncVoidAnalyzer.cs new file mode 100644 index 0000000..5dfc23b --- /dev/null +++ b/src/Particular.Analyzers/AsyncVoidAnalyzer.cs @@ -0,0 +1,45 @@ +namespace Particular.Analyzers +{ + using System.Collections.Immutable; + using System.Linq; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AsyncVoidAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.AsyncVoid); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.LocalFunctionStatement); + } + + void AnalyzeMethod(SyntaxNodeAnalysisContext context) + { + if (context.Node is MethodDeclarationSyntax method) + { + Analyze(context, method.Identifier, method.ReturnType, method.Modifiers); + } + else if (context.Node is LocalFunctionStatementSyntax localFn) + { + Analyze(context, localFn.Identifier, localFn.ReturnType, localFn.Modifiers); + } + } + + static void Analyze(SyntaxNodeAnalysisContext context, SyntaxToken identifier, TypeSyntax returnType, SyntaxTokenList modifiers) + { + if (returnType?.ToString() == "void" && modifiers.Any(token => token.IsKind(SyntaxKind.AsyncKeyword))) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.AsyncVoid, identifier.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } +} diff --git a/src/Particular.Analyzers/Cancellation/CancellationTryCatchAnalyzer.cs b/src/Particular.Analyzers/Cancellation/CancellationTryCatchAnalyzer.cs index 2d15a8a..da0b077 100644 --- a/src/Particular.Analyzers/Cancellation/CancellationTryCatchAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/CancellationTryCatchAnalyzer.cs @@ -29,7 +29,7 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is TryStatementSyntax tryStatement)) + if (context.Node is not TryStatementSyntax tryStatement) { return; } @@ -45,7 +45,7 @@ static void Analyze(SyntaxNodeAnalysisContext context) return; } - if (!(GetFirstCancellationTokenExpressionOrDefault(context, tryStatement) is string cancellationTokenExpression)) + if (GetFirstCancellationTokenExpressionOrDefault(context, tryStatement) is not string cancellationTokenExpression) { return; } @@ -85,7 +85,7 @@ static bool HasFilterWhichGetsIsCancellationRequestedFromExpression(CatchClauseS return false; } - if (!(catchClause.Filter.FilterExpression is MemberAccessExpressionSyntax memberAccess) || !memberAccess.IsKind(SyntaxKind.SimpleMemberAccessExpression)) + if (catchClause.Filter.FilterExpression is not MemberAccessExpressionSyntax memberAccess || !memberAccess.IsKind(SyntaxKind.SimpleMemberAccessExpression)) { return false; } @@ -132,7 +132,7 @@ static bool HasFilterIncludingCaughtException(CatchClauseSyntax catchClause) return filterIdentifiers.Any(); } - static string GetFirstCancellationTokenExpressionOrDefault(SyntaxNodeAnalysisContext context, TryStatementSyntax tryStatement) + static string? GetFirstCancellationTokenExpressionOrDefault(SyntaxNodeAnalysisContext context, TryStatementSyntax tryStatement) { // Because we are examining all descendants, this may result in false positives. // For example, a nested try block may contain cancellable invocations and @@ -172,8 +172,8 @@ static IEnumerable GetCancellationTokenExpressions(IEnumerable !(arg.Expression is LiteralExpressionSyntax)) - .Where(arg => !(arg.Expression is DefaultExpressionSyntax)) + .Where(arg => arg.Expression is not LiteralExpressionSyntax) + .Where(arg => arg.Expression is not DefaultExpressionSyntax) .Where(arg => !IsCancellationTokenNone(arg)) .Select(arg => { @@ -191,7 +191,7 @@ static IEnumerable GetCancellationTokenExpressions(IEnumerable expr != null); + .OfType(); // removes nulls foreach (var expr in argExpressions) { @@ -209,21 +209,17 @@ static string GetCatchType(CatchClauseSyntax catchClause) // { // } // assume "Exception" and "OperationCanceledException" refer to the System types - switch (catchType) - { - case null: - case "Exception": - return ExceptionType; - case "OperationCanceledException": - return OperationCanceledExceptionType; - default: - return catchType; - } + return catchType switch + { + null or "Exception" => ExceptionType, + "OperationCanceledException" => OperationCanceledExceptionType, + _ => catchType, + }; } static bool IsCancellationTokenNone(ArgumentSyntax arg) { - if (!(arg.Expression is MemberAccessExpressionSyntax memberAccess)) + if (arg.Expression is not MemberAccessExpressionSyntax memberAccess) { return false; } @@ -233,7 +229,7 @@ static bool IsCancellationTokenNone(ArgumentSyntax arg) return false; } - if (!(memberAccess.Expression is SimpleNameSyntax @ref)) + if (memberAccess.Expression is not SimpleNameSyntax @ref) { return false; } diff --git a/src/Particular.Analyzers/Cancellation/ContextMethodParameterAnalyzer.cs b/src/Particular.Analyzers/Cancellation/ContextMethodParameterAnalyzer.cs index fd4ef68..33238f0 100644 --- a/src/Particular.Analyzers/Cancellation/ContextMethodParameterAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/ContextMethodParameterAnalyzer.cs @@ -27,7 +27,7 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is TypeDeclarationSyntax type)) + if (context.Node is not TypeDeclarationSyntax type) { return; } @@ -38,7 +38,10 @@ static void Analyze(SyntaxNodeAnalysisContext context) return; } - Analyze(context, context.SemanticModel.GetDeclaredSymbol(type, context.CancellationToken)); + if (context.SemanticModel.GetDeclaredSymbol(type, context.CancellationToken) is INamedTypeSymbol symbol) + { + Analyze(context, symbol); + } } static void Analyze(SyntaxNodeAnalysisContext context, INamedTypeSymbol type) @@ -52,7 +55,7 @@ static void Analyze(SyntaxNodeAnalysisContext context, INamedTypeSymbol type) { context.CancellationToken.ThrowIfCancellationRequested(); - if (!(member is IMethodSymbol method)) + if (member is not IMethodSymbol method) { continue; } diff --git a/src/Particular.Analyzers/Cancellation/DelegateParametersAnalyzer.cs b/src/Particular.Analyzers/Cancellation/DelegateParametersAnalyzer.cs index acb6ebc..9bd8ef3 100644 --- a/src/Particular.Analyzers/Cancellation/DelegateParametersAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/DelegateParametersAnalyzer.cs @@ -23,7 +23,7 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is DelegateDeclarationSyntax delegateSyntax)) + if (context.Node is not DelegateDeclarationSyntax delegateSyntax) { return; } @@ -39,7 +39,10 @@ static void Analyze(SyntaxNodeAnalysisContext context) return; } - Analyze(context, context.SemanticModel.GetInvokeMethod(delegateSyntax, context.CancellationToken, out _).Parameters); + if (context.SemanticModel.GetInvokeMethod(delegateSyntax, context.CancellationToken, out _) is IMethodSymbol method) + { + Analyze(context, method.Parameters); + } } static void Analyze(SyntaxNodeAnalysisContext context, ImmutableArray @params) diff --git a/src/Particular.Analyzers/Cancellation/EmptyTokenAnalyzer.cs b/src/Particular.Analyzers/Cancellation/EmptyTokenAnalyzer.cs index 8a3ab57..8b62536 100644 --- a/src/Particular.Analyzers/Cancellation/EmptyTokenAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/EmptyTokenAnalyzer.cs @@ -23,7 +23,7 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is ArgumentSyntax arg)) + if (context.Node is not ArgumentSyntax arg) { return; } diff --git a/src/Particular.Analyzers/Cancellation/MethodFuncParameterAnalyzer.cs b/src/Particular.Analyzers/Cancellation/MethodFuncParameterAnalyzer.cs index 57d0ada..4b0784e 100644 --- a/src/Particular.Analyzers/Cancellation/MethodFuncParameterAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/MethodFuncParameterAnalyzer.cs @@ -31,12 +31,12 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberDeclarationSyntax member)) + if (context.Node is not MemberDeclarationSyntax member) { return; } - if (!(context.SemanticModel.GetMethod(member, context.CancellationToken, out _) is IMethodSymbol method)) + if (context.SemanticModel.GetMethod(member, context.CancellationToken, out _) is not IMethodSymbol method) { return; } @@ -67,7 +67,7 @@ static void Analyze(SyntaxNodeAnalysisContext context, IMethodSymbol method) static void Analyze(SyntaxNodeAnalysisContext context, IParameterSymbol param) { - if (!(param.Type is INamedTypeSymbol type)) + if (param.Type is not INamedTypeSymbol type) { return; } diff --git a/src/Particular.Analyzers/Cancellation/MethodParametersAnalyzer.cs b/src/Particular.Analyzers/Cancellation/MethodParametersAnalyzer.cs index a705410..4cd7fba 100644 --- a/src/Particular.Analyzers/Cancellation/MethodParametersAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/MethodParametersAnalyzer.cs @@ -3,8 +3,8 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Particular.Analyzers.Extensions; @@ -30,12 +30,12 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberDeclarationSyntax member)) + if (context.Node is not MemberDeclarationSyntax member) { return; } - if (!(context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is IMethodSymbol method)) + if (context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is not IMethodSymbol method || declaredSymbol is null) { return; } diff --git a/src/Particular.Analyzers/Cancellation/MethodTokenNamesAnalyzer.cs b/src/Particular.Analyzers/Cancellation/MethodTokenNamesAnalyzer.cs index 36e90fa..c222373 100644 --- a/src/Particular.Analyzers/Cancellation/MethodTokenNamesAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/MethodTokenNamesAnalyzer.cs @@ -1,13 +1,13 @@ namespace Particular.Analyzers.Cancellation { + using System; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Particular.Analyzers.Extensions; - using System; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class MethodTokenNamesAnalyzer : DiagnosticAnalyzer @@ -28,12 +28,12 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberDeclarationSyntax member)) + if (context.Node is not MemberDeclarationSyntax member) { return; } - if (!(context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is IMethodSymbol method)) + if (context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is not IMethodSymbol method || declaredSymbol is null) { return; } diff --git a/src/Particular.Analyzers/Cancellation/NonPrivateMethodTokenNameAnalyzer.cs b/src/Particular.Analyzers/Cancellation/NonPrivateMethodTokenNameAnalyzer.cs index 085820f..a62c2b9 100644 --- a/src/Particular.Analyzers/Cancellation/NonPrivateMethodTokenNameAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/NonPrivateMethodTokenNameAnalyzer.cs @@ -3,8 +3,8 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Particular.Analyzers.Extensions; @@ -27,12 +27,12 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberDeclarationSyntax member)) + if (context.Node is not MemberDeclarationSyntax member) { return; } - if (!(context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is IMethodSymbol method)) + if (context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is not IMethodSymbol method || declaredSymbol is null) { return; } diff --git a/src/Particular.Analyzers/Cancellation/TaskReturningMethodAnalyzer.cs b/src/Particular.Analyzers/Cancellation/TaskReturningMethodAnalyzer.cs index d3a3f23..53e4c9a 100644 --- a/src/Particular.Analyzers/Cancellation/TaskReturningMethodAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/TaskReturningMethodAnalyzer.cs @@ -3,8 +3,8 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Particular.Analyzers.Extensions; @@ -26,12 +26,12 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberDeclarationSyntax member)) + if (context.Node is not MemberDeclarationSyntax member) { return; } - if (!(context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is IMethodSymbol method)) + if (context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is not IMethodSymbol method || declaredSymbol is null) { return; } diff --git a/src/Particular.Analyzers/Cancellation/TokenAccessibilityAnalyzer.cs b/src/Particular.Analyzers/Cancellation/TokenAccessibilityAnalyzer.cs index 1d6bf5e..b40a002 100644 --- a/src/Particular.Analyzers/Cancellation/TokenAccessibilityAnalyzer.cs +++ b/src/Particular.Analyzers/Cancellation/TokenAccessibilityAnalyzer.cs @@ -26,12 +26,12 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberDeclarationSyntax member)) + if (context.Node is not MemberDeclarationSyntax member) { return; } - if (!(context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is IMethodSymbol method)) + if (context.SemanticModel.GetMethod(member, context.CancellationToken, out var declaredSymbol) is not IMethodSymbol method || declaredSymbol is null) { return; } diff --git a/src/Particular.Analyzers/Cancellation/TokenParamUnusedSuppressor.cs b/src/Particular.Analyzers/Cancellation/TokenParamUnusedSuppressor.cs index afdb843..c6794de 100644 --- a/src/Particular.Analyzers/Cancellation/TokenParamUnusedSuppressor.cs +++ b/src/Particular.Analyzers/Cancellation/TokenParamUnusedSuppressor.cs @@ -20,20 +20,25 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) { foreach (var diagnostic in context.ReportedDiagnostics) { - var node = diagnostic.Location.SourceTree.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + var sourceTree = diagnostic.Location.SourceTree; + if (sourceTree is null) + { + continue; + } + var node = sourceTree.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); if (node == null) { continue; } - if (!(node is ParameterSyntax parameter)) + if (node is not ParameterSyntax parameter) { continue; } // cheapest checks first - if (!(parameter.Type is NameSyntax)) + if (parameter.Type is not NameSyntax) { return; } diff --git a/src/Particular.Analyzers/DateTimeImplicitCastAnalyzer.cs b/src/Particular.Analyzers/DateTimeImplicitCastAnalyzer.cs index d7328e9..8542bbd 100644 --- a/src/Particular.Analyzers/DateTimeImplicitCastAnalyzer.cs +++ b/src/Particular.Analyzers/DateTimeImplicitCastAnalyzer.cs @@ -26,7 +26,7 @@ public override void Initialize(AnalysisContext context) static void AnalyzeVariableDeclaration(SyntaxNodeAnalysisContext context) { - if (!(context.Node is VariableDeclarationSyntax declaration)) + if (context.Node is not VariableDeclarationSyntax declaration) { return; } @@ -46,7 +46,7 @@ static void AnalyzeVariableDeclaration(SyntaxNodeAnalysisContext context) } var initializerType = context.SemanticModel.GetTypeInfo(initializer, context.CancellationToken).Type; - if (initializerType.ToString() != "System.DateTime") + if (initializerType?.ToString() != "System.DateTime") { continue; } @@ -57,19 +57,19 @@ static void AnalyzeVariableDeclaration(SyntaxNodeAnalysisContext context) static void AnalyzeAssignment(SyntaxNodeAnalysisContext context) { - if (!(context.Node is AssignmentExpressionSyntax assignment)) + if (context.Node is not AssignmentExpressionSyntax assignment) { return; } - if (!(context.SemanticModel.GetTypeInfo(assignment.Left, context.CancellationToken).Type is INamedTypeSymbol leftType)) + if (context.SemanticModel.GetTypeInfo(assignment.Left, context.CancellationToken).Type is not INamedTypeSymbol leftType) { return; } if (leftType.IsTupleType && leftType.TypeArguments.Any(t => t.ToString() == "System.DateTimeOffset")) { - if (!(context.SemanticModel.GetTypeInfo(assignment.Right, context.CancellationToken).Type is INamedTypeSymbol rightType)) + if (context.SemanticModel.GetTypeInfo(assignment.Right, context.CancellationToken).Type is not INamedTypeSymbol rightType) { return; } @@ -101,7 +101,7 @@ static void AnalyzeAssignment(SyntaxNodeAnalysisContext context) { var rightType = context.SemanticModel.GetTypeInfo(assignment.Right, context.CancellationToken).Type; - if (rightType.ToString() != "System.DateTime") + if (rightType?.ToString() != "System.DateTime") { return; } @@ -112,34 +112,38 @@ static void AnalyzeAssignment(SyntaxNodeAnalysisContext context) static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MethodDeclarationSyntax method)) + if (context.Node is not MethodDeclarationSyntax method) { return; } var returnType = method.ReturnType.ToString(); - if (returnType != "DateTimeOffset" && returnType != "Task") + if (returnType is not "DateTimeOffset" and not "Task") { return; } - var returnStatements = method.DescendantNodes().OfType().ToImmutableArray(); + var returnStatementExpressions = method.DescendantNodes() + .OfType() + .Select(returnStatement => returnStatement.Expression) + .OfType() + .ToImmutableArray(); - foreach (var returnStatement in returnStatements) + foreach (var returnStatement in returnStatementExpressions) { - var typeInfo = context.SemanticModel.GetTypeInfo(returnStatement.Expression, context.CancellationToken); + var typeInfo = context.SemanticModel.GetTypeInfo(returnStatement, context.CancellationToken); if (typeInfo.Type?.ToString() == "System.DateTime") { - context.ReportDiagnostic(DiagnosticDescriptors.ImplicitCastFromDateTimeToDateTimeOffset, returnStatement.Expression); + context.ReportDiagnostic(DiagnosticDescriptors.ImplicitCastFromDateTimeToDateTimeOffset, returnStatement); } } } static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { - if (!(context.Node is InvocationExpressionSyntax invocation)) + if (context.Node is not InvocationExpressionSyntax invocation) { return; } @@ -149,7 +153,7 @@ static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) return; } - if (!(context.SemanticModel.GetSymbolInfo(invocation.Expression, context.CancellationToken).Symbol?.GetMethodOrDefault() is IMethodSymbol method)) + if (context.SemanticModel.GetSymbolInfo(invocation.Expression, context.CancellationToken).Symbol?.GetMethodOrDefault() is not IMethodSymbol method) { return; } diff --git a/src/Particular.Analyzers/DateTimeNowAnalyzer.cs b/src/Particular.Analyzers/DateTimeNowAnalyzer.cs index 72cd831..799bb88 100644 --- a/src/Particular.Analyzers/DateTimeNowAnalyzer.cs +++ b/src/Particular.Analyzers/DateTimeNowAnalyzer.cs @@ -22,7 +22,7 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberAccessExpressionSyntax memberAccess)) + if (context.Node is not MemberAccessExpressionSyntax memberAccess) { return; } @@ -32,14 +32,14 @@ static void Analyze(SyntaxNodeAnalysisContext context) return; } - if (!(memberAccess.Expression is IdentifierNameSyntax identifier)) + if (memberAccess.Expression is not IdentifierNameSyntax identifier) { return; } var value = identifier?.Identifier.ValueText; - if (value == "DateTime" || value == "DateTimeOffset") + if (value is "DateTime" or "DateTimeOffset") { context.ReportDiagnostic(DiagnosticDescriptors.NowUsedInsteadOfUtcNow, memberAccess, value); } diff --git a/src/Particular.Analyzers/DiagnosticDescriptors.cs b/src/Particular.Analyzers/DiagnosticDescriptors.cs index f56e9f9..114b02c 100644 --- a/src/Particular.Analyzers/DiagnosticDescriptors.cs +++ b/src/Particular.Analyzers/DiagnosticDescriptors.cs @@ -4,7 +4,7 @@ public static class DiagnosticDescriptors { - public static readonly DiagnosticDescriptor DroppedTask = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor DroppedTask = new( id: DiagnosticIds.DroppedTask, title: "Tasks returned from expressions should be returned, awaited, or assigned to a variable", messageFormat: "Return, await, or assign the task to a variable", @@ -12,7 +12,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor CancellableContextMethodCancellationToken = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor CancellableContextMethodCancellationToken = new( id: DiagnosticIds.CancellableContextMethodCancellationToken, title: "Instance methods on types implementing ICancellableContext should not have a CancellationToken parameter", messageFormat: "Remove the CancellationToken parameter", @@ -20,7 +20,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor CancellationTokenNonPrivateRequired = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor CancellationTokenNonPrivateRequired = new( id: DiagnosticIds.CancellationTokenNonPrivateRequired, title: "A parameter of type CancellationToken on a non-private delegate or method should be optional", messageFormat: "Make the CancellationToken parameter optional", @@ -28,7 +28,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor CancellationTokenPrivateOptional = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor CancellationTokenPrivateOptional = new( id: DiagnosticIds.CancellationTokenPrivateOptional, title: "A parameter of type CancellationToken on a private delegate or method should be required", messageFormat: "Make the CancellationToken parameter required", @@ -36,7 +36,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor DelegateCancellationTokenMisplaced = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor DelegateCancellationTokenMisplaced = new( id: DiagnosticIds.DelegateCancellationTokenMisplaced, title: "Delegate CancellationToken parameters should come last", messageFormat: "Move the CancellationToken parameter to end of the parameter list", @@ -44,7 +44,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor EmptyCancellationTokenDefaultLiteral = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor EmptyCancellationTokenDefaultLiteral = new( id: DiagnosticIds.EmptyCancellationTokenDefaultLiteral, title: "The default literal should not be used as an argument instead of CancellationToken.None", messageFormat: "Change the argument to CancellationToken.None", @@ -52,7 +52,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor EmptyCancellationTokenDefaultOperator = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor EmptyCancellationTokenDefaultOperator = new( id: DiagnosticIds.EmptyCancellationTokenDefaultOperator, title: "The default operator should not be used as an argument instead of CancellationToken.None", messageFormat: "Change the argument to CancellationToken.None", @@ -60,7 +60,15 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodFuncParameterCancellationTokenMisplaced = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodCancellationTokenMisnamed = new( + id: DiagnosticIds.MethodCancellationTokenMisnamed, + title: "CancellationToken parameters should be named cancellationToken or have names ending with CancellationToken", + messageFormat: "Rename the CancellationToken parameter to cancellationToken or to end with CancellationToken", + category: "Code", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor MethodFuncParameterCancellationTokenMisplaced = new( id: DiagnosticIds.MethodFuncParameterCancellationTokenMisplaced, title: "Funcs used as method parameters should have CancellationToken parameter type arguments last", messageFormat: "Move the CancellationToken parameter type arguments to end of the parameter type arguments list", @@ -68,7 +76,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodFuncParameterMixedCancellation = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodFuncParameterMixedCancellation = new( id: DiagnosticIds.MethodFuncParameterMixedCancellation, title: "Funcs used as method parameters should not have both CancellationToken parameter type arguments and type arguments implementing ICancellableContext", messageFormat: "Remove either the CancellationToken parameter type arguments or the parameter type arguments implementing ICancellableContext", @@ -76,7 +84,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodFuncParameterMultipleCancellableContexts = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodFuncParameterMultipleCancellableContexts = new( id: DiagnosticIds.MethodFuncParameterMultipleCancellableContexts, title: "Funcs used as method parameters should have at most one parameter type argument implementing ICancellableContext", messageFormat: "Remove the duplicate parameter type arguments implementing ICancellableContext", @@ -84,7 +92,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodFuncParameterMultipleCancellationTokens = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodFuncParameterMultipleCancellationTokens = new( id: DiagnosticIds.MethodFuncParameterMultipleCancellationTokens, title: "Funcs used as method parameters should have at most one CancellationToken parameter type argument", messageFormat: "Remove the duplicate CancellationToken parameter type arguments", @@ -92,7 +100,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodFuncParameterTaskReturnTypeNoCancellation = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodFuncParameterTaskReturnTypeNoCancellation = new( id: DiagnosticIds.MethodFuncParameterTaskReturnTypeNoCancellation, title: "A Func used as a method parameter with a Task, ValueTask, or ValueTask return type argument should have at least one CancellationToken parameter type argument unless it has a parameter type argument implementing ICancellableContext", messageFormat: "Add a CancellationToken parameter type argument", @@ -100,7 +108,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodMixedCancellation = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodMixedCancellation = new( id: DiagnosticIds.MethodMixedCancellation, title: "Methods should not have both CancellationToken parameters and parameters implementing ICancellableContext", messageFormat: "Remove either the CancellationToken parameters or the parameters implementing ICancellableContext", @@ -108,7 +116,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodMultipleCancellableContexts = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodMultipleCancellableContexts = new( id: DiagnosticIds.MethodMultipleCancellableContexts, title: "Methods should have at most one parameter implementing ICancellableContext", messageFormat: "Remove the duplicate parameters implementing ICancellableContext", @@ -116,7 +124,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodMultipleCancellationTokens = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MethodMultipleCancellationTokens = new( id: DiagnosticIds.MethodMultipleCancellationTokens, title: "Methods should have at most one CancellationToken parameter", messageFormat: "Remove the duplicate CancellationToken parameters", @@ -124,15 +132,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MethodCancellationTokenMisnamed = new DiagnosticDescriptor( - id: DiagnosticIds.MethodCancellationTokenMisnamed, - title: "CancellationToken parameters should be named cancellationToken or have names ending with CancellationToken", - messageFormat: "Rename the CancellationToken parameter to cancellationToken or to end with CancellationToken", - category: "Code", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - public static readonly DiagnosticDescriptor NonPrivateMethodSingleCancellationTokenMisnamed = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor NonPrivateMethodSingleCancellationTokenMisnamed = new( id: DiagnosticIds.NonPrivateMethodSingleCancellationTokenMisnamed, title: "Single, non-private CancellationToken parameters should be named cancellationToken", messageFormat: "Rename the CancellationToken parameter to cancellationToken", @@ -140,7 +140,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor TaskReturningMethodNoCancellation = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor TaskReturningMethodNoCancellation = new( id: DiagnosticIds.TaskReturningMethodNoCancellation, title: "A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext", messageFormat: "Add a CancellationToken parameter", @@ -148,7 +148,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor ImproperTryCatchSystemException = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor ImproperTryCatchSystemException = new( id: DiagnosticIds.ImproperTryCatchSystemException, title: "When catching System.Exception, cancellation needs to be properly accounted for", messageFormat: "When a try block involves possible cancellation, catching Exception should be preceded by catching OperationCanceledException, or filtered by exception type and cancellationToken.IsCancellationRequested. See https://go.particular.net/exception-handling-with-cancellation.", @@ -156,7 +156,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor ImproperTryCatchOperationCanceled = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor ImproperTryCatchOperationCanceled = new( id: DiagnosticIds.ImproperTryCatchOperationCanceled, title: "When catching OperationCanceledException, cancellation needs to be properly accounted for", messageFormat: "Catching OperationCanceledException should be filtered by cancellationToken.IsCancellationRequested. See https://go.particular.net/exception-handling-with-cancellation.", @@ -164,7 +164,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MultipleCancellationTokensInATry = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor MultipleCancellationTokensInATry = new( id: DiagnosticIds.MultipleCancellationTokensInATry, title: "Highlight when a try block passes multiple cancellation tokens", messageFormat: "This try block passes more than one CancellationToken (or ICancellableContext) to other methods, which can be confusing. Suppress this message with a #pragma, add a comment explaining the use of the two tokens, and ensure any CancellationToken used in a catch block is the correct one.", @@ -172,7 +172,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor ImplicitCastFromDateTimeToDateTimeOffset = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor ImplicitCastFromDateTimeToDateTimeOffset = new( id: DiagnosticIds.ImplicitCastFromDateTimeToDateTimeOffset, title: "A DateTime should not be implicitly cast to a DateTimeOffset", messageFormat: "Do not implicitly cast a DateTime to a DateTimeOffset", @@ -180,7 +180,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NowUsedInsteadOfUtcNow = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor NowUsedInsteadOfUtcNow = new( id: DiagnosticIds.NowUsedInsteadOfUtcNow, title: "DateTime.UtcNow or DateTimeOffset.UtcNow should be used instead of DateTime.Now and DateTimeOffset.Now, unless the value is being used for displaying the current date-time in a user's local time zone", messageFormat: "Use {0}.UtcNow instead of {0}.Now", @@ -188,7 +188,7 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NonInterfaceTypePrefixedWithI = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor NonInterfaceTypePrefixedWithI = new( id: DiagnosticIds.NonInterfaceTypePrefixedWithI, title: "A non-interface type should not be prefixed with I", messageFormat: "Remove the I prefix from {0}", @@ -196,12 +196,28 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor StructuredLoggingWithRepeatedToken = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor DictionaryHasUnsupportedKeyType = new( + id: DiagnosticIds.DictionaryHasUnsupportedKeyType, + title: "Dictionary keys should implement IEquatable", + messageFormat: "This {0} uses the type {1} as its key, which is a reference type that does not implement IEquatable, which means that every object added to the collection will be evaluated using reference equality (object.Equals()) to be unique. This is often (but not always) a mistake, especially when used as a cache, because every lookup will be a cache miss. Before suppressing this diagnostic or changing the code, be sure to consider whether the collection is really meant to evaluate equality using default reference equality, or if doing so will cause unintended side effects from objects that have the same data appearing to be duplicated.", + category: "Code", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor StructuredLoggingWithRepeatedToken = new( id: DiagnosticIds.StructuredLoggingWithRepeatedToken, title: "Structured logging tokens cannot be repeated", messageFormat: "A token '{0}' was repeated in a logging format string. While this works with string.Format(), it is known to break when using some structured logging libraries and so must be avoided.", category: "Code", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor AsyncVoid = new( + id: DiagnosticIds.AsyncVoid, + title: "Methods should not be declared async void", + messageFormat: "An `async void` method is almost always a mistake as nothing can be returned to await. Should only be used for event delegates, in which case this rule should be disabled in that instance.", + category: "Code", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } } diff --git a/src/Particular.Analyzers/DiagnosticIds.cs b/src/Particular.Analyzers/DiagnosticIds.cs index 6620ab2..fc753df 100644 --- a/src/Particular.Analyzers/DiagnosticIds.cs +++ b/src/Particular.Analyzers/DiagnosticIds.cs @@ -26,6 +26,8 @@ public static class DiagnosticIds public const string ImplicitCastFromDateTimeToDateTimeOffset = "PS0022"; public const string NowUsedInsteadOfUtcNow = "PS0023"; public const string NonInterfaceTypePrefixedWithI = "PS0024"; - public const string StructuredLoggingWithRepeatedToken = "PS0025"; + public const string DictionaryHasUnsupportedKeyType = "PS0025"; + public const string StructuredLoggingWithRepeatedToken = "PS0026"; + public const string AsyncVoid = "PS0027"; } } diff --git a/src/Particular.Analyzers/DictionaryKeysAnalyzer.cs b/src/Particular.Analyzers/DictionaryKeysAnalyzer.cs new file mode 100644 index 0000000..217f54a --- /dev/null +++ b/src/Particular.Analyzers/DictionaryKeysAnalyzer.cs @@ -0,0 +1,233 @@ +namespace Particular.Analyzers +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Threading; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class DictionaryKeysAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.DictionaryHasUnsupportedKeyType); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(StartAnalysis); + } + + void StartAnalysis(CompilationStartAnalysisContext context) + { + var knownTypes = new KnownTypes(context.Compilation); + + context.RegisterSymbolAction(c => AnalyzeProperty(c, knownTypes), SymbolKind.Property); + context.RegisterSymbolAction(c => AnalyzeField(c, knownTypes), SymbolKind.Field); + context.RegisterSymbolAction(c => AnalyzeMethod(c, knownTypes), SymbolKind.Method); + + context.RegisterSyntaxNodeAction(c => AnalyzeVariableDeclaration(c, knownTypes), SyntaxKind.LocalDeclarationStatement); + context.RegisterSyntaxNodeAction(c => AnalyzeClassDeclaration(c, knownTypes), SyntaxKind.ClassDeclaration); + } + + static void AnalyzeProperty(SymbolAnalysisContext context, KnownTypes knownTypes) + { + if (context.Symbol is IPropertySymbol prop) + { + if (GetNearest(prop, context.CancellationToken)?.Type is TypeSyntax typeSyntax) + { + AnalyzeType(knownTypes, prop.Type, typeSyntax, context.ReportDiagnostic); + } + } + } + + static void AnalyzeField(SymbolAnalysisContext context, KnownTypes knownTypes) + { + if (context.Symbol is IFieldSymbol field) + { + if (GetNearest(field, context.CancellationToken)?.Type is TypeSyntax typeSyntax) + { + AnalyzeType(knownTypes, field.Type, typeSyntax, context.ReportDiagnostic); + } + } + } + + static void AnalyzeMethod(SymbolAnalysisContext context, KnownTypes knownTypes) + { + if (context.Symbol is IMethodSymbol method) + { + if (method.AssociatedSymbol is IPropertySymbol) + { + // The getter of a property is tested as a property, not as a method + return; + } + + // Will be null for things like operator overloads + var methodSyntax = GetNearest(method, context.CancellationToken); + + if (!method.ReturnsVoid && methodSyntax != null) + { + AnalyzeType(knownTypes, method.ReturnType, methodSyntax.ReturnType, context.ReportDiagnostic); + } + + for (var i = 0; i < method.Parameters.Length; i++) + { + var parameterType = method.Parameters[i].Type; + var parameterSyntax = GetNearest(method.Parameters[i], context.CancellationToken)?.Type; + if (parameterSyntax != null) + { + // Syntax will be null for a parameter when analyzing a top-level statements class + AnalyzeType(knownTypes, parameterType, parameterSyntax, context.ReportDiagnostic); + } + } + } + } + + static void AnalyzeVariableDeclaration(SyntaxNodeAnalysisContext context, KnownTypes knownTypes) + { + if (context.Node is LocalDeclarationStatementSyntax vDec) + { + // Don't want to analyze "var" + if (vDec.Declaration.Type is GenericNameSyntax nameSyntax) + { + if (context.SemanticModel.GetTypeInfo(vDec.Declaration.Type).Type is ITypeSymbol typeSymbol) + { + AnalyzeType(knownTypes, typeSymbol, nameSyntax, context.ReportDiagnostic); + } + } + + foreach (var variable in vDec.Declaration.Variables) + { + if (variable.Initializer?.Value is ObjectCreationExpressionSyntax creationSyntax) + { + // Don't want to analyze "null" or another variable name or new() + if (creationSyntax.Type != null) + { + if (context.SemanticModel.GetSymbolInfo(creationSyntax.Type, context.CancellationToken).Symbol is INamedTypeSymbol typeSymbol) + { + AnalyzeType(knownTypes, typeSymbol, creationSyntax.Type, context.ReportDiagnostic); + } + } + } + } + } + } + + static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context, KnownTypes knownTypes) + { + if (context.Node is ClassDeclarationSyntax classDec) + { + if (classDec.BaseList != null && context.SemanticModel.GetDeclaredSymbol(classDec, context.CancellationToken) is ITypeSymbol classType) + { + foreach (var baseType in classDec.BaseList.Types) + { + var type = context.SemanticModel.GetTypeInfo(baseType.Type, context.CancellationToken).Type; + if (type != null) + { + AnalyzeType(knownTypes, type, baseType, context.ReportDiagnostic); + } + } + } + } + } + + static TSyntaxType? GetNearest(ISymbol symbol, CancellationToken cancellationToken) where TSyntaxType : SyntaxNode + { + if (symbol.DeclaringSyntaxReferences.Length == 0) + { + return null; + } + + var firstReference = symbol.DeclaringSyntaxReferences[0]; + var declaringSyntax = firstReference.GetSyntax(cancellationToken); + var syntax = declaringSyntax.FirstAncestorOrSelf(); + return syntax; + } + + static void AnalyzeType(KnownTypes knownTypes, ITypeSymbol type, SyntaxNode syntax, Action reportDiagnostic) + { + if (type is IArrayTypeSymbol arrayType) + { + AnalyzeType(knownTypes, arrayType.ElementType, syntax, reportDiagnostic); + return; + } + + if (type is INamedTypeSymbol namedType && namedType.IsGenericType && knownTypes.DictionaryTypes.Contains(namedType.ConstructedFrom, SymbolEqualityComparer.IncludeNullability)) + { + var key = namedType.TypeArguments[0]; + + if (!IsAppropriateDictionaryKey(knownTypes, key)) + { + var simpleType = namedType.Name.Split('`')[0]; + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.DictionaryHasUnsupportedKeyType, syntax.GetLocation(), simpleType, key.ToDisplayString()); + reportDiagnostic(diagnostic); + } + } + } + + static bool IsAppropriateDictionaryKey(KnownTypes knownTypes, ITypeSymbol type) + { + if (type.Equals(knownTypes.String, SymbolEqualityComparer.IncludeNullability) || type.IsValueType) + { + return true; + } + + var implementsIEquatable = type.Interfaces + .Any(iface => iface.IsGenericType && iface.ConstructedFrom.Equals(knownTypes.IEquatableT, SymbolEqualityComparer.Default)); + + if (implementsIEquatable) + { + return true; + } + + bool hasEquals = false; + bool hasGetHashCode = false; + + foreach (var member in type.GetMembers()) + { + hasEquals |= member.Name == "Equals" && member.IsOverride; + hasGetHashCode |= member.Name == "GetHashCode" && member.IsOverride; + } + + return hasEquals && hasGetHashCode; + } + + class KnownTypes + { + public ImmutableHashSet DictionaryTypes { get; } + + public INamedTypeSymbol String { get; } + + public INamedTypeSymbol IEquatableT { get; } + + public KnownTypes(Compilation compilation) + { + var dictionaryTypes = new Type[] + { + typeof(IDictionary<,>), + typeof(Dictionary<,>), + typeof(IReadOnlyDictionary<,>), + typeof(ConcurrentDictionary<,>), + typeof(HashSet<>), + typeof(ISet<>), + typeof(ImmutableHashSet<>), + typeof(ImmutableDictionary<,>) + }; + + DictionaryTypes = dictionaryTypes + .Select(t => compilation.GetTypeByMetadataName(t.FullName)) + .OfType() // Removes not-found types if not found in the compilation + .ToImmutableHashSet(SymbolEqualityComparer.Default); + + String = compilation.GetSpecialType(SpecialType.System_String); + IEquatableT = compilation.GetTypeByMetadataName("System.IEquatable`1") ?? throw new Exception("Cannot find IEquatable"); + } + } + } +} diff --git a/src/Particular.Analyzers/DroppedTaskAnalyzer.cs b/src/Particular.Analyzers/DroppedTaskAnalyzer.cs index d990746..e367a72 100644 --- a/src/Particular.Analyzers/DroppedTaskAnalyzer.cs +++ b/src/Particular.Analyzers/DroppedTaskAnalyzer.cs @@ -22,23 +22,26 @@ public override void Initialize(AnalysisContext context) static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) { - if (!(context.Node is InvocationExpressionSyntax invocation)) + if (context.Node is not InvocationExpressionSyntax invocation) { return; } // cheapest checks first - if (!(invocation.Parent is ExpressionStatementSyntax)) + if (invocation.Parent is not ExpressionStatementSyntax) { return; } - Analyze(context, context.SemanticModel.GetSymbolInfo(invocation.Expression, context.CancellationToken).Symbol, invocation); + if (context.SemanticModel.GetSymbolInfo(invocation.Expression, context.CancellationToken).Symbol is ISymbol symbol) + { + Analyze(context, symbol, invocation); + } } static void Analyze(SyntaxNodeAnalysisContext context, ISymbol expression, InvocationExpressionSyntax invocation) { - if (!(expression.GetMethodOrDefault() is IMethodSymbol method)) + if (expression.GetMethodOrDefault() is not IMethodSymbol method) { return; } diff --git a/src/Particular.Analyzers/Extensions/MethodSymbolExtensions.cs b/src/Particular.Analyzers/Extensions/MethodSymbolExtensions.cs index 2e51e1d..bdd611a 100644 --- a/src/Particular.Analyzers/Extensions/MethodSymbolExtensions.cs +++ b/src/Particular.Analyzers/Extensions/MethodSymbolExtensions.cs @@ -8,26 +8,16 @@ public static class MethodSymbolExtensions public static bool IsTest(this IMethodSymbol method) => method != null && method.GetAttributes().Any(attribute => IsTestAttribute(attribute.AttributeClass)); - static bool IsTestAttribute(INamedTypeSymbol type) => + static bool IsTestAttribute(INamedTypeSymbol? type) => type != null && (IsTestAttribute(type.ToString()) || IsTestAttribute(type.BaseType)); static bool IsTestAttribute(string typeName) { - switch (typeName) + return typeName switch { - case "NUnit.Framework.OneTimeSetUpAttribute": - case "NUnit.Framework.OneTimeTearDownAttribute": - case "NUnit.Framework.SetUpAttribute": - case "NUnit.Framework.TearDownAttribute": - case "NUnit.Framework.TestAttribute": - case "NUnit.Framework.TestCaseAttribute": - case "NUnit.Framework.TestCaseSourceAttribute": - case "NUnit.Framework.TheoryAttribute": - case "Xunit.FactAttribute": - return true; - default: - return false; - } + "NUnit.Framework.OneTimeSetUpAttribute" or "NUnit.Framework.OneTimeTearDownAttribute" or "NUnit.Framework.SetUpAttribute" or "NUnit.Framework.TearDownAttribute" or "NUnit.Framework.TestAttribute" or "NUnit.Framework.TestCaseAttribute" or "NUnit.Framework.TestCaseSourceAttribute" or "NUnit.Framework.TheoryAttribute" or "Xunit.FactAttribute" => true, + _ => false, + }; } public static bool IsEntryPoint(this IMethodSymbol method) => diff --git a/src/Particular.Analyzers/Extensions/SemanticModelExtensions.cs b/src/Particular.Analyzers/Extensions/SemanticModelExtensions.cs index 5404634..d2aaaa1 100644 --- a/src/Particular.Analyzers/Extensions/SemanticModelExtensions.cs +++ b/src/Particular.Analyzers/Extensions/SemanticModelExtensions.cs @@ -1,13 +1,13 @@ namespace Particular.Analyzers.Extensions { + using System.Threading; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis; - using System.Threading; static class SemanticModelExtensions { - public static IMethodSymbol GetMethod(this SemanticModel semanticModel, MemberDeclarationSyntax declarationSyntax, CancellationToken cancellationToken, out ISymbol declaredSymbol) + public static IMethodSymbol? GetMethod(this SemanticModel semanticModel, MemberDeclarationSyntax declarationSyntax, CancellationToken cancellationToken, out ISymbol? declaredSymbol) { switch (declarationSyntax) { @@ -23,11 +23,11 @@ public static IMethodSymbol GetMethod(this SemanticModel semanticModel, MemberDe } } - public static IMethodSymbol GetInvokeMethod(this SemanticModel semanticModel, DelegateDeclarationSyntax declarationSyntax, CancellationToken cancellationToken, out ISymbol declaredSymbol) + public static IMethodSymbol? GetInvokeMethod(this SemanticModel semanticModel, DelegateDeclarationSyntax declarationSyntax, CancellationToken cancellationToken, out ISymbol? declaredSymbol) { var @delegate = semanticModel.GetDeclaredSymbol(declarationSyntax, cancellationToken); declaredSymbol = @delegate; - return @delegate.DelegateInvokeMethod; + return @delegate?.DelegateInvokeMethod; } } } diff --git a/src/Particular.Analyzers/Extensions/SymbolExtensions.cs b/src/Particular.Analyzers/Extensions/SymbolExtensions.cs index aecc711..9e873b8 100644 --- a/src/Particular.Analyzers/Extensions/SymbolExtensions.cs +++ b/src/Particular.Analyzers/Extensions/SymbolExtensions.cs @@ -4,23 +4,17 @@ public static class SymbolExtensions { - public static IMethodSymbol GetMethodOrDefault(this ISymbol symbol) + public static IMethodSymbol? GetMethodOrDefault(this ISymbol symbol) { - switch (symbol) + return symbol switch { - case IFieldSymbol field when field.Type is INamedTypeSymbol type: - return type.DelegateInvokeMethod; - case ILocalSymbol local when local.Type is INamedTypeSymbol type: - return type.DelegateInvokeMethod; - case IMethodSymbol method: - return method; - case IParameterSymbol param when param.Type is INamedTypeSymbol type: - return type.DelegateInvokeMethod; - case IPropertySymbol property when property.Type is INamedTypeSymbol type: - return type.DelegateInvokeMethod; - default: - return null; - } + IFieldSymbol field when field.Type is INamedTypeSymbol type => type.DelegateInvokeMethod, + ILocalSymbol local when local.Type is INamedTypeSymbol type => type.DelegateInvokeMethod, + IMethodSymbol method => method, + IParameterSymbol param when param.Type is INamedTypeSymbol type => type.DelegateInvokeMethod, + IPropertySymbol property when property.Type is INamedTypeSymbol type => type.DelegateInvokeMethod, + _ => null, + }; } } } diff --git a/src/Particular.Analyzers/Extensions/TypeSymbolExtensions.cs b/src/Particular.Analyzers/Extensions/TypeSymbolExtensions.cs index 92ea6b2..8a37e8d 100644 --- a/src/Particular.Analyzers/Extensions/TypeSymbolExtensions.cs +++ b/src/Particular.Analyzers/Extensions/TypeSymbolExtensions.cs @@ -6,10 +6,10 @@ public static class TypeSymbolExtensions { - public static bool IsCancellationToken(this ITypeSymbol type) => + public static bool IsCancellationToken(this ITypeSymbol? type) => type?.ToString() == "System.Threading.CancellationToken"; - public static bool IsCancellableContext(this ITypeSymbol type) + public static bool IsCancellableContext(this ITypeSymbol? type) { if (type == null) { @@ -32,23 +32,23 @@ public static bool IsCancellableContext(this ITypeSymbol type) static bool IsCancellableContext(string type) => type == "NServiceBus.ICancellableContext"; - public static bool IsTask(this ITypeSymbol type) => + public static bool IsTask(this ITypeSymbol? type) => type != null && (IsTask(type.ToString()) || type.BaseType.IsTask()); - static bool IsTask(string type) => + static bool IsTask(string? type) => type != null && (type == "System.Threading.Tasks.Task" || type == "System.Threading.Tasks.ValueTask" || type.StartsWith("System.Threading.Tasks.ValueTask<", StringComparison.Ordinal)); - public static bool IsConfiguredTaskAwaitable(this ITypeSymbol type) => + public static bool IsConfiguredTaskAwaitable(this ITypeSymbol? type) => type != null && IsConfiguredTaskAwaitable(type.ToString()); static bool IsConfiguredTaskAwaitable(string type) => type == "System.Runtime.CompilerServices.ConfiguredTaskAwaitable" || type.StartsWith("System.Runtime.CompilerServices.ConfiguredTaskAwaitable<", StringComparison.Ordinal); - public static bool IsFunc(this INamedTypeSymbol type) => + public static bool IsFunc(this INamedTypeSymbol? type) => type != null && type.TypeArguments.Length > 0 && type.ToString().StartsWith("System.Func<", StringComparison.Ordinal); } } diff --git a/src/Particular.Analyzers/LoggingAnalyzer.cs b/src/Particular.Analyzers/LoggingAnalyzer.cs index 574f5e4..3ab0044 100644 --- a/src/Particular.Analyzers/LoggingAnalyzer.cs +++ b/src/Particular.Analyzers/LoggingAnalyzer.cs @@ -46,17 +46,17 @@ public override void Initialize(AnalysisContext context) static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) { - if (!(context.Node is InvocationExpressionSyntax invocation)) + if (context.Node is not InvocationExpressionSyntax invocation) { return; } - if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess)) + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) { return; } - if (!(memberAccess.Name is IdentifierNameSyntax identifierName)) + if (memberAccess.Name is not IdentifierNameSyntax identifierName) { return; } @@ -82,12 +82,12 @@ static bool Analyze(SyntaxNodeAnalysisContext context, InvocationExpressionSynta { var loggerSymbol = context.SemanticModel.GetSymbolInfo(invocationSyntax, context.CancellationToken).Symbol; - if (!(loggerSymbol.GetMethodOrDefault() is IMethodSymbol method)) + if (loggerSymbol is null || loggerSymbol.GetMethodOrDefault() is not IMethodSymbol method) { return false; } - if (method.ReceiverType.ToString() != loggerFullName) + if (method is null || method.ReceiverType?.ToString() != loggerFullName) { return false; } @@ -100,10 +100,10 @@ static bool Analyze(SyntaxNodeAnalysisContext context, InvocationExpressionSynta var p = method.Parameters[i]; if (formatIndex < 0) { - if (p.Name == "message" || p.Name == "format") + if (p.Name is "message" or "format") { var typeString = p.Type.ToString(); - if (typeString == "string" || typeString == "string?") + if (typeString is "string" or "string?") { formatIndex = i; } @@ -114,7 +114,7 @@ static bool Analyze(SyntaxNodeAnalysisContext context, InvocationExpressionSynta if (p.Type is IArrayTypeSymbol arrayType) { var elementType = arrayType.ElementType.ToString(); - if (elementType == "object" || elementType == "object?") + if (elementType is "object" or "object?") { argsIndex = i; break; diff --git a/src/Particular.Analyzers/Particular.Analyzers.csproj b/src/Particular.Analyzers/Particular.Analyzers.csproj index cf4ecc9..4239879 100644 --- a/src/Particular.Analyzers/Particular.Analyzers.csproj +++ b/src/Particular.Analyzers/Particular.Analyzers.csproj @@ -3,6 +3,8 @@ netstandard2.0 true + 12.0 + enable @@ -14,12 +16,12 @@ - + - + diff --git a/src/Particular.Analyzers/TypeNameAnalyzer.cs b/src/Particular.Analyzers/TypeNameAnalyzer.cs index d768366..b8049c5 100644 --- a/src/Particular.Analyzers/TypeNameAnalyzer.cs +++ b/src/Particular.Analyzers/TypeNameAnalyzer.cs @@ -28,7 +28,7 @@ public override void Initialize(AnalysisContext context) static void Analyze(SyntaxNodeAnalysisContext context) { - if (!(context.Node is MemberDeclarationSyntax type)) + if (context.Node is not MemberDeclarationSyntax type) { return; }