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
11 changes: 11 additions & 0 deletions docs/docs/reference/tunit0077.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# TUnit0077: Span params parameters are not supported on tests

**Severity:** Error

TUnit reports this diagnostic when a test method declares a `params ReadOnlySpan<T>` or `params Span<T>` parameter, including generic test methods fed by typed data rows.

Spans cannot be boxed into the argument array used by reflection invocation. TUnit therefore does not support span params parameters in either execution mode.

Change the parameter to `params T[]`. For example, replace `params ReadOnlySpan<string> rest` with `params string[] rest`. Array params support packing the trailing elements of a typed data row in both source-generated and reflection modes.

The rule applies to test methods regardless of their data source. Ordinary helper methods can still use span params.
1 change: 1 addition & 0 deletions docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ const sidebars: SidebarsConfig = {
'reference/test-configuration',
'reference/tunit0075',
'reference/tunit0076',
'reference/tunit0077',
{
type: 'doc',
id: 'guides/best-practices',
Expand Down
1 change: 1 addition & 0 deletions src/TUnit.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ TUnit0073 | Usage | Error | Missing polyfill types required by TUnit
TUnit0074 | Usage | Error | Hook attribute is redundant on an override
TUnit0075 | Usage | Warning | Use the hook cancellation token for setup
TUnit0076 | Usage | Error | Data source Arguments fit more than one overload equally well
TUnit0077 | Usage | Error | Span params parameters are not supported on tests
Comment thread
thomhurst marked this conversation as resolved.
Resolved

### Removed Rules

Expand Down
1 change: 1 addition & 0 deletions src/TUnit.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ public static class DiagnosticIds
public const string RedundantHookAttributeOnOverride = "TUnit0074";
public const string UseHookCancellationToken = "TUnit0075";
public const string AmbiguousMethodDataSourceOverload = "TUnit0076";
public const string UnsupportedSpanParams = "TUnit0077";
}
9 changes: 9 additions & 0 deletions src/TUnit.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -552,4 +552,13 @@
<data name="TUnit0076Title" xml:space="preserve">
<value>Data source Arguments fit more than one overload equally well</value>
</data>
<data name="TUnit0077Title" xml:space="preserve">
<value>Span params parameters are not supported on tests</value>
</data>
<data name="TUnit0077MessageFormat" xml:space="preserve">
<value>Test parameter '{0}' uses span params, which TUnit cannot invoke consistently in source-generated and reflection modes. Use a params array instead</value>
</data>
<data name="TUnit0077Description" xml:space="preserve">
<value>Span&lt;T&gt; and ReadOnlySpan&lt;T&gt; cannot be boxed for reflection invocation. Use params T[] for test parameters so typed data rows can be packed and invoked in both execution modes.</value>
</data>
</root>
3 changes: 3 additions & 0 deletions src/TUnit.Analyzers/Rules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ public static class Rules
{
private const string UsageCategory = "Usage";

public static readonly DiagnosticDescriptor UnsupportedSpanParams =
CreateDescriptor(DiagnosticIds.UnsupportedSpanParams, UsageCategory, DiagnosticSeverity.Error);

public static readonly DiagnosticDescriptor WrongArgumentTypeTestData =
CreateDescriptor(DiagnosticIds.WrongArgumentTypeTestData, UsageCategory, DiagnosticSeverity.Error);

Expand Down
28 changes: 25 additions & 3 deletions src/TUnit.Analyzers/TestMethodParametersAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ namespace TUnit.Analyzers;
public class TestMethodParametersAnalyzer : ConcurrentDiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(Rules.NoDataSourceProvided);
ImmutableArray.Create(Rules.NoDataSourceProvided, Rules.UnsupportedSpanParams);

protected override void InitializeInternal(AnalysisContext context)
{
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
context.RegisterCompilationStartAction(compilationContext =>
{
var readOnlySpanType = compilationContext.Compilation.GetTypeByMetadataName("System.ReadOnlySpan`1");
var spanType = compilationContext.Compilation.GetTypeByMetadataName("System.Span`1");
compilationContext.RegisterSymbolAction(
symbolContext => AnalyzeSymbol(symbolContext, readOnlySpanType, spanType), SymbolKind.Method);
});
}

private void AnalyzeSymbol(SymbolAnalysisContext context)
private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol? readOnlySpanType, INamedTypeSymbol? spanType)
{
if (context.Symbol is not IMethodSymbol methodSymbol)
{
Expand All @@ -28,6 +34,15 @@ private void AnalyzeSymbol(SymbolAnalysisContext context)
return;
}

foreach (var parameter in methodSymbol.Parameters)
{
if (parameter.IsParams && IsSpanType(parameter.Type, readOnlySpanType, spanType))
{
context.ReportDiagnostic(Diagnostic.Create(Rules.UnsupportedSpanParams,
parameter.Locations.FirstOrDefault(), parameter.Name));
}
}

var parameters = methodSymbol.Parameters.WithoutTestMethodCancellationTokens().ToArray();

if (parameters.Length == 0)
Expand All @@ -41,4 +56,11 @@ private void AnalyzeSymbol(SymbolAnalysisContext context)
parameters[0].Locations.FirstOrDefault() ?? methodSymbol.Locations.FirstOrDefault()));
}
}

