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
50 changes: 31 additions & 19 deletions src/TUnit.Analyzers/TestDataAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -857,32 +857,31 @@ private ImmutableArray<ITypeSymbol> UnwrapTypes(SymbolAnalysisContext context,
return ImmutableArray.Create(type);
}

// Unwrap Func<T> and TestDataRow<T> wrappers in any nesting order, so that Func<T>,
// TestDataRow<T>, Func<TestDataRow<T>>, and TestDataRow<Func<T>> all reduce to the inner T
// before the tuple check below. Looping handles either order without assuming which wraps which.
// Peel only the layers the runtime row reader removes: DataSourceHelpers.ToObjectArray invokes
// one outer Func<T>, then TestDataRowUnwrapper.UnwrapArray strips one TestDataRow<T> and invokes
// one Func<T> inside it. So Func<T>, TestDataRow<T>, Func<TestDataRow<T>>, TestDataRow<Func<T>>
// and Func<TestDataRow<Func<T>>> reduce to T, but anything nested deeper (e.g. Func<Func<T>>)
// reaches the test as a wrapper value and must be checked as such.
var testDataRowTypeSymbol = context.Compilation.GetTypeByMetadataName("TUnit.Core.TestDataRow`1");
bool unwrappedLayer;
do

if (TryUnwrapFunc(type, out var funcResult))
{
unwrappedLayer = false;
isFunc = true;
type = funcResult;
}

if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } genericType
&& genericType.ToDisplayString().StartsWith("System.Func<", StringComparison.Ordinal))
{
isFunc = true;
type = genericType.TypeArguments[0];
unwrappedLayer = true;
}
if (testDataRowTypeSymbol != null
&& type is INamedTypeSymbol { IsGenericType: true } testDataRowType
&& SymbolEqualityComparer.Default.Equals(testDataRowType.OriginalDefinition, testDataRowTypeSymbol))
{
type = testDataRowType.TypeArguments[0];

if (testDataRowTypeSymbol != null
&& type is INamedTypeSymbol { IsGenericType: true } testDataRowType
&& SymbolEqualityComparer.Default.Equals(testDataRowType.OriginalDefinition, testDataRowTypeSymbol))
if (TryUnwrapFunc(type, out var rowFuncResult))
{
type = testDataRowType.TypeArguments[0];
unwrappedLayer = true;
isFunc = true;
type = rowFuncResult;
}
}
while (unwrappedLayer);

if (type is INamedTypeSymbol namedType && namedType.IsTupleType)
{
Expand Down Expand Up @@ -1191,6 +1190,19 @@ private bool IsEnumAndInteger(ITypeSymbol? type1, ITypeSymbol? type2)
return false;
}

private static bool TryUnwrapFunc(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? innerType)
{
if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } genericType
&& genericType.ToDisplayString().StartsWith("System.Func<", StringComparison.Ordinal))
{
innerType = genericType.TypeArguments[0];
return true;
}

innerType = null;
return false;
}

private static bool IsIAsyncEnumerable(ITypeSymbol type, Compilation compilation, [NotNullWhen(true)] out ITypeSymbol? innerType)
{
innerType = null;
Expand Down
121 changes: 121 additions & 0 deletions tests/TUnit.Analyzers.Tests/MethodDataSourceAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,127 @@ public void MyTest(int a, int b, int c)
);
}

[Test]
public async Task Method_Data_Source_With_Func_TestDataRow_Func_No_Error()
{
// The runtime invokes one outer Func, strips one TestDataRow, then invokes one Func inside it,
// so all three layers are removed before the value reaches the test (#7227).
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System;
using System.Collections.Generic;

public class MyClass
{
[MethodDataSource(nameof(GetData))]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<Func<TestDataRow<Func<int>>>> GetData()
{
yield return () => new(() => 1);
}
}
"""
);
}

[Test]
public async Task Method_Data_Source_With_Nested_Func_Reports_Mismatch()
{
// The runtime invokes only the outer Func, so the test receives a Func<int>, not an int (#7227).
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System;
using System.Collections.Generic;

public class MyClass
{
[{|#0:MethodDataSource(nameof(GetData))|}]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<Func<Func<int>>> GetData()
{
yield return () => () => 1;
}
}
""",

Verifier.Diagnostic(Rules.WrongArgumentTypeTestData)
.WithLocation(0)
.WithArguments("Func<int>", "int")
);
}

[Test]
public async Task Method_Data_Source_With_Nested_Func_For_Func_Parameter_No_Error()
{
// The inner Func<int> reaches the test as a value, so a Func<int> parameter matches.
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System;
using System.Collections.Generic;

public class MyClass
{
[MethodDataSource(nameof(GetData))]
[Test]
public void MyTest(Func<int> value)
{
}

public static IEnumerable<Func<Func<int>>> GetData()
{
yield return () => () => 1;
}
}
"""
);
}

[Test]
public async Task Method_Data_Source_With_Nested_TestDataRow_Reports_Mismatch()
{
// The runtime strips only one TestDataRow, so the test receives a TestDataRow<int> (#7227).
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;
using System;
using System.Collections.Generic;

public class MyClass
{
[{|#0:MethodDataSource(nameof(GetData))|}]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<TestDataRow<TestDataRow<int>>> GetData()
{
yield return new(new(1));
}
}
""",

Verifier.Diagnostic(Rules.WrongArgumentTypeTestData)
.WithLocation(0)
.WithArguments("TestDataRow<int>", "int")
);
}

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