Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 110 additions & 12 deletions TUnit.Analyzers.Tests/TimeoutCancellationTokenAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ await Verifier.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Threading.Tasks;

public class TestClass
{
[Test]
Expand All @@ -35,7 +35,7 @@ await Verifier.VerifyAnalyzerAsync(
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

public class TestClass
{
[Test]
Expand All @@ -56,7 +56,7 @@ await Verifier.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Threading.Tasks;

[Timeout(30_000)]
public class TestClass
{
Expand All @@ -80,7 +80,7 @@ await Verifier.VerifyAnalyzerAsync(
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

[Timeout(30_000)]
public class TestClass
{
Expand All @@ -103,12 +103,12 @@ await Verifier.VerifyAnalyzerAsync(
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

[Timeout(30_000)]
public class TestClass
{
private static HttpClient GetHttpClient() => new HttpClient();

[Test]
public async Task TestMethod(CancellationToken cancellationToken)
{
Expand All @@ -128,15 +128,15 @@ await Verifier.VerifyAnalyzerAsync(
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

[Timeout(30_000)]
public class TestClass
{
internal void HelperMethod()
{
// Some helper logic
}

[Test]
public async Task TestMethod(CancellationToken cancellationToken)
{
Expand All @@ -156,15 +156,15 @@ await Verifier.VerifyAnalyzerAsync(
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

[Timeout(30_000)]
public class TestClass
{
private async Task DoSomethingAsync()
{
await Task.Delay(100);
}

[Test]
public async Task TestMethod(CancellationToken cancellationToken)
{
Expand All @@ -183,7 +183,7 @@ await Verifier.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Threading.Tasks;

public class TestClass
{
// This shouldn't happen in practice as Timeout should only be on test/hook methods,
Expand All @@ -193,7 +193,7 @@ private async Task HelperMethodWithTimeout()
{
await Task.Delay(100);
}

[Test]
public async Task TestMethod()
{
Expand All @@ -203,4 +203,102 @@ public async Task TestMethod()
"""
);
}

[Test]
public async Task Test_Method_With_CancellationToken_Not_Last_Shows_Wrong_Order_Error()
{
await Verifier.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

public class TestClass
{
[Test]
[Arguments(1)]
[Timeout(30_000)]
public async Task {|#0:TestMethod|}(CancellationToken cancellationToken, int value)
{
await Task.Delay(100, cancellationToken);
}
}
""",
Verifier.Diagnostic(Rules.CancellationTokenMustBeLastParameter)
.WithLocation(0)
);
}

[Test]
public async Task Test_Method_With_Data_And_CancellationToken_Last_Shows_No_Error()
{
await Verifier.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

public class TestClass
{
[Test]
[Arguments(1)]
[Timeout(30_000)]
public async Task TestMethod(int value, CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
}
}
"""
);
}

[Test]
public async Task Test_Method_With_CancellationToken_First_Of_Multiple_Shows_Wrong_Order_Error()
{
await Verifier.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

public class TestClass
{
[Test]
[Arguments(1, "hello")]
[Timeout(30_000)]
public async Task {|#0:TestMethod|}(CancellationToken cancellationToken, int value, string text)
{
await Task.Delay(100, cancellationToken);
}
}
""",
Verifier.Diagnostic(Rules.CancellationTokenMustBeLastParameter)
.WithLocation(0)
);
}

[Test]
public async Task Test_Method_With_CancellationToken_In_Middle_Shows_Wrong_Order_Error()
{
await Verifier.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System.Threading;
using System.Threading.Tasks;

public class TestClass
{
[Test]
[Arguments(1, "hello")]
[Timeout(30_000)]
public async Task {|#0:TestMethod|}(int value, CancellationToken cancellationToken, string text)
{
await Task.Delay(100, cancellationToken);
}
}
""",
Verifier.Diagnostic(Rules.CancellationTokenMustBeLastParameter)
.WithLocation(0)
);
}
}
1 change: 1 addition & 0 deletions TUnit.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
TUnit0061 | Usage | Error | ClassDataSource type requires parameterless constructor
TUnit0062 | Usage | Warning | CancellationToken must be the last parameter

### Removed Rules

Expand Down
9 changes: 9 additions & 0 deletions TUnit.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,15 @@
<data name="TUnit0072Title" xml:space="preserve">
<value>Conflicting data source attributes</value>
</data>
<data name="TUnit0062Description" xml:space="preserve">
<value>CancellationToken parameter must be the last parameter in the method signature. Move it to the end of the parameter list.</value>
</data>
<data name="TUnit0062MessageFormat" xml:space="preserve">
<value>CancellationToken must be the last parameter</value>
</data>
<data name="TUnit0062Title" xml:space="preserve">
<value>CancellationToken must be the last parameter</value>
</data>
<data name="TUnit0300Description" xml:space="preserve">
<value>Generic types and methods may not be AOT-compatible when using dynamic type creation. Consider using concrete types or ensure all generic combinations are known at compile time.</value>
</data>
Expand Down
3 changes: 3 additions & 0 deletions TUnit.Analyzers/Rules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public static class Rules
public static readonly DiagnosticDescriptor MissingTimeoutCancellationTokenAttributes =
CreateDescriptor("TUnit0015", UsageCategory, DiagnosticSeverity.Warning);

public static readonly DiagnosticDescriptor CancellationTokenMustBeLastParameter =
CreateDescriptor("TUnit0062", UsageCategory, DiagnosticSeverity.Warning);

public static readonly DiagnosticDescriptor MethodMustNotBeStatic =
CreateDescriptor("TUnit0016", UsageCategory, DiagnosticSeverity.Error);

Expand Down
30 changes: 25 additions & 5 deletions TUnit.Analyzers/TimeoutCancellationTokenAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ namespace TUnit.Analyzers;
public class TimeoutCancellationTokenAnalyzer : ConcurrentDiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(Rules.MissingTimeoutCancellationTokenAttributes);
ImmutableArray.Create(
Rules.MissingTimeoutCancellationTokenAttributes,
Rules.CancellationTokenMustBeLastParameter);

protected override void InitializeInternal(AnalysisContext context)
{
Expand All @@ -23,7 +25,7 @@ private void AnalyzeSymbol(SymbolAnalysisContext context)
return;
}

if (!methodSymbol.IsTestMethod(context.Compilation) &&
if (!methodSymbol.IsTestMethod(context.Compilation) &&
!methodSymbol.IsHookMethod(context.Compilation, out _, out _, out _))
{
return;
Expand Down Expand Up @@ -51,15 +53,33 @@ private void AnalyzeSymbol(SymbolAnalysisContext context)
return;
}

var lastParameter = parameters.Last();
var cancellationTokenType = context.Compilation.GetTypeByMetadataName(typeof(CancellationToken).FullName!);

if (!SymbolEqualityComparer.Default.Equals(lastParameter.Type,
context.Compilation.GetTypeByMetadataName(typeof(CancellationToken).FullName!)))
var cancellationTokenIndex = -1;
for (var i = 0; i < parameters.Length; i++)
{
if (SymbolEqualityComparer.Default.Equals(parameters[i].Type, cancellationTokenType))
{
cancellationTokenIndex = i;
break;
}
}

if (cancellationTokenIndex == -1)
{
// CancellationToken is not present at all
context.ReportDiagnostic(
Diagnostic.Create(Rules.MissingTimeoutCancellationTokenAttributes,
context.Symbol.Locations.FirstOrDefault())
);
}
else if (cancellationTokenIndex != parameters.Length - 1)
{
// CancellationToken exists but is not the last parameter
context.ReportDiagnostic(
Diagnostic.Create(Rules.CancellationTokenMustBeLastParameter,
context.Symbol.Locations.FirstOrDefault())
);
}
}
}
Loading