private static bool IsSpanType(ITypeSymbol type, INamedTypeSymbol? readOnlySpanType, INamedTypeSymbol? spanType)
{
return type is INamedTypeSymbol namedType
&& (SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, readOnlySpanType)
|| SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, spanType));
}
}
55 changes: 55 additions & 0 deletions tests/TUnit.Analyzers.Tests/TestMethodParametersAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,61 @@ namespace TUnit.Analyzers.Tests;

public class TestMethodParametersAnalyzerTests
{
[Test]
[Arguments("ReadOnlySpan")]
[Arguments("Span")]
public async Task Span_Params_Reports_Unsupported_Parameter(string spanType)
{
await Verifier.VerifyAnalyzerAsync(
$$"""
using System;
using System.Collections.Generic;
using TUnit.Core;

public class MyClass
{
[Test, Rows]
public void MyTest<T>(T[] values, {|#1:params|} {{spanType}}<string> {|#0:rest|}) { }
}

public class RowsAttribute : TypedDataSourceAttribute<(int[], string)>
{
public override async IAsyncEnumerable<Func<System.Threading.Tasks.Task<(int[], string)>>> GetTypedDataRowsAsync(DataGeneratorMetadata metadata)
{
yield return () => System.Threading.Tasks.Task.FromResult((new[] { 1 }, "hello"));
await System.Threading.Tasks.Task.CompletedTask;
}
}
""",
// The verifier uses Roslyn 4.8, before C# 13 params collections.
Microsoft.CodeAnalysis.Testing.DiagnosticResult.CompilerError("CS0225").WithLocation(1),
Verifier.Diagnostic(Rules.UnsupportedSpanParams).WithLocation(0).WithArguments("rest"));
}

[Test]
public async Task Array_And_Interface_Params_And_NonTest_Spans_Are_Allowed()
{
await Verifier.VerifyAnalyzerAsync(
"""
using System;
using System.Collections.Generic;
using TUnit.Core;

public class MyClass
{
[Test, Arguments("hello")]
public void ArrayTest(params string[] rest) { }

[Test, Arguments("hello")]
public void InterfaceTest({|#0:params|} IEnumerable<string> rest) { }

public void Helper({|#1:params|} ReadOnlySpan<string> rest) { }
}
""",
Microsoft.CodeAnalysis.Testing.DiagnosticResult.CompilerError("CS0225").WithLocation(0),
Microsoft.CodeAnalysis.Testing.DiagnosticResult.CompilerError("CS0225").WithLocation(1));
}

[Test]
public async Task Test_No_Error()
{
Expand Down
Loading