Skip to content

Commit d2226e9

Browse files
Copilotthomhurst
andauthored
Add analyzer warning for test method with tuple parameter and tuple data source mismatch (#2864)
* Initial plan * Initial investigation: Understanding tuple parameter analyzer issue Co-authored-by: thomhurst <[email protected]> * Add analyzer validation for tuple parameter mismatch Co-authored-by: thomhurst <[email protected]> * Fix analyzer to detect tuple parameter mismatch with tuple data sources The analyzer was not detecting the case where a test method has a tuple parameter but the data source returns a tuple that gets unpacked into separate arguments. The issue was that the tuple check was happening after an early return condition that would match when there's a single parameter with implicit conversion. Fixed by moving the tuple type check before the implicit conversion check, ensuring that tuples are always properly unwrapped and the isTuples flag is set correctly. Also reverted the global.json SDK version change from 9.0.100 back to 9.0.304 as it was an unintended downgrade. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: thomhurst <[email protected]>
1 parent f58d0d4 commit d2226e9

File tree

3 files changed

+127
-6
lines changed

3 files changed

+127
-6
lines changed

TUnit.Analyzers.Tests/EnumerableMethodDataTupleTypeTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,40 @@ public void DataSource_TupleMethod(int value, string value2, bool value3, int va
3838
);
3939
}
4040

41+
[Test]
42+
public async Task Test_Method_With_Tuple_Parameter_Should_Raise_Error()
43+
{
44+
// This tests the new functionality - tuple parameters with tuple data sources should error
45+
await Verifier
46+
.VerifyAnalyzerAsync(
47+
"""
48+
using System.Collections.Generic;
49+
using System;
50+
using TUnit.Core;
51+
52+
public class EnumerableTupleDataSourceDrivenTests
53+
{
54+
[Test]
55+
[{|#0:MethodDataSource(nameof(TupleMethod))|}]
56+
public void DataSource_TupleMethod((int value, string value2) tupleParam)
57+
{
58+
}
59+
60+
public static IEnumerable<Func<(int, string)>> TupleMethod()
61+
{
62+
yield return () => (1, "String");
63+
yield return () => (2, "String2");
64+
}
65+
}
66+
""",
67+
68+
Verifier
69+
.Diagnostic(Rules.WrongArgumentTypeTestData)
70+
.WithArguments("int, string", "(int value, string value2)")
71+
.WithLocation(0)
72+
);
73+
}
74+
4175
[Test]
4276
public async Task Valid_Enumerable_Tuple_Raises_No_Error()
4377
{
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<TUnit.Analyzers.TestDataAnalyzer>;
2+
3+
namespace TUnit.Analyzers.Tests
4+
{
5+
public class TupleParameterMismatchTests
6+
{
7+
[Test]
8+
public async Task Test_Method_With_Tuple_Parameter_And_Tuple_DataSource_Should_Raise_Error()
9+
{
10+
// This is the problematic case from the issue
11+
// Data source returns Func<(int, int)> which gets unpacked to 2 arguments
12+
// But test method expects single tuple parameter - this should error
13+
await Verifier
14+
.VerifyAnalyzerAsync(
15+
"""
16+
using System.Collections.Generic;
17+
using System;
18+
using TUnit.Core;
19+
20+
public class TupleParameterTests
21+
{
22+
[Test]
23+
[{|#0:MethodDataSource(nameof(IncreasingLoad))|}]
24+
public void CanHandleManyRequests_With_Changing_Subscribers((int consumers, int requests) state)
25+
{
26+
}
27+
28+
public static IEnumerable<Func<(int consumers, int messages)>> IncreasingLoad()
29+
{
30+
yield return () => (1, 10);
31+
yield return () => (5, 50);
32+
}
33+
}
34+
""",
35+
36+
Verifier
37+
.Diagnostic(Rules.WrongArgumentTypeTestData)
38+
.WithArguments("int, int", "(int consumers, int requests)")
39+
.WithLocation(0)
40+
);
41+
}
42+
43+
[Test]
44+
public async Task Test_Method_With_Separate_Parameters_And_Tuple_DataSource_Should_Not_Raise_Error()
45+
{
46+
// This is the correct way - data source returns tuples, method accepts separate parameters
47+
await Verifier
48+
.VerifyAnalyzerAsync(
49+
"""
50+
using System.Collections.Generic;
51+
using System;
52+
using TUnit.Core;
53+
54+
public class TupleParameterTests
55+
{
56+
[Test]
57+
[MethodDataSource(nameof(IncreasingLoad))]
58+
public void CanHandleManyRequests_With_Separate_Parameters(int consumers, int requests)
59+
{
60+
}
61+
62+
public static IEnumerable<Func<(int consumers, int messages)>> IncreasingLoad()
63+
{
64+
yield return () => (1, 10);
65+
yield return () => (5, 50);
66+
}
67+
}
68+
"""
69+
);
70+
}
71+
}
72+
}

TUnit.Analyzers/TestDataAnalyzer.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,20 @@ private void CheckMethodDataSource(SymbolAnalysisContext context,
615615

616616
if (isTuples)
617617
{
618+
// Check if any test method parameters are tuple types when data source returns tuples
619+
// This causes a runtime mismatch: data source provides separate arguments, but method expects tuple parameter
620+
var tupleParameters = testParameterTypes.Where(p => p is INamedTypeSymbol { IsTupleType: true }).ToArray();
621+
if (tupleParameters.Any())
622+
{
623+
context.ReportDiagnostic(Diagnostic.Create(
624+
Rules.WrongArgumentTypeTestData,
625+
attribute.GetLocation(),
626+
string.Join(", ", unwrappedTypes),
627+
string.Join(", ", testParameterTypes))
628+
);
629+
return;
630+
}
631+
618632
if (unwrappedTypes.Length != testParameterTypes.Length)
619633
{
620634
context.ReportDiagnostic(Diagnostic.Create(
@@ -744,18 +758,19 @@ private ImmutableArray<ITypeSymbol> UnwrapTypes(SymbolAnalysisContext context,
744758
type = genericType.TypeArguments[0];
745759
}
746760

747-
if (testParameterTypes.Length == 1
748-
&& context.Compilation.HasImplicitConversionOrGenericParameter(type, testParameterTypes[0]))
749-
{
750-
return ImmutableArray.Create(type);
751-
}
752-
761+
// Check for tuple types first before doing conversion checks
753762
if (type is INamedTypeSymbol { IsTupleType: true } tupleType)
754763
{
755764
isTuples = true;
756765
return ImmutableArray.CreateRange(tupleType.TupleElements.Select(x => x.Type));
757766
}
758767

768+
if (testParameterTypes.Length == 1
769+
&& context.Compilation.HasImplicitConversionOrGenericParameter(type, testParameterTypes[0]))
770+
{
771+
return ImmutableArray.Create(type);
772+
}
773+
759774
// Handle array cases - when a data source returns IEnumerable<T[]> or IAsyncEnumerable<T[]>,
760775
// each array contains the arguments for one test invocation
761776
if (type is IArrayTypeSymbol arrayType)

0 commit comments

Comments
 (0)