diff --git a/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs b/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs index 8b45e271..83d22e8b 100644 --- a/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs +++ b/src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading; using NUnit.Analyzers.Constants; using NUnit.Framework; using NUnit.Framework.Constraints; @@ -164,6 +165,10 @@ public sealed class NUnitFrameworkConstantsTests (nameof(NUnitFrameworkConstants.NameOfSetUpAttribute), nameof(SetUpAttribute)), (nameof(NUnitFrameworkConstants.NameOfTearDownAttribute), nameof(TearDownAttribute)), +#if NUNIT4 + (nameof(NUnitFrameworkConstants.NameOfCancelAfterAttribute), nameof(CancelAfterAttribute)), +#endif + (nameof(NUnitFrameworkConstants.NameOfExpectedResult), nameof(TestAttribute.ExpectedResult)), (nameof(NUnitFrameworkConstants.NameOfConstraintExpressionAnd), nameof(EqualConstraint.And)), @@ -201,6 +206,11 @@ public sealed class NUnitFrameworkConstantsTests (nameof(NUnitFrameworkConstants.FullNameOfFixtureLifeCycleAttribute), typeof(FixtureLifeCycleAttribute)), (nameof(NUnitFrameworkConstants.FullNameOfLifeCycle), typeof(LifeCycle)), +#if NUNIT4 + (nameof(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute), typeof(CancelAfterAttribute)), + (nameof(NUnitFrameworkConstants.FullNameOfCancellationToken), typeof(CancellationToken)), +#endif + (nameof(NUnitFrameworkConstants.FullNameOfSameAsConstraint), typeof(SameAsConstraint)), (nameof(NUnitFrameworkConstants.FullNameOfSomeItemsConstraint), typeof(SomeItemsConstraint)), (nameof(NUnitFrameworkConstants.FullNameOfEqualToConstraint), typeof(EqualConstraint)), diff --git a/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs b/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs index 66794356..c742cf2d 100644 --- a/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs +++ b/src/nunit.analyzers.tests/Extensions/IMethodSymbolExtensionsTests.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Analyzers.Constants; using NUnit.Analyzers.Extensions; using NUnit.Framework; @@ -22,8 +23,8 @@ public sealed class IMethodSymbolExtensionsTestsGetParameterCounts public void Foo(int a1, int a2, int a3, string b1 = ""b1"", string b2 = ""b2"", params char[] c) { } } }"; - var method = await GetMethodSymbolAsync(testCode).ConfigureAwait(false); - var (requiredParameters, optionalParameters, paramsCount) = method.GetParameterCounts(); + var (method, _) = await GetMethodSymbolAsync(testCode).ConfigureAwait(false); + var (requiredParameters, optionalParameters, paramsCount) = method.GetParameterCounts(false, null); Assert.Multiple(() => { @@ -33,18 +34,45 @@ public void Foo(int a1, int a2, int a3, string b1 = ""b1"", string b2 = ""b2"", }); } - private static async Task GetMethodSymbolAsync(string code) + [Test] + public async Task GetParameterCountsWithCancellationToken([Values] bool hasCancelAfter) + { + var testCode = @" +using System.Threading; + +namespace NUnit.Analyzers.Tests.Targets.Extensions +{ + public sealed class IMethodSymbolExtensionsTestsGetParameterCounts + { + public void Foo(int a1, int a2, int a3, CancellationToken cancellationToken) { } + } +}"; + var (method, compilation) = await GetMethodSymbolAsync(testCode).ConfigureAwait(false); + INamedTypeSymbol? cancellationTokenType = compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + var (requiredParameters, optionalParameters, paramsCount) = method.GetParameterCounts(hasCancelAfter, cancellationTokenType); + int adjustment = hasCancelAfter ? 0 : 1; + + Assert.Multiple(() => + { + Assert.That(requiredParameters, Is.EqualTo(3 + adjustment), nameof(requiredParameters)); + Assert.That(optionalParameters, Is.EqualTo(1 - adjustment), nameof(optionalParameters)); + Assert.That(paramsCount, Is.EqualTo(0), nameof(paramsCount)); + }); + } + + private static async Task<(IMethodSymbol MethodSymbol, Compilation Compilation)> GetMethodSymbolAsync(string code) { - var rootAndModel = await TestHelpers.GetRootAndModel(code).ConfigureAwait(false); + var rootCompilationAndModel = await TestHelpers.GetRootCompilationAndModel(code).ConfigureAwait(false); - MethodDeclarationSyntax methodDeclaration = rootAndModel.Node + MethodDeclarationSyntax methodDeclaration = rootCompilationAndModel.Node .DescendantNodes().OfType().Single() .DescendantNodes().OfType().Single(); - IMethodSymbol? methodSymbol = rootAndModel.Model.GetDeclaredSymbol(methodDeclaration); + IMethodSymbol? methodSymbol = rootCompilationAndModel.Model.GetDeclaredSymbol(methodDeclaration); Assert.That(methodSymbol, Is.Not.Null, $"Cannot find symbol for {methodDeclaration.Identifier}"); - return methodSymbol; + return (methodSymbol!, rootCompilationAndModel.Compilation); } } } diff --git a/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs b/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs index b046e8d0..a3611ce1 100644 --- a/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzerTests.cs @@ -640,7 +640,7 @@ public void AnalyzeWhenNumberOfParametersOfTestIsNotEvidentFromTestSource() [TestFixture] public class AnalyzeWhenNumberOfParametersOfTestIsNotEvidentFromTestSource { - [Explicit(""The code is wrong, but it is too complext for the analyzer to detect this."")] + [Explicit(""The code is wrong, but it is too complex for the analyzer to detect this."")] [TestCaseSource(nameof(TestData))] public void ShortName(int n) { @@ -705,5 +705,63 @@ public void ShortName(int first, int second) RoslynAssert.Valid(analyzer, testCode); } + +#if NUNIT4 + [Test] + public void AnalyzeWhenNumberOfParametersMatchExcludingImplicitSuppliedCancellationToken() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public class AnalyzeWhenNumberOfParametersMatch + { + [TestCaseSource(nameof(TestData), new object[] { 1, 3, 5 })] + [CancelAfter(10)] + public void ShortName(int number, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Assert.Ignore(""Cancelled""); + Assert.That(number, Is.GreaterThanOrEqualTo(0)); + } + + static IEnumerable TestData(int first, int second, int third) + { + yield return first; + yield return second; + yield return third; + } + }", additionalUsings: "using System.Collections.Generic;using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void AnalyzeWhenNumberOfParametersDoesNotMatchNoParametersExpectedNoImplicitSuppliedCancellationToken() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + [TestFixture] + public class AnalyzeWhenNumberOfParametersDoesNotMatchNoParametersExpected + { + [TestCaseSource(↓nameof(TestData), new object[] { 1 })] + public void ShortName(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Assert.Ignore(""Cancelled""); + } + + static IEnumerable TestData() + { + yield return 1; + yield return 2; + yield return 3; + } + }", additionalUsings: "using System.Collections.Generic;using System.Threading;"); + + var expectedDiagnostic = ExpectedDiagnostic + .Create(AnalyzerIdentifiers.TestCaseSourceMismatchInNumberOfParameters) + .WithMessage("The TestCaseSource provides '1' parameter(s), but the target method expects '0' parameter(s)"); + RoslynAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + +#endif } } diff --git a/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs index 3daab0fb..d41a5fbb 100644 --- a/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestCaseUsage/TestCaseUsageAnalyzerTests.cs @@ -755,5 +755,60 @@ public void TestWithGenericParameter(T arg1) { } }"); RoslynAssert.Valid(this.analyzer, testCode); } + +#if NUNIT4 + [Test] + public void AnalyzeWhenTestMethodHasImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [TestCase(100)] + [CancelAfter(50)] + public async Task InfiniteLoopWithCancelAfter(int delayInMs, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(this.analyzer, testCode); + } + + [Test] + public void AnalyzeWhenTestMethodHasNoImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [TestCase(100)] + [CancelAfter(50)] + public async Task InfiniteLoopWith50msCancelAfter(int delayInMs) + { + CancellationToken cancellationToken = TestContext.CurrentContext.CancellationToken; + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(this.analyzer, testCode); + } + + [Test] + public void WhenTestMethodHasCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [↓TestCase(100)] + public async Task InfiniteLoopWith50msCancelAfter(int delayInMs, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(delayInMs, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Diagnostics(this.analyzer, + ExpectedDiagnostic.Create(AnalyzerIdentifiers.TestCaseNotEnoughArgumentsUsage), + testCode); + } +#endif } } diff --git a/src/nunit.analyzers.tests/TestHelpers.cs b/src/nunit.analyzers.tests/TestHelpers.cs index 13b7bf31..82d83bc9 100644 --- a/src/nunit.analyzers.tests/TestHelpers.cs +++ b/src/nunit.analyzers.tests/TestHelpers.cs @@ -56,7 +56,7 @@ internal static Task NotSuppressed(DiagnosticAnalyzer analyzer, DiagnosticSuppre internal static Task Suppressed(DiagnosticAnalyzer analyzer, DiagnosticSuppressor suppressor, string code, Settings? settings = null) => SuppressedOrNot(analyzer, suppressor, code, true, settings); - internal static async Task<(SyntaxNode Node, SemanticModel Model)> GetRootAndModel(string code) + internal static (SyntaxTree Tree, Compilation Compilation) GetTreeAndCompilation(string code) { var tree = CSharpSyntaxTree.ParseText(code); @@ -65,10 +65,23 @@ internal static Task Suppressed(DiagnosticAnalyzer analyzer, DiagnosticSuppresso references: Settings.Default.MetadataReferences, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + return (tree, compilation); + } + + internal static async Task<(SyntaxNode Node, Compilation Compilation, SemanticModel Model)> GetRootCompilationAndModel(string code) + { + (SyntaxTree tree, Compilation compilation) = GetTreeAndCompilation(code); var model = compilation.GetSemanticModel(tree); var root = await tree.GetRootAsync().ConfigureAwait(false); - return (root, model); + return (root, compilation, model); + } + + internal static async Task<(SyntaxNode Node, SemanticModel Model)> GetRootAndModel(string code) + { + (SyntaxNode node, _, SemanticModel model) = await GetRootCompilationAndModel(code).ConfigureAwait(false); + + return (node, model); } } } diff --git a/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs index 02fabcd5..84cc758c 100644 --- a/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/TestMethodUsage/TestMethodUsageAnalyzerTests.cs @@ -536,6 +536,79 @@ public void M(int p) RoslynAssert.Diagnostics(analyzer, expectedDiagnostic.WithMessage(message), testCode); } +#if NUNIT4 + [Test] + public void WhenTestMethodHasImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [Test] + [CancelAfter(50)] + public async Task InfiniteLoopWith50msCancelAfter(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void AnalyzeWhenSimpleTestMethodHasParameterSuppliedByValuesAndCancelAfterAttributes() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [Test] + [CancelAfter(10)] + public void M([Values(1, 2, 3)] int p, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + Assert.Ignore(""Cancelled""); + Assert.That(p, Is.EqualTo(42)); + }", "using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void WhenTestMethodHasNoImplicitlySuppliedCancellationTokenParameter() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [Test] + [CancelAfter(50)] + public async Task InfiniteLoopWith50msCancelAfter() + { + CancellationToken cancellationToken = TestContext.CurrentContext.CancellationToken; + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + RoslynAssert.Valid(analyzer, testCode); + } + + [Test] + public void WhenTestMethodHasCancellationTokenParameter() + { + var expectedDiagnostic = ExpectedDiagnostic.Create( + AnalyzerIdentifiers.SimpleTestMethodHasParameters); + + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@" + [↓Test] + public async Task InfiniteLoopWith50msCancelAfter(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + }", "using System.Threading;"); + + var message = "The test method has '1' parameter(s), but only '0' argument(s) are supplied by attributes"; + RoslynAssert.Diagnostics(analyzer, expectedDiagnostic.WithMessage(message), testCode); + } +#endif + [Test] public void AnalyzeWhenSimpleTestMethodHasParameterWithOutSuppliedValues() { diff --git a/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj b/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj index 05a3809e..52759a12 100644 --- a/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj +++ b/src/nunit.analyzers.tests/nunit.analyzers.tests.csproj @@ -2,7 +2,7 @@ NUnit.Analyzers.Tests net6.0;net462 - 3 + 4 diff --git a/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs b/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs index 9e7999fe..b029f4cd 100644 --- a/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs +++ b/src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs @@ -149,6 +149,9 @@ public static class NUnitFrameworkConstants public const string FullNameOfFixtureLifeCycleAttribute = "NUnit.Framework.FixtureLifeCycleAttribute"; public const string FullNameOfLifeCycle = "NUnit.Framework.LifeCycle"; + public const string FullNameOfCancelAfterAttribute = "NUnit.Framework.CancelAfterAttribute"; + public const string FullNameOfCancellationToken = "System.Threading.CancellationToken"; + public const string NameOfConstraint = "Constraint"; public const string FullNameOfSameAsConstraint = "NUnit.Framework.Constraints.SameAsConstraint"; @@ -180,6 +183,8 @@ public static class NUnitFrameworkConstants public const string NameOfSetUpAttribute = "SetUpAttribute"; public const string NameOfTearDownAttribute = "TearDownAttribute"; + public const string NameOfCancelAfterAttribute = "CancelAfterAttribute"; + public const string NameOfExpectedResult = "ExpectedResult"; public const string NameOfActualParameter = "actual"; diff --git a/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs b/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs index c49c4755..6d35c273 100644 --- a/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs +++ b/src/nunit.analyzers/Extensions/IMethodSymbolExtensions.cs @@ -15,7 +15,9 @@ internal static class IMethodSymbolExtensions /// and the last is the count. /// internal static (uint requiredParameters, uint optionalParameters, uint paramsCount) GetParameterCounts( - this IMethodSymbol @this) + this IMethodSymbol @this, + bool hasCancelAfterAttribute, + INamedTypeSymbol? cancellationTokenType) { var parameters = @this.Parameters; @@ -39,6 +41,15 @@ internal static (uint requiredParameters, uint optionalParameters, uint paramsCo } } + var hasCancellationToken = parameters.Length > 0 && + SymbolEqualityComparer.Default.Equals(parameters[parameters.Length - 1].Type, cancellationTokenType); + if (hasCancelAfterAttribute && hasCancellationToken) + { + // This parameter is optional, it not specified it will be supplied by NUnit. + optionalParameters++; + requiredParameters--; + } + return (requiredParameters, optionalParameters, paramsParameters); } diff --git a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs index 6a0b554d..a76d54ef 100644 --- a/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs +++ b/src/nunit.analyzers/TestCaseSourceUsage/TestCaseSourceUsesStringAnalyzer.cs @@ -121,10 +121,17 @@ private static void AnalyzeCompilationStart(CompilationStartAnalysisContext cont return; } - context.RegisterSyntaxNodeAction(syntaxContext => AnalyzeAttribute(syntaxContext, testCaseSourceAttribute), SyntaxKind.Attribute); + INamedTypeSymbol? cancelAfterType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute); + INamedTypeSymbol? cancellationTokenType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + context.RegisterSyntaxNodeAction(syntaxContext => AnalyzeAttribute(syntaxContext, testCaseSourceAttribute, cancelAfterType, cancellationTokenType), SyntaxKind.Attribute); } - private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTypeSymbol testCaseSourceAttribute) + private static void AnalyzeAttribute( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol testCaseSourceAttribute, + INamedTypeSymbol? cancelAfterType, + INamedTypeSymbol? cancellationTokenType) { var attributeInfo = SourceHelpers.GetSourceAttributeInformation( context, @@ -238,7 +245,9 @@ private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTy IMethodSymbol? testMethod = context.SemanticModel.GetDeclaredSymbol(testMethodDeclaration); if (testMethod is not null) { - var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = testMethod.GetParameterCounts(); + var hasCancelAfterAttribute = testMethod.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + + var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = testMethod.GetParameterCounts(true, cancellationTokenType); if (elementType.SpecialType != SpecialType.System_String && (elementType.SpecialType == SpecialType.System_Object || elementType.IsIEnumerable(out _) || IsOrDerivesFrom(elementType, context.SemanticModel.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfTypeTestCaseParameters)))) @@ -258,7 +267,7 @@ private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context, INamedTy } else { - if (methodRequiredParameters + methodOptionalParameters != 1) + if (methodRequiredParameters + methodOptionalParameters != (hasCancelAfterAttribute ? 2 : 1)) { context.ReportDiagnostic(Diagnostic.Create( mismatchInNumberOfTestMethodParameters, diff --git a/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs b/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs index 9fbb34bf..80cf63c6 100644 --- a/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs +++ b/src/nunit.analyzers/TestCaseUsage/TestCaseUsageAnalyzer.cs @@ -52,17 +52,26 @@ private static void AnalyzeCompilationStart(CompilationStartAnalysisContext cont if (testCaseType is null) return; - context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType), SymbolKind.Method); + INamedTypeSymbol? cancelAfterType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute); + INamedTypeSymbol? cancellationTokenType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType, cancelAfterType, cancellationTokenType), SymbolKind.Method); } - private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol testCaseType) + private static void AnalyzeMethod( + SymbolAnalysisContext context, + INamedTypeSymbol testCaseType, + INamedTypeSymbol? cancelAfterType, + INamedTypeSymbol? cancellationTokenType) { var methodSymbol = (IMethodSymbol)context.Symbol; - var attributes = methodSymbol.GetAttributes(); - if (attributes.Length == 0) + var methodAttributes = methodSymbol.GetAttributes(); + if (methodAttributes.Length == 0) return; + var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + var testCaseAttributes = methodSymbol.GetAttributes() .Where(a => a.ApplicationSyntaxReference is not null && SymbolEqualityComparer.Default.Equals(a.AttributeClass, testCaseType)); @@ -72,7 +81,7 @@ private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbo context.CancellationToken.ThrowIfCancellationRequested(); var (methodRequiredParameters, methodOptionalParameters, methodParamsParameters) = - methodSymbol.GetParameterCounts(); + methodSymbol.GetParameterCounts(hasCancelAfterAttribute, cancellationTokenType); var attributePositionalArguments = attribute.ConstructorArguments.AdjustArguments(); diff --git a/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs b/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs index 18c4ce54..5f1c610d 100644 --- a/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs +++ b/src/nunit.analyzers/TestMethodUsage/TestMethodUsageAnalyzer.cs @@ -89,50 +89,64 @@ private static void AnalyzeCompilationStart(CompilationStartAnalysisContext cont if (testCaseType is null || testType is null) return; - context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType, testType), SymbolKind.Method); + INamedTypeSymbol? cancelAfterType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute); + INamedTypeSymbol? cancellationTokenType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfCancellationToken); + + context.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext, testCaseType, testType, cancelAfterType, cancellationTokenType), SymbolKind.Method); } - private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol testCaseType, INamedTypeSymbol testType) + private static void AnalyzeMethod( + SymbolAnalysisContext context, + INamedTypeSymbol testCaseType, + INamedTypeSymbol testType, + INamedTypeSymbol? cancelAfterType, + INamedTypeSymbol? cancellationTokenType) { var methodSymbol = (IMethodSymbol)context.Symbol; var methodAttributes = methodSymbol.GetAttributes(); - foreach (var attribute in methodAttributes) + // Check Expected Result for TestCases (should this be moved to TestCaseUsageAnalyzer) + foreach (var testCaseAttribute in methodAttributes.Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, testCaseType))) { - if (attribute.AttributeClass is null) - continue; + context.CancellationToken.ThrowIfCancellationRequested(); + + AnalyzeExpectedResult(context, testCaseAttribute, methodSymbol); + } - var isTestCaseAttribute = SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, testCaseType); - var isTestAttribute = SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, testType); + var hasITestBuilderAttribute = HasITestBuilderAttribute(context.Compilation, methodAttributes); - if (isTestCaseAttribute - || (isTestAttribute && !HasITestBuilderAttribute(context.Compilation, methodAttributes))) + if (!hasITestBuilderAttribute) + { + var testAttribute = methodAttributes.SingleOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, testType)); + var hasCancelAfterAttribute = methodAttributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cancelAfterType)); + + if (testAttribute is not null) { context.CancellationToken.ThrowIfCancellationRequested(); - AnalyzeExpectedResult(context, attribute, methodSymbol); + AnalyzeExpectedResult(context, testAttribute, methodSymbol); } - var isSimpleTestBulderAttribute = attribute.DerivesFromISimpleTestBuilder(context.Compilation); + var simpleTestBuilderAttribute = methodAttributes.FirstOrDefault(a => a.DerivesFromISimpleTestBuilder(context.Compilation)); - if (isSimpleTestBulderAttribute) + if (simpleTestBuilderAttribute is not null) { var parameters = methodSymbol.Parameters; var testMethodParameters = parameters.Length; - var hasITestBuilderAttribute = HasITestBuilderAttribute(context.Compilation, methodAttributes); var parametersMarkedWithIParameterDataSourceAttribute = parameters.Count(p => HasIParameterDataSourceAttribute(context.Compilation, p.GetAttributes())); - - if (testMethodParameters > 0 && - !hasITestBuilderAttribute && - parametersMarkedWithIParameterDataSourceAttribute < testMethodParameters) + var hasCancellationToken = parameters.Length > 0 && + SymbolEqualityComparer.Default.Equals(parameters[parameters.Length - 1].Type, cancellationTokenType); + int implicitParameters = hasCancelAfterAttribute && hasCancellationToken ? 1 : 0; + if (testMethodParameters > implicitParameters && + parametersMarkedWithIParameterDataSourceAttribute + implicitParameters < testMethodParameters) { context.ReportDiagnostic(Diagnostic.Create( simpleTestHasParameters, - attribute.ApplicationSyntaxReference.GetLocation(), + simpleTestBuilderAttribute.ApplicationSyntaxReference.GetLocation(), testMethodParameters, - parametersMarkedWithIParameterDataSourceAttribute)); + parametersMarkedWithIParameterDataSourceAttribute + implicitParameters)); } } }